This commit is contained in:
Nick Beckley 2026-06-01 14:05:39 +01:00
parent 8f56934dd8
commit 624ec9a394
51 changed files with 1074 additions and 102 deletions

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS6Som2G3qOumXsSLtOiN5IxsIVpqQVGbBsb+94DaDRL31psw0Dsfz7zDNxLdAUlPbo/HX04Y80JqksctJJjQlGnK+J7zXvCes4ZJLUUr/yE1GR/xK+mJUSHwYJFK1EgVsJZ6OQQKgJYzDky0quR0Lwvp34gbG8DTGLCZl0+YlnGOpCd/jRHmL6W2G5swzSs0aZ7wIS7ZxVCa+fPh7pH+cbvbP+0ufy24FHEzAukJ81xxi4x6azVVEgztDHd0QPBdGLTWwVxkY8yYopuaBT7/qA9zjphDLxohG0aFJeeagMuutOzdgvuvx9LFhui+xOstvH32b+SEifSi/g7zku4mTONNno8TnnA6urRgugnz4eAikJoc03zElEfcsGyTTpPL4xyra7G7acSYSU2enqvd5PIwp8M7Vs8XBA//g8AJkyc9kayV2hhUUkpntRcMweigtfDBMTN4gTwga60ylhvGpRXMylabzoBjNpCavEyzd9Pvy8bPCuY7YEZogQqtUFrbwFB2OLQGGTitve6CBdkZoTropLPOcevRBKtDMcgHF17HiNXDfZmGowDZIR0E11R14KhrpaVaSz0gD9YITmpyN8c8xnVel+ohZnxJGz/Sk1/dtOCF3G0Kr2PGkNdUoP6tS7FdmmENufrhgOLtRjJWRGs+rrn6iDAmDBkL3n1ar/n+IxFabKK7NSWMudq5/Fr95pbXQsV1gTtbRtUhDNgyHBgwp5kB1KARlAADyirQyrKglfNcgWu5H0qE1ASL4arN485yJQZsKUJrqOqCoz4AUPSSMZTetvCeY/U0bbrQMYUgqcUBqLLcUaukpCBbZ4QejBHDT0iu5uOsAAY8Xa5160ADdAiaU6F4S5W1nHa2NdSbzluvO2xD6fkRU8TpJ1AfxpDmZR5ydfVu9Q7dxzWWF6fafPxljPTfbUf3c/iMqSo7dmMsB3Zd67kmB3TLmvCwPVj9G9mV66r227slTMNlJ2vynF3K6/EalraxUprz+Xz+BgAA//8DABTuqPX1BAAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUS2/bOBD+KwLPpkJKFCUK2IPhtFgjTWMk7u5ibxQ5jInIpEFRToMi/31BWa6dooe9CaN5fY/hD+TkHlCLlofDpyO4OKAFinYKFaTgmHBM6JbStqAtJTlrOKVV+S9aIHsHb6hFNVOKKgG4BA6YaVLhrjQKaw2CEqpJUbHUUz4PqP2BpM01HK2C3A9/QRisd6hFf1un/WuaLW2uej/qPPge1m6I0qm0zNf16g5/WW62D5tT1gBDKs6tRi0iHWVUAMGdEByzUte4qanEBnTXKMM5V/WpzLoIwck+H/TLZb720UFUbZEXZU5wIdD7AmkZZVq5kwNs3w5pi4mi2xRfTOHps/2BjhBQWyzOZJ7a3aje3uyHbrS9vokyPEM0Qe7h1YcXOMoeLdAh+AOEaGHi5uEpu6xESU7ygheEoAW6l2pnHWTr2+yhT4ClaggDXWIBRmMmqMSClSXWZSXrgpu6LgxaoDsIDvqrrvdWBT94E7OZ8uzDnJV30brRj0O2dhGeg4ynus+yHwAt0GbsejvsHkHqt61/HNM/N/YJy2oMAVzMNjLusj/lsEMtErJRVAoppORKGagIGKKJ5KTWwDUHzQpdayaY5kwQxZnsKNOyop1JEbRAD2M8jPHM//erJbbB7vegLxtsJ443vYzGh/1ag4vW2CTNOWMyVN9PqLK556q34OIHkpOjKBS6bACbgnLMGi2xrEqBOS+5AapEXdATYcY+jz95IsZoVpqqaXgnOkqgMHXDSG06opUpldBVB3VRk7qBRihCuorRphOKaKAV0wld8HpU8VcrlJNAj6NL15mtE+xX6/B3zi6MbIKPXvk+CWZ7eHobIux/p9rK7w9+sDHhN7O03wa49y79ne9/5mwuXvp4iUG6g2w6PUEqTXklMFXQYcaMxp2uOyxEaaQBU/A6obr16gVClgwmrZs0OXvq4SlbBrWzEVQcQ5r8zwTqJOfn88lc+Mi/ftqufIDl4bCYo38cE0mnZmcDfHhXvg0pfeeHeAV5hvYEvTkvdmUnDcfTU2VTkGvdgOYUF4xWmAlBcSOqGnd104mON1Cpi12zR9A2gIpTv20Y07SZ2d/5chmiNVLFIR3PF68mO83+nFN+XfJKtp/Vp+FX0s1WmjtRkiz7dHo4Jwf9r3fz/f39/T8AAAD//wMAnt4S3SgGAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS6Som2G36MJNvYgXbdDbiDOKhWilBUVtGhj73wtqt0jcW27CQDPzzjMv541NcCTWsvvT6ZczTWlhJUvDFlJCWS4sF/IgZatkK0UlnLFCqr9ZyYYn+sZa5kwIMnjimixxg6Lmne4DRyQvhUShapNrwueFtW8MhgrpPASq5uVPisswT6xlfw0Tzl9zbxiqMM4rVnEeaTctCaaQxfyxe3jiv9/vD8/7618LLTm5GpC1THTSSE+Cd95bbjQ63jgJvCfsmtBba4O7pg1TojjBWC345Xt/nNNEKbSqUroSXHl2KRlCgiy5g4UO305ZxYboMcfLLbx9tm/sTJG1qvwP5rXcXRiHu2XtRjrTeIK4ULwL8/EIE7KSneJ8opgG2rCcKXbzMqRMNAQHTSMlNuBM7bDRtdIOtSBshKvRowdrbK9CXffYNd6Z3qCSBi0Z0+VBN3DjCGmYp+Km/WEcaEqsZM8vxX6E1M/x+A79y5Xo7ieAZtmsZVrU2jpHRmsN3nZKEDobrFVdAOH6TpEMJGpvnJdOSO2V8Lq2rnEIwgdWsg8QXoeJit1j7i5JoW6I90pabhoEDrX23Fpte5LBOyVZyR7mKQ3TOq9LsZsSfY7bvKxlv8K40HXS+xheh0QhrTFD+GSzGfdxxjWk4rsBsrUrLUROWtNpTcVHwiFSSJRxHOJ6q/e/FGXVlvSwxkhTKvaQXovfYHllLfPQBAkePIANoadaUC9QgBUOyaIlNAodGm/QGi+CNdBJg1DLrs8RVjLKfis2i4faUm2U5x30PTekDIcAxI1H4YPoXK3oHcfiecx5EBphCDX31CM3XgL3RmuOuganbO+c6n9AcjOLFBkw0vn6WDcFFrEhtJIrI2tuvJe88bXjnWs639mG6pA1P1GcaPwB1IchxHmZ+1TcvFa8Q/dxnfLBKTbffR0m/s+2o8c5fKFY5B3DMOX3dVvrpWRHgmWNdNzuVfvG9vlxFYftbClXSd3okr0kiGk93cLaV15rd7lcLv8CAAD//wMAfUhktfQEAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTwW7bOBD9FYFnUSEpihJ1C5xd1Ei7MdqgXextxBnVRBXKoKi0ReB/X1B20aa33ohHvpk37w1fWIAnYj27PZ3+eqaQFlay5DdICWW4MFzIRyl7JXvZVJ2sO6Hq/1jJ/D19Zz1rtXPSWeI1GeIaRcOHenQckawUEoVqdK4JnxfWvzDwFdKzd1TNy0eKi58D69knH3D+mnuDr9w0r1jFeaJ9WBIEl8X8s9/d87e3h8eHw+XVQksmVx5Zz8QgtbQk+GCt4brGlnetBD4SDp0bjTGuvdB8SBQDTNWCX372xzkFSq5XlaorwZVl55IhJMiSB1jo8fspq9gsust4ucHbsX9hzxRZr8ofZl7K3bjJ37j56QkC3ow++OXISnaK84li8rTZsU04TZD8HIprk93kKSRWsocPxWGCNM7x6ZVHHy6j7/9g8nfgjj5Qsb/LHEkK6474qKThukPg0NSWG1ObkaSzrZKsZLs5JB/WeV2KfUj0OW4qWc/+hmmhi77b6I4+kUtrzNL/NTlr+ubTbsYMCFayQ5xxdan4abcUlahqkS8f1nRaU/Ge0EdyifJMj3G9lv+NoozaSLs1RgqpOEA6Fm9gObKeWeicBAsWwDg3UiNoFCjAiBbJoCHUClvUVqPRVjijYZAaoZHDmJGsO6dbXBYKGzsMI/EOFHFtleEWB8tNLRAGQLSDe2Vr8TBlHrhOaMKaWxqRayuBW13XHOsGWmXGtlXjL5ZcE5ci+430fPkamwKD2BEayZWWDdfWSt7ZpuVD2w12MB01Lmu+pxho+sWod97FeZnHVFwXpnhl3fs15O9dbMvz1Qf+bYvsbnZfKBY5cvAhb/M15fP5fP4fAAD//wMAHpFmlyUEAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTTW/cNhD9KwLPokxSFCXqZqxTdOEmXiSLtOhtxBnFhGVqQVFOAmP/e0DtBk5664UgBnwz72P4ygI8E+vZ7en07oVCWljJkt9KSijDheFCHqXslexlU3VSKqmbf1nJ/D19Zz1rtXPSWeI1GeIaRcOHenQckawUEoVqdO4JXxbWvzLwFdKLd1TNy2eKi58D69nfPuD8Nc8GX7lpXrGK80T7sCQILpP5sN/d879uD8eHw+XVQksGVx5Zz8QgtbQk+GCt4brGlnetBD4SDp0bjTGuvcB8SBQDTNWCT2/zcU6BkutVpepKcGXZuWQICTLlARY6fj9lFptFd7lebuXt2r+yF4qsV+VPMy/tbtzkb56XYfUT3lzOBMtTVnmK84li8rR5ssmcJkh+DsV10m7yFBIr2cOn4jBBGuf4/JtRny769/9D/jFPf/eN3JoId/MaEuuZrC0r2Xtwjz5Qsb/L7SQprDvio5KG6w6BQ1NbbkxtRpLOtkqyku3mkHxY53Up9iHRl7gJYD37A6aFLtRvo3v0iVxaY1b1j8m7cIgzri4Vb/5LUYmqFiKD1nRaU/GR0EdyibK+Y1yv/f4DUUZtoN0aI4VUHCA9Fn/C8sh6ZqFzEixYAOPcSI2gUaAAI1okg4ZQK2xRW41GW+GMhkFqhEYOY66wklGOu9g2TCoYYFSSkxoF1w0MfOiM5LUgRa4ZZNdmIm8+Fg9TxoHrhCasuaURubYSuNV1zbFuoFVmbFs1/mLJNX0pssFIL5e/sjEwiB2hkVxp2XBtreSdbVo+tN1gB9NR4zLne4qBpl+Meu9dnJd5TMV1eYrfrPu4hvzfi22RvvrAv20Z3c3uiWKRMwYf8npfYz2fz+cfAAAA//8DAHg1vGQ2BAAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUy27kNhD8FYHXiDJfokTdHNuLDBw/sDNIgtyaZMtmLFMDirJ3YfjfA2ns9WMRIBdBaJJd1dXV/UQi3CPpyPF+f/aAMU+kJDmsIcGEpkxTxnecd4J3vK5aLo3R8m9SknCO30lHGuUcdwapRI1UeVZTK3tHvUfDGfdM1GrJCTcT6Z4IhMrjQ3BYjdMfmKYwRtKRP0P04+OCDaFywzj7Ko0DbuKUIbqFzOXm5Jz+fny9u7o+3JpwWh5XwZOOMMsVN8ioNUZTJX1D24YD7dHb1vVaa9ccnoWYMUUYqsnfveH7MUfMrhOVkBWjwpDnknjIsFC2MOHu+35hsUp0usTLNbz+dk/kARPpRPkq5iHdkRvC0f1k5zD4o/VLSrJP4x5TDriqcbUt3khwVrFKaMEYKckFuNsQsdicFlfDUiK4lin0khrsPVWGAzVKSuplDY3QfdOInpTkHFPE4V3Wi+DSOI19Ll5ELj7gbI9PziLYAReMLzBMSEqymbYZoodhjHj2Dd2cD7l2aV6OT8aYQ5zHeSo2MeNNgpfz1/cnc0oYc3EN+bb4DaZb0hEDreNgwABo53qsGfbMM9Cs8ai9Rq+Eb7wyymtlmNMKLFceam77JUJK8uui4XZ2Dqfpjc1qkmFYORQvfToZAsb8QcbFJRyFly3SXnBNVeuBQi0N1VrqHrkzjeCvMDtIN5hJR/rWWsucs0IZ7kAYkL5n0uveCNE0oHpZN0poXbMGnG8awZWXDTqQvWStICW5TqOfXf7cark24DqN/6DLi1SkI5rVdevAWqEVb8G0qLhWrTbYGNUDU9JZ4HUL1nIGNXOmFQ64NqCsr/sfKp3FmxDxNEz7Ab6/w20rXclfHEDLe1Cfrn9JcI+PY7q7PNi4ujzbkZJ8neOyEYrNYpHHEOk3vUz0xTzksLtNCB79xejxZx/hMi7FOqHcIlimLLVaaKq4kdSwHqljgjGrFce2JSU5Hd0dpmJxGIS4TNWPZFfb4ji525DR5Tkt/P5aeVxti+sBcj+m+w+b5F1hnwWohORs6YzHh8M2Wjlq71v0mlOheE2VMZy2pm6obVprrG6xdotiV3Pez7n4ij4kdHmt+MWLK+bJLbq7n8V4dcGLRTlbvLY9bLFV2v+1xJ5Lco8wzQnv123dPZFNjJhW5NP5MIqbeBGGIUzoxugn0tUNbyuhpXph+N/3GlHx2vDn5+fnfwEAAP//AwCXJBKUGwYAAA==

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRU0W7cKBT9FYtn4wDG2PgtmrTaUbabUTvqrvYNc68bFAdGgNNG0fz7Cs+s2jyt9gWha87l3HMOfiPePCMZye3p9OEFfU6kJtltJcGEokxRxo+cj4KPvGsGNgyKib9JTdw9vpKR9NJabjXSFhVSCayjUztbCoCaMw5MdLL0NN8SGd+IcQ3gi7PYhPQVY3LBk5H86TyE7+Vu4xq7hBWaGBbc+5SNt4XMH/vdPf399nB8OFxOJUwF3DggI2ETl1wjo5PWisoWejr03NAZYRrsrJSy/QXmfMbozdIkePp5P4TsMdtRNKJtGBWanGsCJptCeTIJj6+nwmKT6K7U6628bcc38oKRjKL+V8xLuxu7uJvnNK1ugZvLmk16Sts6G5tDfCU1OcVwwpgdbvrcpoTP0/J6NOnp4+VM2aYPP9CuGWEXVp/JSHirSE02gZbFZBd8deW4Wxz6vH3M0fnk7H/1aklNHr5Uh8XkOcTnd358uci8/x8qfzL20Xms9ncFw1FAOyCdBVdUDmCo6VpNlWrVjNzqXnBSk13w2fk1rKna+4zf4jYSGclHsyS88LuN9tFltHmNZc6/VMnVIQZYba5+eslZw5qWsQJa82nN1WcEF9FmLEMc43rt9/U9RCixgXZrjOhzdTD5sfrNpEcyEm0Gy4022hhl7Ywdw5kBM4r1gAoUghTQg9QSlNTMKmkmLsF0fJpLhdQES3SqLa0aejPA0NOug45K0fd0kJ2iknM7Gd2ynsM7HauHpeCMHZhEaKnGGajU3FAt25ZC25leqLnvxfyLJNc8cFYEBny5vLuNgQIYEBSnQvKOSq05HXTX06kfJj2pATtbON9j9Lj8ItQnZ2NIYc7VNSHVO+k+r778O6otLd+dpz82j+6CfcJYFY+N8+WpXG09n8/nfwAAAP//AwDwEAjXggQAAA==

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSU227cNhCGX0Xg9VLm+aA7w25R102ziI226B3JGdlCtNKCojYNjH33gNotEveud8JA/xy++YdvZAoHJB25PR5/OuFUFrIjZdhCgglDmaGMP3PeCd4J23KhBWPyb7IjwyN+JR2xKiWePFKJBqkCpmmUfaIA6DnjwIRWNWd4WUj3RsLQAp6GhO28/IF5GeaJdOTPYYL5S60dhjaN8wptnkd8mJYSplSb+f3h7pH+drt//ri//LXgUsXtAKQj2qXeRB2ot0JThQ6ogxio1lwxLrVXhl9kw1QwT2FsF/j8vT7MZcKSOtEK2TIqPDnvCIQSassxLPj89Vi72BDd1/huC2+f3Rs5YSad2P0L85LuJo3DTZmPI55wPIa8YL5J8+EQJiA7cszzEXMZcMOyTTqOoQzz1FyL3Y0DToXsyMenZj+G0s/58I7V0wXBw/8gcMIca4OAARBcAIdKeSl76dFYEZxzBpSPgTMrHVgXATn3UfRok0xC95AU1IW+jHMM46/Lxs8LFh0wK4xAhV4oY3xiKB322iKDYEw0LnmQzgrlwMngQ+A+ok3epJrvQ0ivw4TNwz3pCOMoQDqkveCGKgeBBi09NUaaHnnyVtRp7uapDNM6r0vzMBV8yRs/0pGfw7jghdxtTq9DwVTWXKH+ZWqxfZ5hTaX57gDOWtZKxqpoLce1NJ8QhoypYMX7nNdrvv9IhBGb6G7NGafS7EN5bX4Jy2vdibFCK6ZS0JKjSL0XrGfMeSlACUzWAFohUlTK97KXOsWQonV9RJDR1gmxGq7ZPC41GsVUT9EKRhVoTb0TjDIAlXw0BhK849h8HKsuJMcUgqQee6DK80C9kpKC1MEK01sr+h+QXM3HWS0PeLpc69aBAXAIhlOhuKbKe06d15ZG66KPxqFOnuzII+YJxx9AfRhSnpe5L83Vu807dJ/Wqb44zebjL8NE/9l2dD+nz5ibuuMwTPXArms978gBw7JmPGwPVvdG9vW6muft3RK29ZKJHXkqIZf1eA1r31rh9fl8Pn8DAAD//wMAJXTPRPUEAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSU227cNhCGX0Xg9VLm+aA7w25Rw029iI026B3FGcZCtNKCojYNjH33gtotEvcud8JA/xy++YdvZAoHJB25PR5/OeFUFrIjZdhCgglDmaGMv3DeCd4J23IjnXH+b7IjwyN+Ix2xKkYePVKJBqkCpmkvU6QA6DnjwIRWNWf4vJDujYShBTwNEdt5+RPzMswT6chfwwTz11o7DG0c5xXaPI/4MC0lTLE288fD3SP9/Xb/8rS//LXgUsXtAKQj2sVkeh2ot0JThQ6ogz5QrbliXGqvDL/IhqlgnsLYLvDle32Yy4QldqIVsmVUeHLeEQgl1Jb7sODLt2PtYkN0X+O7Lbx9dm/khJl0YvcfzEu6mzgON8vaj3jC8RjygvkmzodDmIDsyDHPR8xlwA3LCXM/L0OpRGO0wTnOwQWrtAUntZAWJENwzGrw4INRJomodYLeeauSAsEVGFSqt2RHNnDjGMowT82197txwKmQHXl6bvZjKGnOh3fony9EH34CaG27zgsYAMEFcKiUlzJJj8aK4JwzoHwfOLPSgXU9IOe+FwltlFHoBFFB9ceHEF+HCZuHe9IRxlGAdEiT4IYqB4EGLT01RpqEPHoravW7eSrDtM7r0jxMBT/nbV7SkV/DuOBl0tscX4eCsay5QvhkarF9nmGNpfluAM5a1krGqmgtx7U0HxGGjLFgxfGS12u+/0mEEZvobs0Zp9LsQ3ltfgvLa2VorNCKqRi05Chi8oIlxpyXApTAaA2gFSL2Svkkk9SxD7G3LvUIsrd1Qqx+azaLW6GSs8lRC32iikdOe+0DxeCs9MaEqOw7js3TWHUhOqYQJPWYgCrPA/VKSgpSBytMslakH5BczcJZLQ94uhzr1oEBcAiGU6G4psp7Tp3XlvbW9b43DnX0ZEceMU84/gDqwxDzvMypNFevNe/QfVyn+uA0m+++DhP9Z9vR/Ry/YG7qjsMw1fu6rvW8IwcMy5rxsL1X3RvZ1+NqXrZnS9jWSyZ25LmEXNbjNax9a4XX5/P5/C8AAAD//wMAcuMez/QEAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUXW/bNhiF/4rAa1PhlyhRd5mzYkbaJm28dtgdRb50iCikQVHpgiD/faDsIHaLXexOoHjej+cc6QUF/QioR5f7/e9PEPKEVij75YgRJjGRmNAtpT2jPWtr2QrVSvU3WiF/Dc+oR60whhoFmIMELCxp8MCdwdaCooRawhpRaurdhPoXpH1t4ckbqOP0DdLkY0A9+u6DjT9Kb+1rM8bZ1imOsAlT1sGUYT5v1tf44+Xt9ub2cGuCqYhrb1GPms44OTQaq5Y1WEBncWcHjZuGCkJ5o4SkB5kPGVLQYz3Zh/f+NuYA2fSsZrwmmCn0ukJWZ11GHvQE2+d9mWJBdFXOV8vx8ti/oCdIqGerN5iHchdm9BeP0zD70V6McbfzYbeOwfndnHQujVdon+IeUvawwPnNB52eP8bdrtRDH/Q4AVqhLaRHH/R4eHEFTs9jvotzWshM9uG/7qAe6TlHtEILyXFc2lbHZdajh5B/0Z40vrmrbkedXUyPZybdHdhv/gf6T9rc+wDV5gr1iFBglneAHaMSi85qrBuusJRcOqBGtaxo1jFkH+Y4T9UmZNgdqZ3Nd5nMvc9g8pzKTn/JErbbFO1scvVuMCU1qTkhRTTn/Zyrr2B9ApOhLLFN87HeTxIm2SL64Ef4Bc96TglCrm51vq/+0NN9wSFb1ggijG44BWacYsQR0inOrGBgWmmhZcwMQijHHW/MoM3Qdm4Ay4e2rA0lZNWSa+DSAucEUyscFppYPAAD3DlrXNMRRR07g1vdjEWnTUcEWI4VOIuFohorwTm2vNEtk65tmTvhtD0EgpLS/jSEf05gj1n6fIj22+4Wng7f8TKntLYDKylmgjZYKEVxp5oWD203qEF20Bh1MHSKbyC/QRri5HP5iXyZPZQknl149+UaUoDxxJtP3qQ4RZerYyirM7e+zqH8w6oloD98wP8ssbiK5gFSVWKlfTix8vX19fVfAAAA//8DAItNHA8KBQAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUTW/bOBD9KwLPpkJSlEQa2IPhtFgjTWMk7u5ibxQ5jInIokFRToPA/31BWY6dooe9CaP5ePPeG76jTu0AzdFiv/9ygC72aIaiG0OMsAqTChO6oXTO6JzVuZBlUVXiXzRD7g7e0BzVXGuqJeACKsDckBI3hdXYGJCUUENYyVNP9dyj+TtSLjdwcBpy3/8FoXe+Q3P0t+uMf02zlct16weTB9/Cquuj6nQC8321vMPfFuvNw/qU1UOfinNn0ByVQtuqKRWWNSsxB2GwMI3CZUk5oUUpeUVPZa6LEDrV5r15ucw3PnYQ9ZzlrMgJZhIdZ8ioqBLkRvWwedsnFCNFtyk+G8Pj5/wdHSCgOZudyTy1u9Gtu9n1zeBacxNVeIZog9rBqw8vcFAtmqF98HsI0cHIzcNTdoFESU5yVjFC0AzdK711HWSr2+yhTQsrLQgHU2AJ1mAuqcKSFwU2RalqVtm6ZhbN0B2EDtqrrvdOB997G7OJ8uzTnKXvousGP/TZqovwHFQ81X1VbQ9ohtZD07p++wjKvG3845D+dUObdlkOIUAXs7WK2+xP1W+TLlXNSk64VmVBgWkrGbGECFkwwxnoujJQM6YbzqUtbFHqRummFrYBUzR10uxhiPshnvn/eQViE9xuB+aCYDNyvG5VtD7sVga66KxL0pwzRkO17bhVNvVctg66+IlkNEeEAjOFAGwZrTAXRmFVFhJXVVFZoFrWLKFb+s665+GDJ2Kt4YUthaga2VACzNaCk9o2xGhbaGnKBmpWk1qAkJqQpuRUNFITA7TkJm0XvBl0/NUKxSjQ49Cl68xWae1X1+GfFb8wsg4+eu3bJJhr4emtj7D7nWpLv9v73sW0v52k/dHDve/S3+n+J86m4oWPlxikO8jG0wNVc8NAYqaAYy4KwELSGlsJohCNJpKmrW69foGQJYMp142anD318JQtgt66CDoOIU3+Z1zqJOfX88lc+Mi/f9ksfYDFfj+bon8cEkmnZmcDfHpXfvQpfev7tEUMw5WPnqC1Z1xXbjJwOL1ULgUrYwSYimLGaYm5lBQLWda4qUUjm0pAqeWHW7NHMC6AjmO/zWnaROzvbLkI0VmlY59u55vXo5sme04pv4K8Uu2j+jT8SrnJSVMnSpJjn07v5mig//VsHo/H438AAAD//wMAmj/F9ScGAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxU227cNhT8FYGvXcq8i9SbazvownVsZBdt0TdeDm3WMrWQKCeB4X8vqLUTO0GBvggCL2eGM3POE8r2AVCPTg+Hi0fIZUYbVNK6xAhTmChM6J7SntGe6VZqaYTq/kYblC7hK+pRJ7yn3gDmoACLQCR2PHocAhhKaCBMilrT3s6of0I2tQEek4d2nP+AaU5jRj36M+Uwfq7YNrV+GJfQTuMA2zwXm30l83F7dol/P73ZX98cT80w18ttCqhHUvuonLTYdExiATpgHZzFUlJBKK+c6fFaygWmbId2Dvff8cNYMhTfs5bxlmBm0PMGBVtspezsDPuvh8pilei8rm/W5fW3f0KPMKGebV7FPJY78UM6eZjdkoZwsn7RBh2m8QBTSbCqcb1rvpOgpCUtU4wQtEFX1t+lDM32vLke6hOt10RA4NhADFgYarERnOPApe2Yil3HItqgS5gyDG+qXiU/jfMYS/MicvMOZ3d6dpGtG6BifLDDDGiDtvOu2BzsMGa4+AJ+Kcda+2mp22djLikv4zI321zgdrIv+6/3z5ZpglyaG1vumt/sfFctUh2TgghvJafAfDSMREK04SwIBr5TATrGvBPCRB659M561+noIHDXVft+rRruFu9hnr+zWUMyDCuH5sWnsyFBLu9kRD0iFFjgGnBkVGGhg8VWcoOV4ioC9aZj32D2drqFgnoUtXOOeO+YMNRbZiwPkfCgomGs66yIXHaCKSVJZ33oOkZF4B14yyMnmqENupnGsPjyo9V8NeBmGv8BX6pUqEeKSKm9dY4pQbU1GgRVQisDnRHREsG9s1Rq6xwlVhJvNPOWKmOFCzKaV/oX+TZlOE/zYbBf3+DqVrX8F2+tptGKH45/mOwDfB6n+4/HGLcfL/Zogz4tuU6EZlsj8jll/EXVjr5ahpL2dxPYAOFqDPBzjqC2S7N2qI9dpz2NuNqLhRAKGwMex+i0JoZpQyub89Hfw9TUhNmUa1d9K3a9a04nf5cK+LJMld9fK4/rXXMz2BLH6eHdJHnzsB8FaBmnpDoT4PE4jVaOKgQNQVHMBJVYGEOxNrLDrtPOOKVB+srxeimHpTSfIKQJfFlf/JLFFfPsDvz9z2K8puAlopTUrO2OU2yV9n8NsecNegA7LxM8rNO6f0LbnGFakc+XYytu81UahjSDH3OYUW8YbZUW9IXgfx4TspUdYc/Pz8//AgAA//8DAPtdxY4ZBgAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTy27bMBD8FYFnU+Fbom6B06JG2sZIjLbojSKXNRGFMigqDwT+94KyizS99UIQC87uzOzwFUXzAKhDl4fDh0eIeUIrlMNSYoQpTBQmdEdpx2jH2lq2nHBNf6IVCtfwgjrUCGup1YA5KMDCEYl77i12DjQl1BEmRelpfk2oe0Um1A4eg4V6nL5BmsIYUYe+h+jGpzLbhNoO4+zqNA6wiVM20RYyXzfra/z5cru72Z5eTTAVcB0c6pBsrVe9NFg3TGIBrcOt6w2WkgpCudRC0RMsxAwpmqGe3P3bfDfmCNl2rGa8JphpdFwhZ7IplHszwe7lUFgsFl2V+mopL9fuFT1CQh1b/THz1O7CDuHiYernMLiL05nNdF9UHtJ4gJQDLJ4sMofB5DDG6jxpPQSIGa3QzV21HUz2Y3p4Z9TdSf/mP+TvyvQPz2DnDG49zjGjDlFB0Ap9MXYfIlSbK9QhQoE53gL2jCosWmewkVxjpbjyQK1uWGm3HmMOcR7nqdrEDL/SIgB16KMZJjhRv0x2HzLYPKei6ocqWdim0c02V2/+U1KTmpPC5GbOhzlXt+BCApuh6Nul+dzvHwhTbAGt55Qg5mpr8r76ZKZ9MUU1TAoirJGcArNeM+IJaTVnTjCwjXLQMGZ7IbTnnkvbG9s3re/B8b4pCqGsu1oSpjWR0ivARDqCBeceG9oy3GhtGy+4U4y/87G6GQrO2JYIcBxr8A4LTQ3WgnPsuDQNU75pmP/LkvP2KSnjHTye/srCQDnXglMUM0ElFlpT3GrZ4L5pe92rFqTVaIWuIUUY/jLqS7BpnEafq3N4qnfW3c6x/PdqCdJTiPh52dHVaO8hVWXHJsQS7/Naj8fj8TcAAAD//wMA9x2TBjYEAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRUTW/cNhD9KwLPosxvkboZ6wRduKkXySIteqPIUUxYphYk5cQw9r8X1G6R+FT0QhBDvpk3bx75hqJ9BjSg29PpwwvEklGLSthCjDCFicKEHikdGB2Y7qSmhDHyN2pRuIdXNKBeOEedAcxBARaeSDzyyWHvwVBCPWFS1Jz2W0bDG7Kh8/ASHHRL/gophyWiAf0Zol++19o2dG5eVt+lZYZ9zMVGV8n8sd/d499vD8eHw+VWhlzBXfBoQFK7SY3SYtMziQVoj7UfLZaSCkK5NELRCyzEAinaucv+6Wd9v5QIxQ2sY7wjmBl0bpG3xVbKo81wfD1VFptEdzXebuFtO7yhF0hoYO2/Yl7S3bg53DzncQ2zv7msxeanvK2TdWVJr6hFp7ScIJUAmz63OcPzOL8ebX76eLlTt/nDD3BrAb9b1ljQgCjvUYs2gebZlrDE5spxNweIZTssKcQc3H/l4qhFD1+aw2zLtKTnd/P4cpF5/z9U/mTdY4jQ7O/QgAgF5rkGPDGqsNDeYiu5wUpxNQF1pmcVs1tiCXFd1tzsY4FvaWsJDeijnTNc+N0m9xgKuLKm2udfqvrqkBa/utL8nCUlHek4IRW0ltNams/gQwJXoDZxTOs139f3EKbYBtqtKUEszcGWx+Y3mx9r56pnUhDhrOQUmJsMIxMh2nDmBQPXKw89Y24Uwkx84tKN1o29nkbwfOxrh1Ct02xu9caB11RizX2PhdISa6MI9saDFUqNxKl3OjYPc8VZp4kAz7GByWNhqMVGcI49l7Znaup7Nv0iydUPlNTyHl4u725joLzX4BXFTFCJhTEUayN7PPZ6NKPSIJ1BLbqHFGH+RahPwaUlL1Nprg5p3kn3eY3172g2t3wPEf/YZnS3uCdITZ2xDbE+letYz+fz+R8AAAD//wMAWwyKLoIEAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUX2/kJhTFv4rF8+DlrwG/RUmrjtJtRrtRW/VlhOGSoXHwFONsV9F89xWeibLpPvXNRpzLPT/O5QUl+wSoR1fH40/PkMqMNqjEdYkR1mHSYULvKe0Z7ZlpiVJKM/MX2qB4C19Rj5RwjjoDmEMHWHgi8cCDw96DoYR6wqSoNe3DjPoXZGPr4Tk6aKf5d8hznBLq0R8x+elLPdvG1o3T4ts8jbBNc7HJ1WZ+217f4l+vdvd3u/OuGeYqbqNHPZLahW6QFhvFJBagPdZ+sFhKKgjl0oiOnmUxFcjJju3sH9/O91NJUFzPWsZbgplBpw3yttja8mBnuP96rF2siG7q+mZdXj/7F/QMGfVs8wrzXO6DG+OHvCS0Qcc8HSGXCCuD1dY42hKn1FwqX48RUjlv/Rtc2a++mHbK+MGAEMp5sCC5d8QzQThoP9BAhXAcjHbECCqJC0QzGwKT2gY9ANqgu8/NbrQlTPnpHejPZ37b/4Hvo3WHmKDZ3qAeEQrMcw04MNphob3FVnKDu453AagzilXN9ZRKTMu0zM02FXjIq2vUo5/tOF/6u8ruEAu4suSK4s+uBma0S3KH/TFPIY6wj/PeQ7DLWFCPwkW7y5NfXGneLpKSlrSckFp4KcelNJ/AxwyuQDV6n5fLmf+RsI6touslZ0il2dlyaH6x86HS6RSTgghnJafAXDCMBEK04cwLBk51HhRjbhDCBB64dIN1g9JhAM8HVSlAzU2zXmlQnWKaARaSCSwEMGys0tgpbZTojBwG8451czdWnXWaCPAcGwgeC0MtNoJz7Lm0inVBKRZqxI/HfTlH6jVJNaj+O1qXxFFCf8Sc4Z8F5jOsC+/9cpZ7eD5P7mqj816D7yhmgkosjKFYG6nwoPRghk6DdNXGLeQE43e0P0aXp3kKpblEsXnH/9OS6uvTrLH8EhP+dw3DzeQeITc1TDamOmyX/Jw26AnsvGR4Wh+v/gUdrXu0D9VKgAzJwd5NSyrrfM7+8fWPvo3aDzvJ6XQ6fQMAAP//AwDsbjNRGwUAAA==

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSTy27bMBBFf0Xg2lT4lqhd4LSokaYxmqAtuqPIUUxEIQ2KygOB/72g7CJJd90JA92ZO+cOX1EwD4A6dL7ff3qEkCe0QtkvJUaYwkRhQm8p7RjteFMTwbhm7DdaIX8JL6hDjbCWWg2YgwIsHJG454PFzoGmhDrCpCg9zd2EuldkfO3g0Vuo4/QD0uRjQB366YOLT2W28bUd4+zqFEfYhCmbYIuZb5v1Jf56vr293h7/mmAq4to71CHZ2kH10mDdMIkFtA63rjdYSioI5VILRY8yHzKkYMZ6cvdv813MAbLtWM14TTDT6LBCzmRTLPdmgtuXfXGxILoo9dVSXj67V/QICXVs9Rfmsd2ZHf2ZjQ8PJrizwQc/7dAK7VPcQ8oeFhzLhuNoso+hOg1Zjx5CRit0fVNtR5OHmB4+MLo5rr75j82vjN35ANXmAnWIUGCOt4AHRhUWrTPYSK6xUlwNQK1uWNGsY8g+zHGeqk3IcJcWl6hDn804wdHfebI7n8HmORXrv1TJGp59XkdXCrg02qboZpurN96U1KTmhJQmc97PufoOziewGcpSt2k+9f9HwhRbROs5JQi52pq8q76YaVdIqIZJQYQ1klNgdtCMDIS0mjMnGNhGOWgYs70QeuADl7Y3tm/aoQfH+6YYhRJvtVyUEsISwhs88MZgQbjEpqUcG0GVsj21vbQfuFbXY9EZ2xIBjmMNg8NCU4O14Bw7Lk3D1NA0bHiH5BQ5JWW8g8fj2zg6cK4FpyhmgkostKa41bLBfdP2ulctSKvRCl1CCjC+A3XlbYpTHHJ1upjqA7rvcyjvu1qu58kH/LxkdhHtPaSqZG58KOd8ivlwOBz+AAAA//8DAL8FuWgmBAAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUTW/bOBD9KwLPokJR1AcF7MFwWqyRpjESd3exN4ocxkRk0qAop0GR/76gJNdO0cPehBFn5r03b+YHsuIAqEWr4/HTCWwYUIqCmUKU0AqTCpN8l9O2pC3hGWclY1X5L0qRuYM31KKaSZlLDriACjBTpMRdoSVWCnhOckVoyWJN8Tyg9gcSJlNwMhIyN/wFfjDOohb9baxyr7G3MJns3agy73rY2CEIKyOYr5v1Hf6y2u4etvOrAYaYnBmFWpQzUpayo1gB0ZhVHcNcCYIFr/JOS9Yw4HOasQG8FX02qJdLf+WChSBbmtEiI5hy9J4iJYKIkDsxwO7tGFFMEt3GeDqFp8/2BzqBRy1Nz2LO5W5kb24OQzeaXt0E4Z8haC8O8Or8C5xEj1J09O4IPhiYtHl4Si6QcpKRjFaUEJSieyH3xkKyuU0e+khYyIYwUAXmoBVmPBeYs6LAqihFTStd11SjFN2Bt9BfVb030rvB6ZAskicf+qydDcaObhySjQ3w7EWY8z6LfgCUou3Y9WbYP4JQbzv3OMZ/duwjl/XoPdiQbEXYJ3+KYY9aVFY1LRlhUpRFDlRqTokmpOEFVYyCrCsFNaWyY4zrQhel7ITs6kZ3oIquzlGKHsZwHMNZ/+9XIHbeHA6gLgh2k8bbXgTt/GGjwAajTRzN+cVkqL6fWCVLzXVvwIYPIqMWkRyoKhrAmuYVZo0SWJQFx1VVVBpyyWsa0a2d1eZ5/KkT0VqxQpdNU3W8ywlQXTeM1LojSupCclV2UNOa1A00XBLSlSxvOi6JgrxkKrLzTo0y/GqFYhrQ42jjdiabSPvVWPy9YhdFtt4FJ10fB2Z6eHobAhx+N7W1OxzdYELkr5fRfhvg3tn4d9n/RbMleeXCJQZxD5Jp9SqiteRC4kqJGrOKl7ihBceS5VCIuma1bFCKbp18AZ9Egwljp5mcPfXwlKy83JsAMow+dv5nIjWP8/N5ZS56ZF8/7dbOw+p4TJfoH6co0lzsbIAPd+XbEJ/v3RBZBD9e+egJen3GdeUmBaf5UpmJplINqCrHlOUlZpznuOFljbu66XhXNVDKeGFmtyaPoIwHGaZ6u7nbIuzvbLnywWghwxB354uTk5sWey5PfgV5NbWf2XPzq8ktTloq5SQ69mm+m5OB/tfZfH9/f/8PAAD//wMAbXToGicGAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUXW/bNhSG/4rAa1EhKYoSdZc5K2akbdzGa4fdUeSRQ0QhDZJKFwT+7wNlB7Fb7GJ3AnXe8/Geh3xFTj0B6tH1fv/7M7gUUYmSXY4YYQITgQndUtY3rCeyajlnom3/RiWyt/CCetRyramWgGsQgLkhDR7qUWNjQFJCDWENzznVLqL+FSlbGXi2Giofv0GI1jvUo+/WGf8j11a20pOfTRX8BGsXk3I6N/N5vbrFH68327vNMSpCzOLKGtQjyknT6IFhA2TEXAwcS6MIVlLQYdS84yCPMusSBKemKprH9/rGJwdJ96xidUUwk+hQIqOSyi0PKsL2ZZ+7WCy6yeflcrx89q/oGQLqWflm5jHdlZ7s1VMcZjuZq8nvdtbtVt6NdjcHlXLhEu2D30NIFhZzfrNOhZePfrfL+dAHNUVAJdpCeLJOTccfNzCqeUr3fg6LM9E8/lcM6pGak0clWpycpqVscRpmNVlw6RftWeG7+2IzqTT68HSxpPuj9+v/Yf0npR+sg2J9g3pEKDBTd4BHRgXmnVFYNbXEQtRiBKplyygq0cq7ZN3s51isXYLdybWL/q6DfrAJdJpDnukvkWHbBG9mnYr3BVNSkaomJIvmtJ9T8RWMDaAT5CG2YT7l+0nCBFtEH+wEv9izmkMAl4qNSg/FHyo+oB41omUNJ1yrpqbA9CgZGQnpZM0MZ6BbYaBlTA+cy7Ee60YPSg9tNw5g6qHNY0OGrFi4ZgM0wJjEhnLAHDTBnewMVhIYNYyQrjMX5hZ3U9Yp3REOpsYSRoO5pApLXtfY1I1qmRjblo1nPm2PQFCSy59D+GcEc2Lp8xHtt9kNPB/v8dKnMKYDIyhmnDaYS0lxJ5sWD203yEF00OgMwcq76N+M/AZh8NGm/Ih8mS1kEi8C3vdyC8HBdLabT1YHH/2YihOUxcW2vs4uv2HFAugP6/A/CxY3Xj9CKDJWyrqzVR4Oh8O/AAAA//8DAH+Nh0gKBQAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRUXW/cKBT9KxbPxuHL2PgtmrTaUbabUTvqrvYNw6VBcWAEOG0UzX9f4ZlVm6fVviCEOfeee87BbyjoZ0ATuj2dPrxAKBm1qPjtiBEmMZGY0CNlU88mSjpJlGJC/o1a5O/hFU1oEMZQowBzkICFJT2euTPYWlCUUEtYL2pN/S2j6Q1p31l48Qa6mL9Cyj4GNKE/fbDxe+2tfWeWuNouxQX2IRcdTCXzx353j3+/PRwfDpdbGXIFd96iCVFB+t7MDFsgDgs5C6ysJlgrSWdnxChAXWA+FEhBL122Tz/721gCFDOxjvGOYKbQuUVWF10pzzrD8fVUWWwS3dXzdjvettMbeoGEJtb+K+al3I1Z/M1znle/2JvLWnR+ytvqtCkxvaIWnVI8QSoeNn1uc4bneXk96vz08XKnbvOHH2DWAnYX11DqxHxALdoEWhZdfAzNleNu8RDK9rEkH7I3/1WLoxY9fGkOiy4upud3fny5yLz/Hyp/0ubRB2j2d2hChAKzfATsGJVYjFZj3XOFpeTSATVqYBS1aBdD8WGNa272ocC3tI2EJvRRLxku/G6TefQFTFlTnfMvWXN1SNGupjQ/vaSkIx0npILWclpL8xmsT2AK1CGOab3W+/oewiTbQLs1JQilOejy2Pym8yOaUC8H1gsijO45BWacYsQRMirOrGBgBmlhYMzMQijHHe/NrM08jG4Gy+ehTgg1Os2WVuEoFc4qPA6DwcIYgzWAxkSMPYAgehTzOx2bh6XitBmJAMuxAmexUFRjJTjHlvd6YNINA3O/SHLNAyW1vYWXy7vbGEhrR7CSYiZoj4VSFI+qH/A8jLOa5Qi9qU7eQwqw/CLUJ29SzNGV5pqQ5p10n9dQ/x3NlpbvPuAfm0d30TxBaqrH2of6VK62ns/n8z8AAAD//wMAC2rMKYIEAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxU227cNhD9FYGvFRXeL3pzbQddpI6N7KIt+sbL0FYtUwuKchIY/vdCWjtXFOiLIAw5cw7PnJknlN0DoB6dHY+Xj5DrjFpUhy3ECFOYKEzogbJesp6STlEtuJF/oxYN7+Az6pEWIdBgAXNQgEUkEnueAo4RLCU0EibFWtPdzqh/Qm7oIjwOAbpp/gPKPEwZ9ejPIcfp44rthi6M0xK7Mo2wy3N1Oaxk3u/O3+Hfz24O1zenWzPMa3I3RNQjKoiUwTMcgSQslBfYRkews4r6FIQRYE9pQ65Qshu7Od5/xY9TzVBDzzrGO4KZRc8tiq66lbJ3Mxw+H1cWm0QXa7zdwttv/4QeoaCeta9insq9CePw5mH2yzDGN9sXtehYpiOUOsCmxvW++UqCko50TDFCUIuuXLgbMjS7i+Z6XJ/ogiECIscWUsTCUoet4BxHLp1mKmnNEmrROygZxm+qXg2hTPOUavMicvMdzv7s/DI7P8KK8daNM6AW7eZ9dTm6ccpw+QnCUk+1DmVZj8+nXIe8TMvc7HKF2+Jezl/zz5dSINfmxtW75jc336EeSaWZFEQEJzkFFpJlJBFiLGdRMAhaRdCMBS+ETTxxGbwLXpvkIXKvKWrRr6uG+yUEmOevbDaTjOPGoXnp0/k4QK7fyYh6RCiwyA3gxKjCwkSHneQWK8VVAhqsZl9gDq7cQkU9SsZ7T0LwTFgaHLOOx0R4VMkyprUTiUstmFKSaBei1oyKyDUExxMnhqEW3ZQpLqH+2Gq+NeCmTP9AqKtUqEeKSGmC854pQY2zBgRVwigL2orkiODBOyqN854SJ0mwhgVHlXXCR5nsK/3LfDtkuBjm4+g+f4NrOtXxX4JzhiYnfrj+trgH+DiV+/cnG3fvLw+oRR+WvG6EZrda5OOQ8Se1TvTVMtbhcFfARYhXU4SffQTruDSnCQ1KcM8DDjxRLEyS2EovsQYPkLxMTAXUoosp3ENpVoe5Ia9T9aXY9b45K+FuqBDqUlZ+f208rvfNzehqmsrDd5vkm4f9KEDHOCVrZyI8nrbRxlHFaCAqipmgEgtrKTZWauy18dYrAzKsil0v9bjU5gPEoUCo24tfvLhhnt9BuP9ZjFcXvFiUktVr+9MW26T9X0vsuUUP4OalwMO2rfsntMsZyoZ8sZxGcZevhnEcZghTjjPqjaGdoVq/EPyva5awTnOtn5+fn/8FAAD//wMAPZDLtBkGAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTS2/cIBD+KxbnxeFlML5Fm1ZdpW1WyaqtesMwdFEcvMI4D0X73yu8W6XprReERnwz32N4RdE8AOrQ5eHw4RFintAK5bCUGGESE4kJ3VHWNayjpJaUt4qSn2iFwjW8oA4pYS21GjAHCVg40uCee4udA00JdYQ1ovQ0vybUvSITagePwUI9Tt8gTWGMqEPfQ3TjU5ltQm2HcXZ1GgfYxCmbaAuZr5v1Nf58ud3dbE+vJpgKuA4OdYgK0jS2Z9gB8VjIXmDtDMFGS9p7K1oB+gQLMUOKZqgnd/823405QrYdqxmvCWYaHVfImWwK5d5MsHs5FBaLRVelvlrKy7V7RY+QUMdWf8w8tbuwQ7h4mPo5DO7idGYz3ReVhzQeIOUAiyeLzGEwOYyxOk9aDwFiRit0c1dtB5P9mB7eGXV30r/5D/m7Mv3DM9g5g1uPc8wnLFqhL8buQ4Rqc4U6RCgwx1vAnlGJResMNg3XWEouPVCrFaNohdZjzCHO4zxVm5jhV1oEoA59NMMEJ+qXye5DBpvnVFT9kGUXtml0s83Vm/+U1KTmpDC5mfNhztUtuJDAZij6dmk+9/sHwiRbQOs5JYi52pq8rz6ZaY861EjFGkGENQ2nwKzXjHhCWs2ZEwyskg4UY7YXQnvueWN7Y3vV+h4c71VRCCXu6rxhTgLXCje2sVgw6nAvPcctgAKtlPLg3vlY3QwFZ2xLBDiONXiHhaYGa8E5drwxikmvFPN/WXJOn5Iy3sHj6a8sDKRzLThJMRO0wUJrilvdKNyrtte9bKGxJeRrSBGGv4z6Emwap9Hn6rw81TvrbudY/nu1LNJTiPh5yehqtPeQqpKxCbGs9znW4/F4/A0AAP//AwATWzwkNgQAAA==

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rA81LhlyhRN8Nu0YWbepEYbdHLghoOvaxlaktRTgNj/3tA7RqOm1NvEjHvfDx8hy8k2ickPbk6Hn96xphnsiE5rEeCCU2Zpozfc9E3oue85lJpppq/yIaEW/xKetIqAA4GqUSNVDnW0EF6oM6h4Yw7JhpVctqHmfQvxIba4XMArKf5d0xzmCLpyR8huulLqW1DDeO0uDpNI27jnG2E0sxv2+tb+uvV7v5ud46acS7iOjjSE65Y08AgqEPmqdKDosZZRq3RfPCgOoXmLAsxY4p2rGf3+FbfTTlihl7UQtaMCkNOG+JstqXlwc54//VYulgR3ZTzzXq8fvYv5BkT6cXmFeY53QcYw4e0RLIhxzQdMeWAK4N1rHG0OUyxumS+HgPGfA79GyHv17lEB61xg0GlWnBosZEOmBOKSezcwD1XCiSaDphRvGHgWSes96LprO8GJBty97najTb7KT29A/35zG/7P/B9tHAIEavtDekJ4yic7JB6wTVVnbPUNtJQraX2yMG0gpMNuZ5iDnGZlrnaxowPaZ2a9ORnO86X/q4SHEJGyEsqKP7UxTCjXSIc9sc0+TDiPsx7h94uYyY98RftLk1ugVy9XSRnNaslYyXxko9Lrj6hCwkhYxn0Pi2Xmv+RCC1W0fWSEsZc7Ww+VL/Y+UB60uhWNIopsI3kKMAbwTxjnZHCKYHQaoetEDAoZbz0soHBwtB2fkAnh7ZQwOKbar1SJ7QTzgEdpBBUeW3pIJDTdjDgQDrpNX/Hurobi85CxxQ6SQ16R5XhlholJXWysa3Qvm2FLxY/Hvf5bKlXJxWjuu9oXRzHGf8Rc8J/FpzPsC6898tZ7vD5vLnrGNq5Dp3mVCjeUGUMp51pWjq03WAG3WEDxTK3mCKO39H+GCBN8+RzdbFi9Y7/pyWW16dabfklRPrvaoabCR4xVcVMNsSybBf/nDbkCe28JHxaH6/+hRwtPNqHMorHhBFwD9MS87qfs3t8/eNvq/ZDJDudTqdvAAAA//8DAGGM7QYbBQAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTy27bMBD8FYFnU+FLlKhb4LSokaYxmqAteqPIZUxEIQ2KygOB/72g7CJJb70RC87s7MzuKwr6AVCPzvf7T48Q8oRWKPulxAiTmEhM6C1lfcN6RmqiJGuY/I1WyF/CC+pRK4yhRgHmIAELSxo8cGewtaAooZawRhROfTeh/hVpX1t49AbqOP2ANPkYUI9++mDjU+mtfW3GONs6xRE2Yco6mCLm22Z9ib+eb2+vt8dfE0wFXHuLekQFaRozMGyBOCzkILCymmCtJB2cEZ0AdYT5kCEFPdaTvX/rb2MOkE3PasZrgplChxWyOusiedAT3L7si4rFootSXy3l5dm/okdIqGerv2Ye6c7M6M9MfHjQwZ45H/y0Qyu0T3EPKXtY7FgmHEedfQzVqcl69BAyWqHrm2o76uxievjg0c1x9M1/TH6lzc4HqDYXqEeEArO8A+wYlVh0VmPdcIWl5NIBNaplFK3QOobswxznqdqEDHdpUYl69FmPExz1nSez8xlMnlOR/kuWrOHZ53W0pYAL0TZFO5tcvflNSU1qTkghmfN+ztV3sD6ByVCGuk3zif8fCJNsAa3nlCDkaqvzrvqipx3qUSNb1ggijG44BWacYsQR0inOrGBgWmmhZcwMQijHHW/MoM3Qdm4Ay4e2CIUSb7VslLKdNco4bAgoLGBgWFE9YDVwrQ20ClT3wdfqeiw4bToiwHKswFksFNVYCc6x5Y1umXRty9w7S06RU1LaW3g83saiQFrbgZUUM0EbLJSiuFNNi4e2G9QgO2hMSfYSUoDxnVFX3qQ4RZer08ZUH6z7Pody39WyPU8+4Ocls4to7iFVJXPtQ1nnU8yHw+HwBwAA//8DAHsvl/wmBAAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzm6Juht2ihptmERtt0duIM4qFaKUFRW0aGP7vBbVbJO6tN2LAGb585p15ZTMciXXs9nT66UxzXlnN8riHlFCOC8eFfJaqs6rTbSO0ts6rv1jNxkf6xjrmTYwyBuKaHHGDwvJeD5EjUpBColDWlJrweWXdK4OxQTqPkZpl/Z3SOi4z69gf44zL1/I2jE2clg2btEz0MK8Z5ljE/PZw98h/vT08fzxcbq20luRmRNYxHaV2ZiAevO65Ca3kwUrJvXZKSuxtq8IlbZwzpRmmZsUv39/HJc+UY6capRvBVWBvNUPIUCT3sNLzt1NRsSO6L/F6D+/H7pWdKbFO1f/CvJS7idN4s279RGeaTpBWSjdxOR5hRlazU1pOlPJIO5YzpX5Zx1yIxuihbaXEFryxHlttlfaoBWErvMWAAZxxg4rWDti3wZvBoJIGHRnTe1azHdw0QR6Xubpqv5tGmjOr2cen6jBBHpZ0fIf+6UL04X8ALbLLbWG1856M1hqC65Ug9C46p/oIwg+9IhlJ2GB8kF5IHZQIxUitRxAhspp9gPgyzlQ93LOOCUkKdUt8UNJx0yJwsDpw57QbSMbglWQ1u1vmPM7bsq3Vw5zpc9r/yzr2M0wrXX56m+LLmCnmLRUIf7pixkNacIu5+m4AKRrRaCFK0pZPW64+EY6JYqaC4zlt13r/SVFO7Ul3W0o05+oA+aX6BdYX1rEAbZQQIAC4GAeyggaBApzwSA4doVHo0QSDzgQRnYFeGgQr+6FEWM2o+K3aLa4sWOvbyI0NkpvBAu/bVnFHIAZDxmqw7zhWH6eSB7EVhlDzQANyEyTwYLTmqC145Qbv1fADkqtZpCiAkc6XYd0VOMSW0EmujLTchCB5G6znvW/70LuWbCyaHynNNP0A6sMY07IuQ66uXqveofu0zWXhVLvvvo4z/3vv0f0Sv1CqSo9hnMt8Xdv6VrMjwbolOu77qntlhzJc1fO+tpRupFO2Zk8ZUt5O17B2jQhSvr29vf0DAAD//wMAY0OS2PQEAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUy27jNhSGX0XgWlR4EyVqlzod1MhkYkzcadEdRR45RGTSoKhMg8DvPqDsIM4MuuhOOOJ/Lt/5yVfk9R5Qh64Ph9+fwacJlSi5JcQIk5hITOiWsq5mHW8rUrdEKf4PKpG7hRfUoUYYQ40CzEECFpbUuOeDwdaCooRawmqRc+rdhLpXpF1l4dkZqML0DeLkgkcd+st5G77n2tpVZgyzrWIYYe2npL3JzXxZr27x5+vN9n5zOjXBlMWVs6hD3FAuxQBYNbzHQrUUq5pS3HDJKLV93TJ1kjmfIHo9VpN9eq9vQ/KQTMcqxiuCmULHElmddG651xNsXw65iwXRTY6XS3j57F7RM0TUsfIN5indlRnd1X7qZzfaqzHsds7vVsEPbjdHnXLhEh1iOEBMDhY4vzmv48vnsNvlfOiTHidAJdpC3Duvx9OPGxj0PKaHMMeFzGSf/usM6pCeU0AlWkiO41K2OA+zGh349Iv2ovD9Q7EZdRpC3H9Y0sOJ/fp/oL/T5tF5KNY3qEOEArO8BTwwKrForca65gpLyeUA1KiGUVSiVfDJ+TnMU7H2CXZnah/6u47m0SUwaY55pr9lNtsmBjubVLwvmJKKVJyQLJrTYU7FV7AugkmQh9jG+ZzvJwmTbBF9ciP8gmc1xwg+FRudHos/9PSIOqR0a6hWWmktjRmgJjAQS7QkjQVpJVjBbGOFElYKRYwUuqfC6pr2Q46gEkE2WbH4uqeGGaE5HhijWNTGYNVTg2shDTEDk0S3H+AW92PWadMSAZZjBYPFQlGNleAcW17rhsmhadhwwelsCEoy9UsT/jmBPXvpy8nab7NbeD7d46VPaW0LVlLMBK2xUIriVtUN7pu2V71soTZ5slXwU3gD+Q1iHyaX8iNy57zb6/HnI++buYXoYbzYzp0zMUxhSMXZlsWHfX2dfX7FisWi353H/y7GuAnmCWKRjaWdv1jm8Xg8/gAAAP//AwD+kh3KDAUAAA==

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/zQzbBb1HVTL+JFWvQ2Ike2EC21oKhNA2P/e0DtBol7640Y8J0ZPvMO30iEA5KO3B6Pv5ww5oXUJI9bSDChKdOU8T0XXSs6aRrnrGil/YfUZHzEr6QjRnnPvUMqUSNVgbW0l4OnIaDjjAcmWlVywstCujcCYxPwNHps5uUTpmWcI+nIX2MM85dSG8bGT/MamjRP+BCXDNGXZv58uHukf9zu9k+7y60FlyJuxkA6Ij2XWg1InZE9Vc5y6lrOqZFacB761gp3kY0xY4owNUv4/KN+mHPE7DvRCNkwKhw51yRAhtJyDwvuvx5LFxui+xKvt/B27N7ICRPpRP0d5iXdjZ/GmzwfJzzhdIS0YLrx8+EAMZCaHNN8xJRH3LBsL50myOMcq2uxu2nEmElNnp6r3QR5mNPhHavnC4KH/0HghKkvt1krtTGopJTgdC8YBqO91qL3wMzQC+QeWeuUcdwwLp1gTrbaWBOAOU9q8jLNPUy/Lxs/J1hvAzNCC1TohNLaeYbS4tAaZAG07rX1LkhrhLLBSnAA3PVovNO+GOQD+NcxYvVwTzrCOIogLdJBcE2VDUChlY5qLfWA3DsjOKnJ3RzzGNd5XaqHmPElbfxIR36FacELudvkX8eMPq+pQP1bl2K7NIfV5+qHAzhrWCMZK6I1H9dcfcQwJvQZC959Wq/5/iMRWmyiuzUljLnaQX6tfoPltVAB6zm48lTt/YAtw4EFBpqZgDpoDEoEE5RTQSvHvFbQcxWg5f1QIqQmWAxXbR4XPUrmvKQwMEsVG4A6FEgHr5TTrLUMwzuO1dNUdOAtUxgkdTgEqhwH6pSUNMgWjNCDMWL4CcnVfJwVwAFPl23dOtAhWAyaU6F4S5VznFrXGtob27teW2x96fkRU8TpJ1AfRp/mZR5ydfVu9Q7dxzWWH6fafPxljPTfbUb3s/+MqSozhjGWBbuO9VyTA8KyJjxsH1b3RnZlu6r99m8J2XAt2po8Z0h5PV7DUjfMcX4+n8/fAAAA//8DAMyRdbf1BAAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUS2/bOBD+KwLPpkJSFCUK2IPhtFgjTWMk7u5ibxQ5jInIokFRToMi/31BWa6dooe9CaN5fY/hD9SrPaAGLQ+HT0fo44AWKLopxAgTmAhM6JaypmRNUeecE84Z/RctkLuDN9SgimtNtQRcgADMDSlxW1iNjQFJCTWElTz1VM8Dan4g5XIDR6ch98NfEAbne9Sgv11v/GuarVyuOz+aPPgO1v0QVa/TMl/Xqzv8ZbnZPmxOWQMMqTh3BjWo0LQQ3AKWVdFiLmuKZUkprgrBKDVtWTN5KnN9hNCrLh/My2W+8bGHqBuWsyInmEn0vkBGRZVWbtUA27dD2mKi6DbFF1N4+mx+oCME1LDFmcxTuxvduZv90I6uMzdRhWeINqg9vPrwAkfVoQU6BH+AEB1M3Dw8ZZeVKMlJzgQjBC3QvdI710O2vs0eugRY6ZpwMAWWYA3mkioseVFgU5SqYsJWFbNoge4g9NBddb13OvjB25jNlGcf5qx8H10/+nHI1n2E56Diqe6z6gZAC7QZ284Nu0dQ5m3rH8f0rx+7hGU1hgB9zDYq7rI/1bBDDZKq1lRJJZUSWlsoCVhiiBKkMiCMAMOZqQyX3AguiRZctZQbVdLWpghaoIcxHsZ45v/71RLb4PZ7MJcNthPHm05F68N+baCPzrokzTljMlTXTaiyueeqc9DHDySjBhEKzBQ1YMuowLw2CquykFiIQligWlaMngiz7nn8yROx1vDClnUtWtlSAsxWNSeVbYnRttDSlC1UrCJVDbXUhLQlp3UrNTFAS24SuuDNqOOvVigmgR7HPl1ntk6wX12Pvwt+YWQTfPTad0kw18HT2xBh/zvVVn5/8IOLCb+dpf02wL3v09/5/mfO5uKlj5cYpDvIptOrK0JqKzQuNK0wb0HguqootowxWVVlqSuGFujW6xcIWTKYcv2kydlTD0/ZMuidi6DjGNLkfyZQJzk/n0/mwkf+9dN25QMsD4fFHP3jmEg6NTsb4MO78m1I6Ts/xCvIM7Qn6Ox5sSs7GTieniqXgsKYGoygmHFaYi4lxbUsK9xWdStbUUOpL3bNHsG4ADpO/bZhTNNmZn/ny2WIziodh3Q8X7ye7DT7c075dckr2X5Wn4ZfSTdbae5ESbLs0+nhnBz0v97N9/f39/8AAAD//wMAsR8nGCgGAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTy27bMBD8FYFnU+FLlKhb4LSokbYxEqMteqO4q5qIQhkUlQcC/3tB2UWa3nohiJVmd2Z2+EqCfUDSksvD4cMjhjSRFUl+KQkmNGWaMr7joq1Eq2SpjGK1Fj/JivhrfCEtqZVz3BmkEjVSBayinewdBUDDGQcmKpV72l8TaV+J9SXgo3dYjtM3jJMfA2nJdx9gfMqzrS/dMM5QxnHATZiSDS6T+bpZX9PPl9vdzfb014RTBpceSEuk41KrHqmpZUeVaTg1Fee0llpwDl3VCHOC+ZAwBjuUE9y/zYcxBUyuFaWQJaPCkOOKgE02U+7shLuXQ2axWHSV66ulvFzbV/KIkbRi9cfMU7sLN/iLh6mb/QAXpzPZ6T6rPMTxgDF5XDxZZA6DTX4MxXnSevAYElmRm7tiO9jUj/HhnVF3J/2b/5C/y9M/PKObE8J6nEMiLeEyf/pi3d4HLDZXpCWMowDZIO0F11Q1YKmtpKFaS90jd6YWnKzIegzJh3mcp2ITEv6KiwDSko92mPBE/TK6vU/o0hyzqh86Z2EbR5hdKt7856xkpWQsg+Z0mFNxi+AjuoRZ3y7O537/QIQWC2g9x4ghFVub9sUnO+1JS4xtHLfGGmu1cz1WDHsGzGpWA2rQCEpADcoo0Mowp5XtuAJb8a7PFbIimNddLAmrUWqplaTATUMVakttA45ygB6saWrVN+98LG6GjLOuYQpBUoM9UGW4pUZJSUFWtha6r2vR/2XJefucZYMBH09vZWGgARoEzalQvKLKGE4bU9W0q5vOdLrBymXO1xgDDn8Z9cW7OE5jn4pzeIp31t3OIb/3YgnSkw/0ednR1ejuMRZ5x9aHHO/zWo/H4/E3AAAA//8DAFkKilI2BAAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTy27cOBD8FYFnUeZLlKibMd5FBk7Wg8RIFnuj2K0MEZkcUJSTwJh/X1AzQeLcciOKrO7qquYLCfYJyUBuT6e/njHkhdQk+w0STGjKNGX8kYuhFYOSjTKaCan+IzXx9/idDKRTznFnkErUSBWwlo5ychQADWccmGhVqWk/L2R4IdY3gM/eYROXj5gWHwMZyCcfIH4tva1v3BxXaFKccR+WbIMrYv7Z7+7p29vD48Ph8mrBpZAbD2Qg0nGp1YTUdHKkyvScmpZz2kktOIex7YW50HzImIKdmwW+/OwPMQfMbhCNkA2jwpBzTcBmWySPdsHH76eiYrPoruD1Bm/H4YU8YyKDqH+YeSl342Z/4+LTkw1wM/nglyOpySnFE6bscbNjm3CebfYxVNcmu9ljyKQmDx+qw2zzFNPTK48+XEbf/8Hk76w7+oDV/o4MhHEUIHukk+Caqh4sta00VGupJ+TOdIKTmuxiyD6scV2qfcj4OW0qyUD+tvOCF323yR19RpfXVKT/q0vW+M3nXYQCMFKTQ4qwulz9tJuzhjWSlcuHNZ/WXL1H8AldxjLTY1qv5X+jCC020m5NCUOuDjYfqzd2OZKBGNs7bo011mrnJmwZTgyY1awD1KARlIAOlFGglWFOKztyBbbl41SQorukW20LpRlDC2qkUjJNFQOkFoWjrOUtMxa0A/HK1uphLjzreqYQJDU4AVWGW2qUlBRkazuhp64T0y+WXBPnrPgN+Hz5GhcFAD2C5lQo3lJlDKe9aTs6dv1oRt1j64rme0wB51+MeuddikuccnVdmOqVde/XUL53tS3PVx/oty2yu+i+YKpK5NaHss3XlM/n8/l/AAAA//8DAEtVZ1slBAAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRUy27cOBD8FaHPosyXKFE3Y5xgB96sB8kgG+RGka2YsEwNSMqJYfjfF9TMwvFpsReCaLGK1dVFvUAwjwgDXJ9OH54w5AQ1ZL+VOOWKUEUoOzI+tHyQopGaCSXkd6jB3+IzDNBJa5nVSAQqJNLRloxissQ51IwyR3krC6f5kWB4AeMbh0/eYrOkrxiTXwIM8LcPbvlZ7ja+sfOyuiYuM+5DyibYIuav/e6W/Hl9ON4dzqcSpgJuvIMBhGVCyQmJ7sRIpO4Z0S1jpBOKM+bGtuf6DPMhYwxmbpJ7eLvfLTlgtgNvuGgo4Rpea3AmmyJ5NAmPz6eiYrPoptTrrbxthxd4wggDr/8180x3ZWd/9ZjG1c/u6rxmkx7Stk7G5iU+Qw2nuJwwZo+bP9cp4eM4Px9Nevh4PlO26cMvtGtGt1vWkGEAJhTUsBk0zyb7JVQXjbvZY8jbxxx9SN7+F5eAGu6+VIfZ5GmJj+/m8eVs8/5/uPzJ2HsfsNrfwACUIXeiRzJxpojsnSGmFZooJdSEzOqOM6hht4Tsw7qsqdqHjD/i1hIM8NHMCc/6rqO99xltXmPp85squTrExa02V2+zZLShjaC0gNZ8WnP1GZ2PaDOWJo5xvfB9fQ/him+g3RojhlwdTL6v/jDpHgbQprfMaKONUdZO2FKcqKNG0c6hcgqd5K5zUkunpKZWSTMy6UzLxqlUoAYs0am2tFo2GdTKEjnKjkhLOemtMASF0pJJw0dWPHnzsbqbC87Ynkp0gmicHJGaGaKlEMSJ1nRcTV3Hp98sueSB0ULm8On87jYFyrkenWKES9YSqTUjvW47Mnb9qEfVY2uL5luMAeffjPrkbVzSMuXqkpDqnXWf11D+HdWWlp8+kF/bjG4W+4CxKjM2PpSnchnr6+vr6z8AAAD//wMAN4ao/4IEAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUy27kNhD8FYHXiDJfokTdHNuLDBw/sDNIgtyaZMtmLFMDirJ3YfjfA2ns9WMRIBdBaJJd1dXV/UQi3CPpyPF+f/aAMU+kJDmsIcGEpkxTxndcdLXolKyU0aqR5m9SknCO30lHGuUcdwapRI1UeVZTK3tHvUfDGfdM1GrJCTcT6Z4IhMrjQ3BYjdMfmKYwRtKRP0P04+OCDaFywzj7Ko0DbuKUIbqFzOXm5Jz+fny9u7o+3JpwWh5XwZOOSMelVj1S00hLlWk5NTXntJFacO5t3QpzeBZixhRhqCZ/94bvxxwxu05UQlaMCkOeS+Ihw0LZwoS77/uFxSrR6RIv1/D62z2RB0ykE+WrmId0R24IR/eTncPgj9YvKck+jXtMOeCqxtW2eCPBWcUqoQVjpCQX4G5DxGJzWlwNS4ngWqbQS2qw91QZDtQoKamXNTRC900jelKSc0wRh3dZL4JL4zT2uXgRufiAsz0+OYtgB1wwvsAwISnJZtpmiB6GMeLZN3RzPuTapXk5PhljDnEe56nYxIw3CV7OX9+fzClhzMU15NviN5huSUcMtI6DAQOgneuxZtgzz0CzxqP2Gr0SvvHKKK+VYU4rsFx5qLntlwgpya+LhtvZOZymNzarSYZh5VC89OlkCBjzBxlJRxhH4WWLtBdcU9V6oFBLQ7WWukfuTCP4K8wO0g1m0pG+tdYy56xQhjsQBqTvmfS6N0I0Dahe1o0SWtesAeebRnDlZYMOZC9ZK0hJrtPoZ5c/t1quDbhO4z/o8iIV6Yhmdd06sFZoxVswLSquVasNNkb1wJR0FnjdgrWcQc2caYUDrg0o6+v+h0pn8SZEPA3TfoDv73DbSlfyFwfQ8h7Up+tfEtzj45juLg82ri7PdqQkX+e4bIRis1jkMUT6TS8TfTEPOexuE4JHfzF6/NlHuIxLsU6ol1w72ViKogeqGmDUtojUaqcatNIZsahxOro7TMXiMAhxmaofya62xXFytyGjy3Na+P218rjaFtcD5H5M9x82ybvCPgtQCcnZ0hmPD4dttHLU3rfoNadC8ZoqYzhtTd1Q27TWWN1i7RbFrua8n3PxFX1I6PJa8YsXV8yTW3R3P4vx6oIXi3K2eG172GKrtP9riT2X5B5hmhPer9u6eyKbGDGtyKfzYRQ38SIMQ5jQjdFPpKuVairGhHhh+N/3jKqYEvr5+fn5XwAAAP//AwCgOTEOGwYAAA==

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

@ -0,0 +1,41 @@
using Microsoft.AspNetCore.Mvc;
using PlotLine.Services;
namespace PlotLine.Controllers;
public sealed class ExportsController(IExportService exports) : Controller
{
public async Task<IActionResult> StoryBible(int projectId, string format = "html") =>
ExportResult(await exports.ExportStoryBibleAsync(projectId, format), format);
public async Task<IActionResult> NarrativeTimeline(int projectId, string format = "html") =>
ExportResult(await exports.ExportNarrativeTimelineAsync(projectId, format), format);
public async Task<IActionResult> Character(int id, string format = "html") =>
ExportResult(await exports.ExportCharacterReferenceAsync(id, format), format);
public async Task<IActionResult> SceneBrief(int id, string format = "html") =>
ExportResult(await exports.ExportSceneBriefAsync(id, format), format);
public async Task<IActionResult> ChapterBrief(int id, string format = "html") =>
ExportResult(await exports.ExportChapterBriefAsync(id, format), format);
public async Task<IActionResult> WritingSession(int sceneId, string format = "html") =>
ExportResult(await exports.ExportWriterSessionBriefAsync(sceneId, format), format);
public async Task<IActionResult> ResearchPack(int projectId, string format = "html") =>
ExportResult(await exports.ExportResearchPackAsync(projectId, format), format);
public async Task<IActionResult> Search(int projectId, string q, string format = "html") =>
ExportResult(await exports.ExportSearchResultsAsync(projectId, q, format), format);
private IActionResult ExportResult((string FileName, string ContentType, string Content) export, string format)
{
if (string.Equals(format, "html", StringComparison.OrdinalIgnoreCase))
{
return Content(export.Content, export.ContentType);
}
return File(System.Text.Encoding.UTF8.GetBytes(export.Content), export.ContentType, export.FileName);
}
}

View File

@ -10,8 +10,11 @@ public sealed class ScenesController(
IAssetService assets,
ICharacterService characters,
ILocationService locations,
IWriterWorkspaceService writerWorkspace) : Controller
IWriterWorkspaceService writerWorkspace,
IWebHostEnvironment environment) : Controller
{
private const long MaxAttachmentBytes = 25 * 1024 * 1024;
public async Task<IActionResult> Create(int chapterId)
{
var model = await scenes.GetCreateAsync(chapterId);
@ -340,15 +343,33 @@ public sealed class ScenesController(
return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
}
if (model.UploadedFile is not null && model.UploadedFile.Length > MaxAttachmentBytes)
{
TempData["WriterWorkspaceError"] = "Attachment files must be 25 MB or smaller.";
return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
}
if (model.UploadedFile is not null && model.UploadedFile.Length > 0)
{
model.FilePath = await SaveUploadedAttachmentAsync(model.SceneID, model.UploadedFile);
}
if (string.IsNullOrWhiteSpace(model.FilePath) && string.IsNullOrWhiteSpace(model.ExternalUrl))
{
TempData["WriterWorkspaceError"] = "Upload a file or enter an external link.";
return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
}
await writerWorkspace.SaveAttachmentAsync(model);
return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteAttachment(int id, int sceneId, int? projectId, int? bookId)
public async Task<IActionResult> DeleteAttachment(int id, int sceneId, int? projectId, int? bookId, string? filePath)
{
await writerWorkspace.DeleteAttachmentAsync(id);
DeleteUploadedAttachment(filePath);
return RedirectForScene(sceneId, projectId, bookId);
}
@ -361,4 +382,50 @@ public sealed class ScenesController(
return RedirectToAction(nameof(Edit), new { id = sceneId });
}
private async Task<string> SaveUploadedAttachmentAsync(int sceneId, IFormFile uploadedFile)
{
var uploadRoot = Path.Combine(environment.WebRootPath, "uploads", "scenes", sceneId.ToString());
Directory.CreateDirectory(uploadRoot);
var originalName = Path.GetFileName(uploadedFile.FileName);
var safeName = MakeSafeFileName(originalName);
var storedName = $"{Guid.NewGuid():N}_{safeName}";
var absolutePath = Path.Combine(uploadRoot, storedName);
await using var stream = System.IO.File.Create(absolutePath);
await uploadedFile.CopyToAsync(stream);
return $"/uploads/scenes/{sceneId}/{storedName}";
}
private static string MakeSafeFileName(string fileName)
{
var name = string.IsNullOrWhiteSpace(fileName) ? "attachment" : fileName;
foreach (var invalidChar in Path.GetInvalidFileNameChars())
{
name = name.Replace(invalidChar, '-');
}
name = string.Join("-", name.Split([' ', '\t', '\r', '\n'], StringSplitOptions.RemoveEmptyEntries));
return name.Length > 120 ? name[^120..] : name;
}
private void DeleteUploadedAttachment(string? filePath)
{
if (string.IsNullOrWhiteSpace(filePath) || !filePath.StartsWith("/uploads/scenes/", StringComparison.OrdinalIgnoreCase))
{
return;
}
var uploadsRoot = Path.GetFullPath(Path.Combine(environment.WebRootPath, "uploads"));
var relativePath = filePath.TrimStart('/').Replace('/', Path.DirectorySeparatorChar);
var absolutePath = Path.GetFullPath(Path.Combine(environment.WebRootPath, relativePath));
if (!absolutePath.StartsWith(uploadsRoot, StringComparison.OrdinalIgnoreCase) || !System.IO.File.Exists(absolutePath))
{
return;
}
System.IO.File.Delete(absolutePath);
}
}

View File

@ -212,6 +212,18 @@ public interface IWriterWorkspaceRepository
Task<IReadOnlyList<StoryBibleAttentionItem>> GetWriterDashboardRemindersAsync(int? projectId);
}
public interface IExportRepository
{
Task<ExportPacket> GetStoryBibleAsync(int projectId);
Task<ExportPacket> GetNarrativeTimelineAsync(int projectId);
Task<ExportPacket> GetCharacterReferenceAsync(int characterId);
Task<ExportPacket> GetSceneBriefAsync(int sceneId);
Task<ExportPacket> GetChapterBriefAsync(int chapterId);
Task<ExportPacket> GetWriterSessionBriefAsync(int sceneId);
Task<ExportPacket> GetResearchPackAsync(int projectId);
Task<ExportPacket> GetSearchResultsAsync(int projectId, string query);
}
public sealed class TimelineData
{
public Project? Project { get; init; }
@ -731,6 +743,101 @@ public sealed class WriterWorkspaceRepository(ISqlConnectionFactory connectionFa
}
}
public sealed class ExportRepository(ISqlConnectionFactory connectionFactory) : IExportRepository
{
public async Task<ExportPacket> GetStoryBibleAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
using var result = await connection.QueryMultipleAsync("dbo.Export_StoryBible", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
return new ExportPacket
{
Title = "Story Bible",
Characters = (await result.ReadAsync<StoryBibleCharacterSummary>()).ToList(),
PlotThreads = (await result.ReadAsync<StoryBibleThreadSummary>()).ToList(),
Assets = (await result.ReadAsync<StoryBibleAssetSummary>()).ToList(),
Relationships = (await result.ReadAsync<StoryBibleRelationshipSummary>()).ToList(),
Locations = (await result.ReadAsync<StoryBibleLocationSummary>()).ToList(),
Timeline = (await result.ReadAsync<StoryBibleTimelineSummary>()).ToList(),
OpenQuestions = (await result.ReadAsync<StoryBibleAttentionItem>()).ToList(),
RevisionItems = (await result.ReadAsync<StoryBibleRevisionItem>()).ToList()
};
}
public async Task<ExportPacket> GetNarrativeTimelineAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<StoryBibleTimelineSummary>("dbo.Export_NarrativeTimeline", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
return new ExportPacket { Title = "Narrative Timeline Summary", Timeline = rows.ToList() };
}
public async Task<ExportPacket> GetCharacterReferenceAsync(int characterId)
{
using var connection = connectionFactory.CreateConnection();
using var result = await connection.QueryMultipleAsync("dbo.Export_CharacterReference", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure);
return new ExportPacket
{
Title = "Character Reference Pack",
Character = await result.ReadSingleOrDefaultAsync<Character>(),
CharacterRelationships = (await result.ReadAsync<CharacterRelationship>()).ToList(),
CharacterAppearances = (await result.ReadAsync<SceneCharacter>()).ToList(),
CharacterKnowledge = (await result.ReadAsync<CharacterKnowledgeItem>()).ToList(),
CharacterCustody = (await result.ReadAsync<AssetCustodyEvent>()).ToList(),
Items = (await result.ReadAsync<ExportBriefItem>()).ToList()
};
}
public async Task<ExportPacket> GetSceneBriefAsync(int sceneId)
{
using var connection = connectionFactory.CreateConnection();
using var result = await connection.QueryMultipleAsync("dbo.Export_SceneBrief", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
return new ExportPacket
{
Title = "Scene Brief",
SceneHeader = await result.ReadSingleOrDefaultAsync<ExportSceneBriefHeader>(),
Items = (await result.ReadAsync<ExportBriefItem>()).ToList()
};
}
public async Task<ExportPacket> GetChapterBriefAsync(int chapterId)
{
using var connection = connectionFactory.CreateConnection();
using var result = await connection.QueryMultipleAsync("dbo.Export_ChapterBrief", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure);
return new ExportPacket
{
Title = "Chapter Brief",
ChapterHeader = await result.ReadSingleOrDefaultAsync<ExportChapterBriefHeader>(),
Timeline = (await result.ReadAsync<StoryBibleTimelineSummary>()).ToList(),
Items = (await result.ReadAsync<ExportBriefItem>()).ToList()
};
}
public async Task<ExportPacket> GetWriterSessionBriefAsync(int sceneId)
{
using var connection = connectionFactory.CreateConnection();
using var result = await connection.QueryMultipleAsync("dbo.Export_WriterSessionBrief", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
return new ExportPacket
{
Title = "Writing Session Brief",
SceneHeader = await result.ReadSingleOrDefaultAsync<ExportSceneBriefHeader>(),
Items = (await result.ReadAsync<ExportBriefItem>()).ToList()
};
}
public async Task<ExportPacket> GetResearchPackAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<ExportResearchItem>("dbo.Export_ResearchPack", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
return new ExportPacket { Title = "Research Pack", ResearchItems = rows.ToList() };
}
public async Task<ExportPacket> GetSearchResultsAsync(int projectId, string query)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<StoryBibleSearchResult>("dbo.Export_SearchResults", new { ProjectID = projectId, Query = query }, commandType: CommandType.StoredProcedure);
return new ExportPacket { Title = $"Search Results: {query}", SearchResults = rows.ToList() };
}
}
public sealed class SceneDependencyRepository(ISqlConnectionFactory connectionFactory) : ISceneDependencyRepository
{
public async Task<SceneDependencyLookupData> GetLookupsAsync()

View File

@ -1276,6 +1276,95 @@ public sealed class ChapterWorkflowScene
public int UnresolvedNoteCount { get; set; }
}
public sealed class ExportSceneBriefHeader
{
public int ProjectID { get; set; }
public string ProjectName { get; set; } = string.Empty;
public int BookID { get; set; }
public string BookTitle { get; set; } = string.Empty;
public int ChapterID { get; set; }
public decimal ChapterNumber { get; set; }
public string ChapterTitle { get; set; } = string.Empty;
public int SceneID { get; set; }
public decimal SceneNumber { get; set; }
public string SceneTitle { get; set; } = string.Empty;
public string? Summary { get; set; }
public string? ScenePurposeNotes { get; set; }
public string? SceneOutcomeNotes { get; set; }
public string RevisionStatusName { get; set; } = string.Empty;
public string? PovCharacterName { get; set; }
public string? LocationPath { get; set; }
public string TimeLabel { get; set; } = string.Empty;
public string DraftStatus { get; set; } = "Planned";
public bool IsBlocked { get; set; }
public string? BlockedReason { get; set; }
public int? Priority { get; set; }
}
public sealed class ExportBriefItem
{
public string ItemType { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string? Detail { get; set; }
public string? Status { get; set; }
public int? SortOrder { get; set; }
}
public sealed class ExportChapterBriefHeader
{
public int ProjectID { get; set; }
public string ProjectName { get; set; } = string.Empty;
public int BookID { get; set; }
public string BookTitle { get; set; } = string.Empty;
public int ChapterID { get; set; }
public decimal ChapterNumber { get; set; }
public string ChapterTitle { get; set; } = string.Empty;
public string? Summary { get; set; }
public string RevisionStatusName { get; set; } = string.Empty;
public string? GoalSummary { get; set; }
public string? EmotionalGoal { get; set; }
public string? ReaderTakeaway { get; set; }
public string? MustInclude { get; set; }
public string? RisksOrConcerns { get; set; }
}
public sealed class ExportResearchItem
{
public string BookTitle { get; set; } = string.Empty;
public decimal ChapterNumber { get; set; }
public string ChapterTitle { get; set; } = string.Empty;
public int SceneID { get; set; }
public decimal SceneNumber { get; set; }
public string SceneTitle { get; set; } = string.Empty;
public string Topic { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string? Detail { get; set; }
public string? Reference { get; set; }
}
public sealed class ExportPacket
{
public string Title { get; set; } = string.Empty;
public IReadOnlyList<StoryBibleCharacterSummary> Characters { get; init; } = [];
public IReadOnlyList<StoryBibleThreadSummary> PlotThreads { get; init; } = [];
public IReadOnlyList<StoryBibleAssetSummary> Assets { get; init; } = [];
public IReadOnlyList<StoryBibleRelationshipSummary> Relationships { get; init; } = [];
public IReadOnlyList<StoryBibleLocationSummary> Locations { get; init; } = [];
public IReadOnlyList<StoryBibleTimelineSummary> Timeline { get; init; } = [];
public IReadOnlyList<StoryBibleAttentionItem> OpenQuestions { get; init; } = [];
public IReadOnlyList<StoryBibleRevisionItem> RevisionItems { get; init; } = [];
public ExportSceneBriefHeader? SceneHeader { get; init; }
public ExportChapterBriefHeader? ChapterHeader { get; init; }
public Character? Character { get; init; }
public IReadOnlyList<SceneCharacter> CharacterAppearances { get; init; } = [];
public IReadOnlyList<CharacterRelationship> CharacterRelationships { get; init; } = [];
public IReadOnlyList<CharacterKnowledgeItem> CharacterKnowledge { get; init; } = [];
public IReadOnlyList<AssetCustodyEvent> CharacterCustody { get; init; } = [];
public IReadOnlyList<ExportBriefItem> Items { get; init; } = [];
public IReadOnlyList<ExportResearchItem> ResearchItems { get; init; } = [];
public IReadOnlyList<StoryBibleSearchResult> SearchResults { get; init; } = [];
}
public sealed class AnalyticsBusyScene
{
public int SceneID { get; set; }

View File

@ -34,6 +34,7 @@ public class Program
builder.Services.AddScoped<IAnalyticsRepository, AnalyticsRepository>();
builder.Services.AddScoped<IStoryBibleRepository, StoryBibleRepository>();
builder.Services.AddScoped<IWriterWorkspaceRepository, WriterWorkspaceRepository>();
builder.Services.AddScoped<IExportRepository, ExportRepository>();
builder.Services.AddScoped<IProjectService, ProjectService>();
builder.Services.AddScoped<IBookService, BookService>();
builder.Services.AddScoped<IChapterService, ChapterService>();
@ -47,6 +48,7 @@ public class Program
builder.Services.AddScoped<IAnalyticsService, AnalyticsService>();
builder.Services.AddScoped<IStoryBibleService, StoryBibleService>();
builder.Services.AddScoped<IWriterWorkspaceService, WriterWorkspaceService>();
builder.Services.AddScoped<IExportService, ExportService>();
var app = builder.Build();
@ -57,6 +59,7 @@ public class Program
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();

View File

@ -1,3 +1,5 @@
using System.Net;
using System.Text;
using Microsoft.AspNetCore.Mvc.Rendering;
using PlotLine.Data;
using PlotLine.Models;
@ -164,6 +166,18 @@ public interface IWriterWorkspaceService
Task DeleteAttachmentAsync(int sceneAttachmentId);
}
public interface IExportService
{
Task<(string FileName, string ContentType, string Content)> ExportStoryBibleAsync(int projectId, string format);
Task<(string FileName, string ContentType, string Content)> ExportNarrativeTimelineAsync(int projectId, string format);
Task<(string FileName, string ContentType, string Content)> ExportCharacterReferenceAsync(int characterId, string format);
Task<(string FileName, string ContentType, string Content)> ExportSceneBriefAsync(int sceneId, string format);
Task<(string FileName, string ContentType, string Content)> ExportChapterBriefAsync(int chapterId, string format);
Task<(string FileName, string ContentType, string Content)> ExportWriterSessionBriefAsync(int sceneId, string format);
Task<(string FileName, string ContentType, string Content)> ExportResearchPackAsync(int projectId, string format);
Task<(string FileName, string ContentType, string Content)> ExportSearchResultsAsync(int projectId, string query, string format);
}
public sealed class ProjectService(IProjectRepository projects, IBookRepository books) : IProjectService
{
public Task<IReadOnlyList<Project>> ListAsync() => projects.ListAsync();
@ -1894,6 +1908,274 @@ public sealed class WriterWorkspaceService(
public Task DeleteAttachmentAsync(int sceneAttachmentId) => writerWorkspace.DeleteAttachmentAsync(sceneAttachmentId);
}
public sealed class ExportService(IExportRepository exports) : IExportService
{
public async Task<(string FileName, string ContentType, string Content)> ExportStoryBibleAsync(int projectId, string format)
=> Render(await exports.GetStoryBibleAsync(projectId), "story-bible", format);
public async Task<(string FileName, string ContentType, string Content)> ExportNarrativeTimelineAsync(int projectId, string format)
=> Render(await exports.GetNarrativeTimelineAsync(projectId), "narrative-timeline", format);
public async Task<(string FileName, string ContentType, string Content)> ExportCharacterReferenceAsync(int characterId, string format)
=> Render(await exports.GetCharacterReferenceAsync(characterId), "character-reference", format);
public async Task<(string FileName, string ContentType, string Content)> ExportSceneBriefAsync(int sceneId, string format)
=> Render(await exports.GetSceneBriefAsync(sceneId), "scene-brief", format);
public async Task<(string FileName, string ContentType, string Content)> ExportChapterBriefAsync(int chapterId, string format)
=> Render(await exports.GetChapterBriefAsync(chapterId), "chapter-brief", format);
public async Task<(string FileName, string ContentType, string Content)> ExportWriterSessionBriefAsync(int sceneId, string format)
=> Render(await exports.GetWriterSessionBriefAsync(sceneId), "writing-session-brief", format);
public async Task<(string FileName, string ContentType, string Content)> ExportResearchPackAsync(int projectId, string format)
=> Render(await exports.GetResearchPackAsync(projectId), "research-pack", format);
public async Task<(string FileName, string ContentType, string Content)> ExportSearchResultsAsync(int projectId, string query, string format)
=> Render(await exports.GetSearchResultsAsync(projectId, query), "search-results", format);
private static (string FileName, string ContentType, string Content) Render(ExportPacket packet, string slug, string format)
{
var normalised = string.IsNullOrWhiteSpace(format) ? "html" : format.Trim().ToLowerInvariant();
return normalised switch
{
"markdown" or "md" => ($"{slug}.md", "text/markdown; charset=utf-8", RenderMarkdown(packet)),
"text" or "txt" => ($"{slug}.txt", "text/plain; charset=utf-8", RenderText(packet)),
_ => ($"{slug}.html", "text/html; charset=utf-8", RenderHtml(packet))
};
}
private static string RenderMarkdown(ExportPacket packet)
{
var builder = new StringBuilder();
builder.AppendLine($"# {packet.Title}").AppendLine();
AppendPacket(builder, packet, markdown: true);
return builder.ToString();
}
private static string RenderText(ExportPacket packet)
{
var builder = new StringBuilder();
builder.AppendLine(packet.Title).AppendLine(new string('=', packet.Title.Length)).AppendLine();
AppendPacket(builder, packet, markdown: false);
return builder.ToString();
}
private static string RenderHtml(ExportPacket packet)
{
var body = RenderMarkdown(packet)
.Split('\n')
.Select(line =>
{
var text = WebUtility.HtmlEncode(line.TrimEnd());
if (text.StartsWith("### ")) return $"<h3>{text[4..]}</h3>";
if (text.StartsWith("## ")) return $"<h2>{text[3..]}</h2>";
if (text.StartsWith("# ")) return $"<h1>{text[2..]}</h1>";
if (text.StartsWith("- ")) return $"<li>{text[2..]}</li>";
return string.IsNullOrWhiteSpace(text) ? "" : $"<p>{text}</p>";
});
return $$"""
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{{WebUtility.HtmlEncode(packet.Title)}}</title>
<style>
body { font-family: Georgia, 'Times New Roman', serif; line-height: 1.55; max-width: 920px; margin: 32px auto; padding: 0 24px; color: #24201a; }
h1, h2, h3 { font-family: Arial, sans-serif; line-height: 1.2; }
h1 { border-bottom: 2px solid #d8d0c2; padding-bottom: 12px; }
h2 { margin-top: 32px; border-bottom: 1px solid #e6dfd2; padding-bottom: 6px; }
li { margin: 5px 0; }
@media print { body { margin: 0; max-width: none; } a { color: inherit; } }
</style>
</head>
<body>
{{string.Join(Environment.NewLine, body)}}
</body>
</html>
""";
}
private static void AppendPacket(StringBuilder builder, ExportPacket packet, bool markdown)
{
if (packet.SceneHeader is not null) AppendSceneHeader(builder, packet.SceneHeader, markdown);
if (packet.ChapterHeader is not null) AppendChapterHeader(builder, packet.ChapterHeader, markdown);
if (packet.Character is not null) AppendCharacter(builder, packet);
AppendTimeline(builder, packet.Timeline, markdown);
AppendCharacters(builder, packet.Characters);
AppendThreads(builder, packet.PlotThreads);
AppendAssets(builder, packet.Assets);
AppendRelationships(builder, packet.Relationships);
AppendLocations(builder, packet.Locations);
AppendItems(builder, "Brief Details", packet.Items, markdown);
AppendResearch(builder, packet.ResearchItems, markdown);
AppendSearch(builder, packet.SearchResults, markdown);
AppendAttention(builder, packet.OpenQuestions);
AppendRevision(builder, packet.RevisionItems);
}
private static void Heading(StringBuilder builder, string text, bool markdown, int level = 2)
=> builder.AppendLine(markdown ? $"{new string('#', level)} {text}" : text).AppendLine(markdown ? "" : new string('-', text.Length));
private static void AppendSceneHeader(StringBuilder builder, ExportSceneBriefHeader scene, bool markdown)
{
Heading(builder, $"{scene.BookTitle} / Chapter {scene.ChapterNumber} / Scene {scene.SceneNumber}: {scene.SceneTitle}", markdown);
builder.AppendLine($"- POV: {scene.PovCharacterName ?? "Not set"}");
builder.AppendLine($"- Location: {scene.LocationPath ?? "Not set"}");
builder.AppendLine($"- Time: {scene.TimeLabel}");
builder.AppendLine($"- Revision: {scene.RevisionStatusName}");
builder.AppendLine($"- Draft status: {scene.DraftStatus}");
if (scene.Priority.HasValue) builder.AppendLine($"- Priority: {scene.Priority}");
if (scene.IsBlocked) builder.AppendLine($"- Blocked: {scene.BlockedReason}");
AppendBlock(builder, "Summary", scene.Summary, markdown);
AppendBlock(builder, "Purpose", scene.ScenePurposeNotes, markdown);
AppendBlock(builder, "Outcome", scene.SceneOutcomeNotes, markdown);
}
private static void AppendChapterHeader(StringBuilder builder, ExportChapterBriefHeader chapter, bool markdown)
{
Heading(builder, $"{chapter.BookTitle} / Chapter {chapter.ChapterNumber}: {chapter.ChapterTitle}", markdown);
builder.AppendLine($"- Revision: {chapter.RevisionStatusName}");
AppendBlock(builder, "Summary", chapter.Summary, markdown);
AppendBlock(builder, "Goal", chapter.GoalSummary, markdown);
AppendBlock(builder, "Emotion", chapter.EmotionalGoal, markdown);
AppendBlock(builder, "Reader Takeaway", chapter.ReaderTakeaway, markdown);
AppendBlock(builder, "Must Include", chapter.MustInclude, markdown);
AppendBlock(builder, "Risks", chapter.RisksOrConcerns, markdown);
}
private static void AppendBlock(StringBuilder builder, string title, string? text, bool markdown)
{
if (string.IsNullOrWhiteSpace(text)) return;
builder.AppendLine(markdown ? $"### {title}" : title).AppendLine(text.Trim()).AppendLine();
}
private static void AppendTimeline(StringBuilder builder, IReadOnlyList<StoryBibleTimelineSummary> rows, bool markdown)
{
if (!rows.Any()) return;
Heading(builder, "Timeline Summary", markdown);
foreach (var book in rows.GroupBy(x => new { x.BookID, x.BookTitle }))
{
builder.AppendLine(markdown ? $"### {book.Key.BookTitle}" : book.Key.BookTitle);
foreach (var scene in book)
{
builder.AppendLine($"- Chapter {scene.ChapterNumber}, Scene {scene.SceneNumber}: {scene.SceneTitle}");
builder.AppendLine($" POV: {scene.PovCharacterName ?? "Not set"}; Location: {scene.LocationPath ?? "Not set"}; Status: {scene.RevisionStatusName}");
if (!string.IsNullOrWhiteSpace(scene.Summary)) builder.AppendLine($" {scene.Summary}");
}
builder.AppendLine();
}
}
private static void AppendCharacters(StringBuilder builder, IReadOnlyList<StoryBibleCharacterSummary> rows)
{
if (!rows.Any()) return;
builder.AppendLine("## Characters").AppendLine();
foreach (var c in rows) builder.AppendLine($"- {c.CharacterName}: {c.SceneCount} scenes; first {c.FirstSceneLabel ?? "not placed"}; last {c.LastSceneLabel ?? "not placed"}");
builder.AppendLine();
}
private static void AppendThreads(StringBuilder builder, IReadOnlyList<StoryBibleThreadSummary> rows)
{
if (!rows.Any()) return;
builder.AppendLine("## Plot Threads").AppendLine();
foreach (var t in rows) builder.AppendLine($"- {t.ThreadTitle} ({t.ThreadStatusName}, importance {t.Importance}): {t.EventHistory ?? "No events"}");
builder.AppendLine();
}
private static void AppendAssets(StringBuilder builder, IReadOnlyList<StoryBibleAssetSummary> rows)
{
if (!rows.Any()) return;
builder.AppendLine("## Story Assets").AppendLine();
foreach (var a in rows) builder.AppendLine($"- {a.AssetName} ({a.KindName}, {a.CurrentStateName ?? "No state"}): {a.EventHistory ?? "No events"}");
builder.AppendLine();
}
private static void AppendRelationships(StringBuilder builder, IReadOnlyList<StoryBibleRelationshipSummary> rows)
{
if (!rows.Any()) return;
builder.AppendLine("## Relationships").AppendLine();
foreach (var r in rows) builder.AppendLine($"- {r.CharacterAName} / {r.CharacterBName}: {r.RelationshipTypeName}; {r.CurrentStateName ?? "No state"}");
builder.AppendLine();
}
private static void AppendLocations(StringBuilder builder, IReadOnlyList<StoryBibleLocationSummary> rows)
{
if (!rows.Any()) return;
builder.AppendLine("## Locations").AppendLine();
foreach (var l in rows) builder.AppendLine($"- {l.LocationPath}: {l.SceneCount} scenes, {l.CharacterCount} characters, {l.AssetCount} assets");
builder.AppendLine();
}
private static void AppendCharacter(StringBuilder builder, ExportPacket packet)
{
var c = packet.Character!;
builder.AppendLine($"## {c.CharacterName}").AppendLine();
builder.AppendLine($"- Short name: {c.ShortName}");
builder.AppendLine($"- Sex: {c.Sex}");
builder.AppendLine($"- Age at series start: {c.AgeAtSeriesStart}");
builder.AppendLine($"- Height: {c.Height}");
builder.AppendLine($"- Eye colour: {c.EyeColour}");
if (!string.IsNullOrWhiteSpace(c.DefaultDescription)) builder.AppendLine().AppendLine(c.DefaultDescription).AppendLine();
foreach (var app in packet.CharacterAppearances) builder.AppendLine($"- Appearance: {app.BookTitle} / Ch {app.ChapterNumber} / Scene {app.SceneNumber}: {app.SceneTitle} ({app.RoleInSceneTypeName})");
foreach (var rel in packet.CharacterRelationships) builder.AppendLine($"- Relationship: {rel.CharacterAName} / {rel.CharacterBName}: {rel.RelationshipTypeName}");
foreach (var item in packet.CharacterKnowledge) builder.AppendLine($"- Knowledge: {item.KnowledgeStateName} - {item.AssetName ?? item.ThreadTitle}: {item.Description}");
builder.AppendLine();
}
private static void AppendItems(StringBuilder builder, string title, IReadOnlyList<ExportBriefItem> rows, bool markdown)
{
if (!rows.Any()) return;
Heading(builder, title, markdown);
foreach (var group in rows.GroupBy(x => x.ItemType))
{
builder.AppendLine(markdown ? $"### {group.Key}" : group.Key);
foreach (var item in group) builder.AppendLine($"- {item.Title}{(string.IsNullOrWhiteSpace(item.Status) ? "" : $" [{item.Status}]")}{(string.IsNullOrWhiteSpace(item.Detail) ? "" : $": {item.Detail}")}");
builder.AppendLine();
}
}
private static void AppendResearch(StringBuilder builder, IReadOnlyList<ExportResearchItem> rows, bool markdown)
{
if (!rows.Any()) return;
Heading(builder, "Research Pack", markdown);
foreach (var item in rows)
{
builder.AppendLine($"- {item.BookTitle} / Ch {item.ChapterNumber} / Scene {item.SceneNumber}: {item.Topic} - {item.Title}");
if (!string.IsNullOrWhiteSpace(item.Detail)) builder.AppendLine($" {item.Detail}");
if (!string.IsNullOrWhiteSpace(item.Reference)) builder.AppendLine($" {item.Reference}");
}
builder.AppendLine();
}
private static void AppendSearch(StringBuilder builder, IReadOnlyList<StoryBibleSearchResult> rows, bool markdown)
{
if (!rows.Any()) return;
Heading(builder, "Search Results", markdown);
foreach (var group in rows.GroupBy(x => x.ResultType))
{
builder.AppendLine(markdown ? $"### {group.Key}" : group.Key);
foreach (var result in group) builder.AppendLine($"- {result.Title}{(string.IsNullOrWhiteSpace(result.Detail) ? "" : $": {result.Detail}")}");
}
}
private static void AppendAttention(StringBuilder builder, IReadOnlyList<StoryBibleAttentionItem> rows)
{
if (!rows.Any()) return;
builder.AppendLine("## Open Questions and Warnings").AppendLine();
foreach (var item in rows) builder.AppendLine($"- {item.Category}: {item.Title}{(string.IsNullOrWhiteSpace(item.Detail) ? "" : $" - {item.Detail}")}");
builder.AppendLine();
}
private static void AppendRevision(StringBuilder builder, IReadOnlyList<StoryBibleRevisionItem> rows)
{
if (!rows.Any()) return;
builder.AppendLine("## Revision Status").AppendLine();
foreach (var item in rows) builder.AppendLine($"- {item.GroupName}: {item.ItemCount} {item.Detail}");
builder.AppendLine();
}
}
public sealed class AssetService(IProjectRepository projects, IAssetRepository assets, ILocationRepository locations) : IAssetService
{
public async Task<StoryAssetListViewModel?> GetAssetsAsync(int projectId)

View File

@ -0,0 +1,278 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
CREATE OR ALTER PROCEDURE dbo.Export_StoryBible
@ProjectID int
AS
BEGIN
SET NOCOUNT ON;
EXEC dbo.StoryBible_CharacterSummaries @ProjectID;
EXEC dbo.StoryBible_ThreadSummaries @ProjectID;
EXEC dbo.StoryBible_AssetSummaries @ProjectID;
EXEC dbo.StoryBible_RelationshipSummaries @ProjectID;
EXEC dbo.StoryBible_LocationSummaries @ProjectID;
EXEC dbo.StoryBible_TimelineSummary @ProjectID;
EXEC dbo.StoryBible_OpenQuestions @ProjectID;
EXEC dbo.StoryBible_RevisionProgress @ProjectID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Export_NarrativeTimeline
@ProjectID int
AS
BEGIN
SET NOCOUNT ON;
EXEC dbo.StoryBible_TimelineSummary @ProjectID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Export_SceneBrief
@SceneID int
AS
BEGIN
SET NOCOUNT ON;
SELECT p.ProjectID, p.ProjectName, b.BookID, b.BookTitle, c.ChapterID, c.ChapterNumber, c.ChapterTitle,
s.SceneID, s.SceneNumber, s.SceneTitle, s.Summary, s.ScenePurposeNotes, s.SceneOutcomeNotes,
rs.StatusName AS RevisionStatusName,
pov.CharacterName AS PovCharacterName,
location.LocationPath,
COALESCE(s.RelativeTimeText, CONVERT(nvarchar(30), s.StartDateTime, 113), N'Time not set') AS TimeLabel,
COALESCE(sw.DraftStatus, N'Planned') AS DraftStatus,
COALESCE(sw.IsBlocked, 0) AS IsBlocked,
sw.BlockedReason,
sw.Priority
FROM dbo.Scenes s
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = c.BookID
INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID
INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = s.RevisionStatusID
LEFT JOIN dbo.Characters pov ON pov.CharacterID = COALESCE(s.POVCharacterID, c.POVCharacterID)
LEFT JOIN dbo.LocationPaths location ON location.LocationID = s.PrimaryLocationID
LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID
WHERE s.SceneID = @SceneID;
SELECT N'Purpose' AS ItemType, spt.PurposeName AS Title, CAST(NULL AS nvarchar(max)) AS Detail, CAST(NULL AS nvarchar(100)) AS Status, spt.SortOrder
FROM dbo.ScenePurposes sp
INNER JOIN dbo.ScenePurposeTypes spt ON spt.ScenePurposeTypeID = sp.ScenePurposeTypeID
WHERE sp.SceneID = @SceneID
UNION ALL
SELECT N'Character', ch.CharacterName, CONCAT(ISNULL(role.TypeName, N'No role'), N' / ', ISNULL(presence.TypeName, N'No presence'), N'. ', ISNULL(sc.AppearanceNotes, N'')), ISNULL(location.LocationPath, N'No location'), 100
FROM dbo.SceneCharacters sc
INNER JOIN dbo.Characters ch ON ch.CharacterID = sc.CharacterID
LEFT JOIN dbo.CharacterRoleInSceneTypes role ON role.CharacterRoleInSceneTypeID = sc.RoleInSceneTypeID
LEFT JOIN dbo.PresenceTypes presence ON presence.PresenceTypeID = sc.PresenceTypeID
LEFT JOIN dbo.LocationPaths location ON location.LocationID = sc.LocationID
WHERE sc.SceneID = @SceneID
UNION ALL
SELECT N'Plot Thread', pt.ThreadTitle, CONCAT(tet.TypeName, N': ', te.EventTitle, N'. ', ISNULL(te.EventDescription, N'')), CAST(pt.Importance AS nvarchar(20)), 200
FROM dbo.ThreadEvents te
INNER JOIN dbo.PlotThreads pt ON pt.PlotThreadID = te.PlotThreadID
INNER JOIN dbo.ThreadEventTypes tet ON tet.ThreadEventTypeID = te.EventTypeID
WHERE te.SceneID = @SceneID
UNION ALL
SELECT N'Asset', sa.AssetName, CONCAT(aet.TypeName, N': ', ae.EventTitle, N'. ', ISNULL(ae.EventDescription, N'')), ast.StateName, 300
FROM dbo.AssetEvents ae
INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ae.StoryAssetID
INNER JOIN dbo.AssetEventTypes aet ON aet.AssetEventTypeID = ae.AssetEventTypeID
LEFT JOIN dbo.AssetStates ast ON ast.AssetStateID = ae.ToStateID
WHERE ae.SceneID = @SceneID
UNION ALL
SELECT N'Relationship', CONCAT(ca.CharacterName, N' / ', cb.CharacterName), CONCAT(rs.StateName, N'. ', ISNULL(re.Description, N'')), CAST(re.Intensity AS nvarchar(20)), 400
FROM dbo.RelationshipEvents re
INNER JOIN dbo.CharacterRelationships cr ON cr.CharacterRelationshipID = re.CharacterRelationshipID
INNER JOIN dbo.Characters ca ON ca.CharacterID = cr.CharacterAID
INNER JOIN dbo.Characters cb ON cb.CharacterID = cr.CharacterBID
INNER JOIN dbo.RelationshipStates rs ON rs.RelationshipStateID = re.RelationshipStateID
WHERE re.SceneID = @SceneID
UNION ALL
SELECT N'Note', COALESCE(sn.NoteTitle, snt.TypeName), sn.NoteText, CASE WHEN sn.IsResolved = 1 THEN N'Resolved' ELSE N'Open' END, 500
FROM dbo.SceneNotes sn
INNER JOIN dbo.SceneNoteTypes snt ON snt.SceneNoteTypeID = sn.SceneNoteTypeID
WHERE sn.SceneID = @SceneID
UNION ALL
SELECT N'Attachment', sa.Title, CONCAT(ISNULL(sa.Notes, N''), N' ', ISNULL(sa.ExternalUrl, N''), N' ', ISNULL(sa.FilePath, N'')), sat.TypeName, 600
FROM dbo.SceneAttachments sa
INNER JOIN dbo.SceneAttachmentTypes sat ON sat.SceneAttachmentTypeID = sa.SceneAttachmentTypeID
WHERE sa.SceneID = @SceneID
UNION ALL
SELECT N'Checklist', sci.ItemText, CAST(NULL AS nvarchar(max)), CASE WHEN sci.IsCompleted = 1 THEN N'Done' ELSE N'Open' END, 700
FROM dbo.SceneChecklistItems sci
WHERE sci.SceneID = @SceneID
UNION ALL
SELECT N'Warning', wt.TypeName, cw.Message, ws.SeverityName, 800
FROM dbo.ContinuityWarnings cw
INNER JOIN dbo.WarningTypes wt ON wt.WarningTypeID = cw.WarningTypeID
INNER JOIN dbo.WarningSeverities ws ON ws.WarningSeverityID = cw.WarningSeverityID
WHERE cw.SceneID = @SceneID AND cw.IsDismissed = 0
ORDER BY SortOrder, ItemType, Title;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Export_ChapterBrief
@ChapterID int
AS
BEGIN
SET NOCOUNT ON;
SELECT p.ProjectID, p.ProjectName, b.BookID, b.BookTitle, c.ChapterID, c.ChapterNumber, c.ChapterTitle,
c.Summary, rs.StatusName AS RevisionStatusName, cg.GoalSummary, cg.EmotionalGoal, cg.ReaderTakeaway,
cg.MustInclude, cg.RisksOrConcerns
FROM dbo.Chapters c
INNER JOIN dbo.Books b ON b.BookID = c.BookID
INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID
INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = c.RevisionStatusID
LEFT JOIN dbo.ChapterGoals cg ON cg.ChapterID = c.ChapterID
WHERE c.ChapterID = @ChapterID;
SELECT b.BookID, b.BookTitle, c.ChapterID, c.ChapterNumber, c.ChapterTitle, s.SceneID, s.SceneNumber,
s.SceneTitle, s.Summary, pov.CharacterName AS PovCharacterName, location.LocationPath, rs.StatusName AS RevisionStatusName
FROM dbo.Scenes s
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = c.BookID
INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = s.RevisionStatusID
LEFT JOIN dbo.Characters pov ON pov.CharacterID = COALESCE(s.POVCharacterID, c.POVCharacterID)
LEFT JOIN dbo.LocationPaths location ON location.LocationID = s.PrimaryLocationID
WHERE s.ChapterID = @ChapterID AND s.IsArchived = 0
ORDER BY s.SortOrder, s.SceneID;
SELECT N'Warning' AS ItemType, wt.TypeName AS Title, cw.Message AS Detail, ws.SeverityName AS Status, 10 AS SortOrder
FROM dbo.ContinuityWarnings cw
INNER JOIN dbo.WarningTypes wt ON wt.WarningTypeID = cw.WarningTypeID
INNER JOIN dbo.WarningSeverities ws ON ws.WarningSeverityID = cw.WarningSeverityID
WHERE cw.ChapterID = @ChapterID AND cw.IsDismissed = 0
UNION ALL
SELECT N'Open Thread', pt.ThreadTitle, pt.Summary, ts.StatusName, 20
FROM dbo.ThreadEvents te
INNER JOIN dbo.Scenes s ON s.SceneID = te.SceneID
INNER JOIN dbo.PlotThreads pt ON pt.PlotThreadID = te.PlotThreadID
INNER JOIN dbo.ThreadStatuses ts ON ts.ThreadStatusID = pt.ThreadStatusID
WHERE s.ChapterID = @ChapterID AND pt.IsArchived = 0 AND ts.IsOpenStatus = 1
UNION ALL
SELECT N'Open Asset', sa.AssetName, sa.Description, ast.StateName, 30
FROM dbo.AssetEvents ae
INNER JOIN dbo.Scenes s ON s.SceneID = ae.SceneID
INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ae.StoryAssetID
LEFT JOIN dbo.AssetStates ast ON ast.AssetStateID = sa.CurrentStateID
WHERE s.ChapterID = @ChapterID AND sa.IsArchived = 0 AND sa.IsResolved = 0
UNION ALL
SELECT N'Open Note', COALESCE(sn.NoteTitle, snt.TypeName), sn.NoteText, snt.TypeName, 40
FROM dbo.SceneNotes sn
INNER JOIN dbo.SceneNoteTypes snt ON snt.SceneNoteTypeID = sn.SceneNoteTypeID
INNER JOIN dbo.Scenes s ON s.SceneID = sn.SceneID
WHERE s.ChapterID = @ChapterID AND sn.IsResolved = 0
ORDER BY SortOrder, ItemType, Title;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Export_WriterSessionBrief
@SceneID int
AS
BEGIN
SET NOCOUNT ON;
EXEC dbo.Export_SceneBrief @SceneID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Export_CharacterReference
@CharacterID int
AS
BEGIN
SET NOCOUNT ON;
DECLARE @ProjectID int = (SELECT ProjectID FROM dbo.Characters WHERE CharacterID = @CharacterID);
SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID,
Height, EyeColour, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
FROM dbo.Characters
WHERE CharacterID = @CharacterID;
EXEC dbo.Relationship_ListByCharacter @CharacterID;
SELECT sc.SceneCharacterID, sc.SceneID, sc.CharacterID, ch.ProjectID, ch.CharacterName, ch.ShortName, ch.BirthDate, ch.AgeAtSeriesStart,
sc.RoleInSceneTypeID, role.TypeName AS RoleInSceneTypeName, sc.PresenceTypeID, presence.TypeName AS PresenceTypeName,
sc.LocationID, location.LocationName, location.LocationPath, sc.EntryLocationID, entry.LocationName AS EntryLocationName,
sc.ExitLocationID, exitLocation.LocationName AS ExitLocationName, sc.AppearanceNotes, sc.OutfitDescription,
sc.PhysicalCondition, sc.EmotionalState, sc.KnowledgeNotes, s.SceneNumber, s.SceneTitle, c.ChapterNumber, b.BookTitle,
sc.CreatedDate, sc.UpdatedDate
FROM dbo.SceneCharacters sc
INNER JOIN dbo.Characters ch ON ch.CharacterID = sc.CharacterID
INNER JOIN dbo.Scenes s ON s.SceneID = sc.SceneID
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = c.BookID
LEFT JOIN dbo.CharacterRoleInSceneTypes role ON role.CharacterRoleInSceneTypeID = sc.RoleInSceneTypeID
LEFT JOIN dbo.PresenceTypes presence ON presence.PresenceTypeID = sc.PresenceTypeID
LEFT JOIN dbo.LocationPaths location ON location.LocationID = sc.LocationID
LEFT JOIN dbo.LocationPaths entry ON entry.LocationID = sc.EntryLocationID
LEFT JOIN dbo.LocationPaths exitLocation ON exitLocation.LocationID = sc.ExitLocationID
WHERE sc.CharacterID = @CharacterID AND s.IsArchived = 0 AND c.IsArchived = 0 AND b.IsArchived = 0
ORDER BY b.SortOrder, c.SortOrder, s.SortOrder;
EXEC dbo.CharacterKnowledge_ListByCharacter @CharacterID;
SELECT ace.AssetCustodyEventID, ace.StoryAssetID, sa.AssetName, ace.SceneID,
ace.AssetCustodyEventTypeID, acet.TypeName AS CustodyEventTypeName, ace.Description,
STRING_AGG(CONCAT(COALESCE(custodian.CharacterName, aec.CharacterNameText), N' (', cr.RoleName, N')'), N', ') AS CustodianSummary,
ace.CreatedDate, ace.UpdatedDate, b.BookTitle, ch.ChapterNumber, s.SceneNumber, s.SceneTitle
FROM dbo.AssetCustodyEvents ace
INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ace.StoryAssetID
INNER JOIN dbo.AssetCustodyEventTypes acet ON acet.AssetCustodyEventTypeID = ace.AssetCustodyEventTypeID
INNER JOIN dbo.Scenes s ON s.SceneID = ace.SceneID
INNER JOIN dbo.Chapters ch ON ch.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = ch.BookID
INNER JOIN dbo.AssetCustodyEventCharacters aec ON aec.AssetCustodyEventID = ace.AssetCustodyEventID
LEFT JOIN dbo.Characters custodian ON custodian.CharacterID = aec.CharacterID
LEFT JOIN dbo.CustodyRoles cr ON cr.CustodyRoleID = aec.CustodyRoleID
WHERE aec.CharacterID = @CharacterID
GROUP BY ace.AssetCustodyEventID, ace.StoryAssetID, sa.AssetName, ace.SceneID, ace.AssetCustodyEventTypeID, acet.TypeName, ace.Description, ace.CreatedDate, ace.UpdatedDate, b.BookTitle, ch.ChapterNumber, s.SceneNumber, s.SceneTitle, b.SortOrder, ch.SortOrder, s.SortOrder
ORDER BY b.SortOrder, ch.SortOrder, s.SortOrder;
SELECT N'Note' AS ItemType, COALESCE(sn.NoteTitle, snt.TypeName) AS Title, sn.NoteText AS Detail,
CASE WHEN sn.IsResolved = 1 THEN N'Resolved' ELSE N'Open' END AS Status, 10 AS SortOrder
FROM dbo.SceneNotes sn
INNER JOIN dbo.SceneNoteTypes snt ON snt.SceneNoteTypeID = sn.SceneNoteTypeID
INNER JOIN dbo.SceneCharacters sc ON sc.SceneID = sn.SceneID
WHERE sc.CharacterID = @CharacterID AND sn.IsResolved = 0
ORDER BY SortOrder, Title;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Export_ResearchPack
@ProjectID int
AS
BEGIN
SET NOCOUNT ON;
SELECT b.BookTitle, c.ChapterNumber, c.ChapterTitle, s.SceneID, s.SceneNumber, s.SceneTitle,
snt.TypeName AS Topic, COALESCE(sn.NoteTitle, snt.TypeName) AS Title, sn.NoteText AS Detail,
CAST(NULL AS nvarchar(1000)) AS Reference
FROM dbo.SceneNotes sn
INNER JOIN dbo.SceneNoteTypes snt ON snt.SceneNoteTypeID = sn.SceneNoteTypeID
INNER JOIN dbo.Scenes s ON s.SceneID = sn.SceneID
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = c.BookID
WHERE b.ProjectID = @ProjectID AND snt.TypeName IN (N'Research Note', N'Continuity Note', N'Timeline Note') AND s.IsArchived = 0 AND c.IsArchived = 0 AND b.IsArchived = 0
UNION ALL
SELECT b.BookTitle, c.ChapterNumber, c.ChapterTitle, s.SceneID, s.SceneNumber, s.SceneTitle,
sat.TypeName, sa.Title, sa.Notes, COALESCE(sa.ExternalUrl, sa.FilePath)
FROM dbo.SceneAttachments sa
INNER JOIN dbo.SceneAttachmentTypes sat ON sat.SceneAttachmentTypeID = sa.SceneAttachmentTypeID
INNER JOIN dbo.Scenes s ON s.SceneID = sa.SceneID
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = c.BookID
WHERE b.ProjectID = @ProjectID AND s.IsArchived = 0 AND c.IsArchived = 0 AND b.IsArchived = 0
ORDER BY BookTitle, ChapterNumber, SceneNumber, Topic, Title;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Export_SearchResults
@ProjectID int,
@Query nvarchar(200)
AS
BEGIN
SET NOCOUNT ON;
EXEC dbo.StoryBible_Search @ProjectID, @Query;
END;
GO

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
using PlotLine.Models;
@ -249,6 +250,7 @@ public sealed class SceneAttachmentEditViewModel
public string? FilePath { get; set; }
public string? ExternalUrl { get; set; }
public string? Notes { get; set; }
public IFormFile? UploadedFile { get; set; }
}
public sealed class ChapterGoalEditViewModel

View File

@ -42,6 +42,7 @@
<a class="btn btn-outline-secondary" asp-action="Edit" asp-route-id="@Model.Chapter.ChapterID">Edit chapter</a>
<a class="btn btn-outline-primary" asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.Book.BookID">Timeline</a>
<a class="btn btn-outline-primary" asp-controller="Writer" asp-action="Chapter" asp-route-id="@Model.Chapter.ChapterID">Chapter workflow</a>
<a class="btn btn-outline-primary" asp-controller="Exports" asp-action="ChapterBrief" asp-route-id="@Model.Chapter.ChapterID">Chapter brief</a>
<a class="btn btn-primary" asp-controller="Scenes" asp-action="Create" asp-route-chapterId="@Model.Chapter.ChapterID">Add scene</a>
</div>
</div>

View File

@ -33,6 +33,8 @@
<div class="button-row">
<a class="btn btn-outline-secondary" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Characters</a>
<a class="btn btn-outline-secondary" asp-controller="Timeline" asp-action="Index" asp-route-ProjectID="@Model.Project.ProjectID" asp-route-SelectedSceneID="@firstAppearance?.SceneID" asp-route-FocusType="character" asp-route-FocusID="@Model.Character.CharacterID">Back to timeline</a>
<a class="btn btn-outline-primary" asp-controller="Exports" asp-action="Character" asp-route-id="@Model.Character.CharacterID">Export character</a>
<a class="btn btn-outline-secondary" asp-controller="Exports" asp-action="Character" asp-route-id="@Model.Character.CharacterID" asp-route-format="markdown">Markdown</a>
<a class="btn btn-outline-primary" asp-action="Edit" asp-route-id="@Model.Character.CharacterID">Edit character</a>
</div>
</div>

View File

@ -41,7 +41,15 @@
<p class="eyebrow">@Model.Book?.BookTitle / Chapter @Model.Chapter?.ChapterNumber</p>
<h1>@ViewData["Title"]</h1>
</div>
<a class="btn btn-outline-secondary" asp-controller="Chapters" asp-action="Details" asp-route-id="@Model.ChapterID">Back to chapter</a>
<div class="button-row">
@if (Model.SceneID > 0)
{
<a class="btn btn-outline-primary" asp-controller="Exports" asp-action="SceneBrief" asp-route-id="@Model.SceneID">Scene brief</a>
<a class="btn btn-outline-secondary" asp-controller="Exports" asp-action="SceneBrief" asp-route-id="@Model.SceneID" asp-route-format="markdown">Markdown</a>
<a class="btn btn-outline-primary" asp-controller="Exports" asp-action="WritingSession" asp-route-sceneId="@Model.SceneID">Prepare Writing Session</a>
}
<a class="btn btn-outline-secondary" asp-controller="Chapters" asp-action="Details" asp-route-id="@Model.ChapterID">Back to chapter</a>
</div>
</div>
<section class="edit-panel scene-editor">

View File

@ -384,19 +384,50 @@
}
else
{
<div class="writer-note-list">
<div class="attachment-card-list">
@foreach (var attachment in Model.Attachments)
{
<details class="writer-mini-card">
<summary>
var hasFile = !string.IsNullOrWhiteSpace(attachment.FilePath);
var fileHref = hasFile && attachment.FilePath!.StartsWith("http", StringComparison.OrdinalIgnoreCase)
? attachment.FilePath
: hasFile && attachment.FilePath!.StartsWith("/")
? attachment.FilePath
: hasFile && !System.IO.Path.IsPathRooted(attachment.FilePath!)
? "/" + attachment.FilePath!.TrimStart('/', '\\')
: null;
<details class="writer-mini-card attachment-card">
<summary class="attachment-summary">
<span class="attachment-kind">@(hasFile ? "File" : "Link")</span>
<span class="status-pill">@attachment.TypeName</span>
<strong>@attachment.Title</strong>
</summary>
<form asp-controller="Scenes" asp-action="SaveAttachment" method="post" class="writer-edit-form">
<div class="attachment-card-body">
@if (!string.IsNullOrWhiteSpace(attachment.Notes))
{
<p>@attachment.Notes</p>
}
@if (hasFile)
{
<p class="muted">@System.IO.Path.GetFileName(attachment.FilePath)</p>
}
<div class="button-row compact-buttons">
@if (!string.IsNullOrWhiteSpace(fileHref))
{
<a class="btn btn-outline-secondary btn-sm" href="@fileHref" target="_blank" rel="noopener">Open</a>
<a class="btn btn-outline-secondary btn-sm" href="@fileHref" download>Download</a>
}
@if (!string.IsNullOrWhiteSpace(attachment.ExternalUrl))
{
<a class="btn btn-outline-secondary btn-sm" href="@attachment.ExternalUrl" target="_blank" rel="noopener">Open link</a>
}
</div>
</div>
<form asp-controller="Scenes" asp-action="SaveAttachment" method="post" enctype="multipart/form-data" class="writer-edit-form">
<input type="hidden" name="SceneAttachmentID" value="@attachment.SceneAttachmentID" />
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<input type="hidden" name="FilePath" value="@attachment.FilePath" />
<select class="form-select form-select-sm" name="SceneAttachmentTypeID">
@foreach (var option in Model.SceneAttachmentTypeOptions)
{
@ -404,8 +435,9 @@
}
</select>
<input class="form-control form-control-sm" name="Title" value="@attachment.Title" />
<input class="form-control form-control-sm" name="ExternalUrl" value="@attachment.ExternalUrl" placeholder="External URL" />
<input class="form-control form-control-sm" name="FilePath" value="@attachment.FilePath" placeholder="File path" />
<label class="form-label mb-0">Replace file</label>
<input class="form-control form-control-sm" name="UploadedFile" type="file" />
<input class="form-control form-control-sm" name="ExternalUrl" value="@attachment.ExternalUrl" placeholder="Or external URL" />
<textarea class="form-control form-control-sm" name="Notes" rows="2">@attachment.Notes</textarea>
<button class="btn btn-primary btn-sm" type="submit">Save attachment</button>
</form>
@ -414,31 +446,31 @@
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<input type="hidden" name="filePath" value="@attachment.FilePath" />
<button class="btn btn-outline-danger btn-sm" type="submit">Remove attachment</button>
</form>
@if (!string.IsNullOrWhiteSpace(attachment.ExternalUrl))
{
<a href="@attachment.ExternalUrl" target="_blank" rel="noopener">Open link</a>
}
@if (!string.IsNullOrWhiteSpace(attachment.FilePath))
{
<p class="muted">@attachment.FilePath</p>
}
</details>
}
</div>
}
<form asp-controller="Scenes" asp-action="SaveAttachment" method="post" class="writer-add-form">
<details class="writer-mini-card attachment-add-card">
<summary><strong>+ Add Attachment</strong></summary>
<form asp-controller="Scenes" asp-action="SaveAttachment" method="post" enctype="multipart/form-data" class="writer-add-form">
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<label class="form-label mb-0">Attachment type</label>
<select class="form-select form-select-sm" name="SceneAttachmentTypeID" asp-items="Model.SceneAttachmentTypeOptions"></select>
<label class="form-label mb-0">Title</label>
<input class="form-control form-control-sm" name="Title" placeholder="Reference title" />
<input class="form-control form-control-sm" name="ExternalUrl" placeholder="External URL" />
<input class="form-control form-control-sm" name="FilePath" placeholder="File path" />
<label class="form-label mb-0">Choose file</label>
<input class="form-control form-control-sm" name="UploadedFile" type="file" />
<label class="form-label mb-0">Or external URL</label>
<input class="form-control form-control-sm" name="ExternalUrl" placeholder="https://..." />
<textarea class="form-control form-control-sm" name="Notes" rows="2" placeholder="Why this reference matters"></textarea>
<button class="btn btn-outline-primary btn-sm" type="submit">Add attachment</button>
</form>
</details>
</section>
<section class="inspector-section warning-inspector-section" data-section-title="Continuity Warnings" data-section-id="inspector-warnings">

View File

@ -42,6 +42,14 @@
</form>
</div>
<div class="button-row export-action-row">
<a class="btn btn-outline-primary btn-sm" asp-controller="Exports" asp-action="StoryBible" asp-route-projectId="@Model.Project.ProjectID">Printable Story Bible</a>
<a class="btn btn-outline-secondary btn-sm" asp-controller="Exports" asp-action="StoryBible" asp-route-projectId="@Model.Project.ProjectID" asp-route-format="markdown">Markdown</a>
<a class="btn btn-outline-secondary btn-sm" asp-controller="Exports" asp-action="StoryBible" asp-route-projectId="@Model.Project.ProjectID" asp-route-format="text">Text</a>
<a class="btn btn-outline-primary btn-sm" asp-controller="Exports" asp-action="NarrativeTimeline" asp-route-projectId="@Model.Project.ProjectID">Timeline export</a>
<a class="btn btn-outline-primary btn-sm" asp-controller="Exports" asp-action="ResearchPack" asp-route-projectId="@Model.Project.ProjectID">Research pack</a>
</div>
<nav class="story-bible-nav" aria-label="Story Bible sections">
<a href="#characters">Characters</a>
<a href="#threads">Plot Threads</a>
@ -62,6 +70,7 @@
<h2>Results for "@Model.Query"</h2>
</div>
<a class="btn btn-outline-secondary btn-sm" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Clear search</a>
<a class="btn btn-outline-primary btn-sm" asp-controller="Exports" asp-action="Search" asp-route-projectId="@Model.Project.ProjectID" asp-route-q="@Model.Query" asp-route-format="markdown">Export results</a>
</div>
@if (!Model.SearchResults.Any())
{

View File

@ -23,6 +23,8 @@
</div>
<div class="button-row">
<a class="btn btn-outline-primary" asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.Book.BookID">Open timeline</a>
<a class="btn btn-outline-primary" asp-controller="Exports" asp-action="ChapterBrief" asp-route-id="@Model.Chapter.ChapterID">Export brief</a>
<a class="btn btn-outline-secondary" asp-controller="Exports" asp-action="ChapterBrief" asp-route-id="@Model.Chapter.ChapterID" asp-route-format="markdown">Markdown</a>
<a class="btn btn-outline-secondary" asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.Book.BookID">Warnings</a>
</div>
</div>

View File

@ -42,6 +42,7 @@ else
}
<div class="story-bible-actions">
<a asp-controller="Scenes" asp-action="Edit" asp-route-id="@scene.SceneID">Open scene</a>
<a asp-controller="Exports" asp-action="WritingSession" asp-route-sceneId="@scene.SceneID">Session brief</a>
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@scene.ProjectID" asp-route-bookId="@scene.BookID" asp-route-selectedSceneId="@scene.SceneID">Timeline</a>
<a asp-controller="Warnings" asp-action="Index" asp-route-projectId="@scene.ProjectID" asp-route-bookId="@scene.BookID" asp-route-sceneId="@scene.SceneID">Warnings</a>
</div>

View File

@ -2644,6 +2644,39 @@ body.dragging-location [data-drag-type="location"] {
white-space: nowrap;
}
.attachment-card-list {
display: grid;
gap: 10px;
}
.attachment-summary {
align-items: center;
}
.attachment-kind {
border-radius: 999px;
padding: 3px 8px;
background: rgba(47, 111, 99, 0.1);
color: var(--plotline-accent);
font-size: 0.76rem;
font-weight: 800;
text-transform: uppercase;
}
.attachment-card-body {
display: grid;
gap: 6px;
margin-top: 10px;
}
.attachment-card-body p {
margin: 0;
}
.attachment-add-card {
margin-top: 10px;
}
.writer-checklist {
display: grid;
gap: 6px;
@ -2709,6 +2742,26 @@ body.dragging-location [data-drag-type="location"] {
line-height: 1;
}
.export-action-row {
margin: -8px 0 18px;
}
@media print {
header,
.breadcrumb-trail,
.context-header,
.button-row,
.story-bible-nav,
.inspector-quick-nav {
display: none !important;
}
.plotline-shell {
max-width: none;
padding: 0;
}
}
@media (max-width: 760px) {
.plotline-shell {
padding-left: 14px;

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 KiB