Phase 1L
This commit is contained in:
parent
6e237b7287
commit
0496c4d242
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSTT2/cNhDFv4rAsyjzn0hRN2Odogs38SJZpEVvFGcUE5apBUU5CYz97gG1GzjprRdBGOjNvPeb0SuJ7hlJT25Pp3cvGPNCapLDVhJMaMo0ZfzITN+2vZIN10YKLv4lNQn3+J30xCjvubdIJWqkClhLBzl6CoCWMw5MtKr0dF8W0r8SFxrAl+CxmZfPmJYwR9KTv0OE+WuZ7ULjp3mFJs0T7uOSXfTFzIf97p7+dXs4PhwuXy24FHETgPSEGyaEUIZ2ykuqOhhpB5ZT3Y6ghnZw3rqLLMSMKbqpWeDpbT7MOWL2vWiEbBgVlpxrAi67YnlwCx6/n4qLDdFdqddbeXvtX8kLJtKL+ifMS7sbP4Wb52VYwwQ3l2d2y1NJeUrzCVMOuDHZYk6Ty2GO1XXSbgoYM6nJw6fqMLk8zun5N1CfLvn3/yP+sUx/9w39mhF28xpz0UpDavLe+ccQsdrfkZ4wjgJkh3QUXJd2jrpWWqq11CNyb43gpCa7OeYQ13ldqn3M+CVtAUhP/nDTghfrt8k/how+r6mk+keXWzikGVafqzf+nDWskYwV0ZpPa64+IoSEPmPJd0zrtd9/JEKLTbRbU8KYq4PLj9WfbnkkPbGu89xZZ53T3o/YMhwZMKeZAdSgEZQAA8oq0Moyr5UbuALX8mEsFVITLOuutguz0jA5qIGyDhlVQijaGddSawZjOxgkUwXxG8fqYSo65zumECS1OAJVljtqlZQUZOuM0KMxYvwFyXX7nBXAgC+Xf2VzoAE6BM2pULylylpOO9saOphusIPusPXF8z2miNMvoN4Hn+ZlHnN1PZ7qN3Qf11j+92o7pK8h0m/bju5m/4SpKjt2IZbzvq71fD6ffwAAAP//AwCvA78iNgQAAA==
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTwW7bOBD9FYFnUSEpihJ1C5xd1Ei7MdqgLXobcUY1UYUyKCptEfjfF5RdpN3b3ohHvpk37w1fWIAnYj27PZ3+eqaQFlay5DdICWW4MFzIR9H2TdPrupKtakXTfGEl8/f0k/Ws1c5JZ4nXZIhrFA0f6tFxRLJSSBSq0bkmfF1Y/8LAV0jP3lE1Lx8pLn4OrGeffMD5e+4NvnLTvGIV54n2YUkQXBbzz353z9/eHh4fDpdXCy2ZXHlkPZOtUErplnfa1Vx3OPIOreSmGVEPzQDOwoXmQ6IYYKoW/PbaH+cUKLleVaquBFeWnUuGkCBLHmChx5+nrGKz6C7j5QZvx/6FPVNkvSp/mXkpd+Mmf+PmpycIeDP64JcjK9kpzieKydNmxzbhNEHycyiuTXaTp5BYyR4+FIcJ0jjHpz88+nAZff8/Jn8H7ugDFfs71jMhSWHdER+VNJkDHJracmNqM5J0tlWSlWw3h+TDOq9LsQ+JvsZNJevZ3zAtdNF3G93RJ3JpjVn6Z5Ozph8+7WbMgGAlO8QZV5eKV7ulqERVi3z5sKbTmor3hD6SS5Rneozrtfx/KMqojbRbY6SQigOkY/EGliPrmYXOSbBgAYxzIzWCRoECjGiRDBpCrbBFbTUabYUzGgapERo5jBnJunO6xbZQ2liDQkqOYDquW6E52LrhQ9MNDvQgqM4WvdpaPEyZB64TmrDmlkbk2krgVtc1x7qBVpmxbdX4myXXxKXIxZCeL19jU2AQO0IjudKy4dpayTvbtHxou8EOpqPGZc33FANNvxn1zrs4L/OYiuvCFH9Y934N+XsX2/J894H/2CK7m903ikWOHHzI23xN+Xw+n/8FAAD//wMAJ8SPOyUEAAA=
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACoxUy27kNhD8FYHXiDJfokjdHNuLDBw/sDNIgtxaZMtmLFMDirJ3YfjfA2ns9WMRIBdBaJJd1dXV/UQi3CNpyfF+f/aAMU+kJDmsIcGEpkxTxnesaeu6VbLiDau15n+TkoRz/E5a0ijnuLNIJWqkyrOadrJ31Hu0nHHPRK2WnHAzkfaJQKg8PgSH1Tj9gWkKYyQt+TNEPz4u2BAqN4yzr9I44CZOGaJbyFxuTs7p78fXu6vrw60Jp+VxFTxpCW+YEEI11CgnqTK+p8ZbTnXde9XVHTgLh2chZkwRhmryd2/4fswRs2tFJWTFqLDkuSQeMiyUO5hw932/sFglOl3i5Rpef9sn8oCJtKJ8FfOQ7sgN4eh+6uYw+KP1S0qyT+MeUw64qnG1Ld5IcFaxSmjBGCnJBbjbELHYnBZXw1IiOMMUekkt9p4qy4FaJSX1soZG6L5pRE9Kco4p4vAu60VwaZzGPhcvIhcfcLbHJ2cRugEXjC8wTEhKspm2GaKHYYx49g3dnA+5dmlejk/GmEOcx3kqNjHjTYKX89f3J3NKGHNxDfm2+A2mW9ISC8ZxsGABtHM91gx75hlo1njUXqNXwjdeWeW1ssxpBR1XHmre9UuElOTXRcPt7BxO0xub1STDsHIoXvp0MgSM+YOMpCWMo/DSIO0F14tLgEItLdVa6h65s43grzA7SDeYSUt603Udc64TynIHwoL0PZNe91aIpgHVy7pRQuuaNeB80wiuvGzQgewlM4KU5DqNfnb5c6vl2oDrNP6DLi9SkZZoVtfGQdcJrbgBa1BxrYy22FjVA1PSdcBrA13HGdTMWSMccG1Bdb7uf6h0Fm9CxNMw7Qf4/g7XVLqSvzgAw3tQn65/SXCPj2O6uzzYuLo825GSfJ3jshGKzWKRxxDpN71M9MU85LC7TQge/cXo8Wcf4TIuxWFCbW28YpLqpkaqhOPUcGVo4xB75TrXWU1Kcjq6O0zF4jAIcZmqH8mutsVxcrcho8tzWvj9tfK42hbXA+R+TPcfNsm7wj4LUAnJ2dIZjw+HbbRy1N4b9JpToXhNlbWcGls3tGtMZzttsHaLYldz3s+5+Io+JHR5rfjFiyvmyS26u5/FeHXBi0U5W7y2PWyxVdr/tcSeS3KPMM0J79dt3T6RTYyYVuTT+TCKm3gRhiFM6MboJ9Jq0ZjKGmZfGP7nPVnbSinJn5+fn/8FAAD//wMA8VM0JxsGAAA=
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSU0W7cKhCGX8Xi2jiAMTa+izatukrbrJpVe3TuxjBuUBy8Apw2ivbdK+yt2lwdnRuExvzDP98MfiUenpD05Pp0eveMPkVSkuTWkGBCUaYo40fW9k3Ty7riqlGiVf+SkrhbfCE9aaUx3GikNSqk0rKGDvVoqLWoOeOWiUbmnPA9kv6VgKssPjuD1Ry/Yohu9qQn35y38498N7jKTPNiqzBPuPcxgTfZzOf97pZ+vD4c7w7bqYgxiytnSU94y4QQsqWdNDWVnR1pZzWnqhmtHJoBjIZN5nzC4GGqon38c7+dk8dkelGJumJUaHIuiYUE2fIAEY8vp+xiRXST4+UaXrf9K3nGQHpR/oa5pbsyk7t6isPiJnu1rQniY1zXEUyawwspySnMJwzJ4crnOkZ8GqaXI8TH99uZvI3vfqJZEtrdvPiUK64z1RXQNEFysy8uHneTQ5/Wjyk4H535r1w1KcndfXGYII1zeHrTj/sN8/5/UP4E5sF5LPY3pCeMo7B1h3QUXGUNUGhqTZWq1Yjc6FZwUpLd7JPzy7zEYu8Tfg9rSaQn72GKuPm7DubBJTRpCbnOf1QmcAizXUwq/vSSs4pVNWNZtKTTkoovaF1AkzAXcQzLJd/XtxKhxCraLSGgT8UB0kPxAeID6YmGznDQoAGUMSM2DEdmGSjWWlRWoZXCtlZqaZXUzCgJA5cWGj6MOUJKgnl0inValWlbVMJSQItUKom0q7uRGgVty0dRKz284VjcTVkHpmMSbU01jpZKzYFqWdfU1g20Qo1tK8a/kFzmgbMM2OLz9u42B9Z2aBWnQvKGSq057XTT0qHtBj2oDhuTPd9i8Dj9BeqTM2GO85iKy4QUb9B9WXz+dxTrtPxwnv5ce3Qzm0cMRe4xOJ+fyqWt5/P5/AsAAP//AwC9ok9gggQAAA==
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUTW/cRgyG/4owZ4083x+6GXaLum7qRbxIi95GQ8oWopUWo9GmgbH/PRjtBol7600g9JKchy/5RqZwQNKS2+PxlxNOeSE1ycMWEkwYygxlfM9cy03LXCO5lU7rf0hNhkf8SlpiVYw8eqQSDVIFTNNO9pECoOeMAxNalZzhZSHtGwlDA3gaIjbz8gnTMswTaclfwwTzl1I7DE0c5xWaNI/4MC05TLE08+fD3SP943a3f9pd/lpwKeJmANISMJYzNI468JYqxYB2PQiqteLIHWoX4kU2TBnTFMZmgc8/6sOcJ8yxFY2QDaPCk3NNIORQWu7Cgvuvx9LFhui+xOstvH22b+SEibSi/g7zku4mjsNNno8jnnA8hrRguonz4RAmIDU5pvmIKQ+4YdleOo4hD/NUXYvdjQNOmdTk6bnajSH3czq8Y/V8QfDwPwicMHWkJZJpaaxFJaUM3nSCIVgTjRFdDMz2nUAekWmvrOeWcekF81Ib6ywE5kuml3Huwvj7svHzgnUOmBVGoEIvlDE+MpQOe22RQTCmMy56kM4K5cDJ4EPgvkMbvYnFIB9CfB0mrB7uSUsYRwHSIe0FN1Q5CDRo6akx0vTIo7eCk5rczVMepnVel+phyviSNn6kJb+GccELudsUX4eMMa+pQP3blGK7NMMac/XDAZw1rJGMFdGaj2uuPiIMCWPGgnef1mu+/0iEEZvobk0Jp1ztQn6tfgvLa6ESXOTBl6eaGHvUDHsGLBhmAQ0YBCXAgvIKjPIsGhU6riBo3vUlQmqCxXDV5nEThWGscxSl7qgCkNQF7ymXDHqhe+fxPcfqaSy6EB1TCJJ67IEqzwP1SkoKUgcrTG+t6H9CcjUfZwUw4OmyrZcOAByC4VQorqnynlPntaWddZ3vjEMdS8+PmCYcfwL1YYhpXuY+V1fvVu/QfVyncnGqzcdfhon+u83ofo6fMVVlxmGYyoJdx3quyQHDsiY8bAerfSO7sl3VfrtbQjTKS1WT5xxSXo/XsJaN0uZ8Pp+/AQAA//8DAL7aIIL0BAAA
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACoxUTW/bOBD9KwLPpkJKFEUK2IPhtFgjTWMk7u5ibxQ5jInIokFRToMi/31BWa6dooe9CaP5ePPeG/5AvdoDatDycPh0hD4OaIGim0IFKTgmHBO6JaKhvCEirymVRFT/ogVyd/CGGlQzramWgEvggJkhFW5Lq7ExICmhhhQVSz3V84CaH0i53MDRacj98BeEwfkeNehv1xv/mmYrl+vOjyYPvoN1P0TV6wTm63p1h78sN9uHzSlrgCEV586gBhleUwJcYGFkjRkjBrfWFLiqGAUqoBJKn8pcHyH0qssH83KZb3zsIeqmyIsyJ7iQ6H2BjIoqQW7VANu3Q0IxUXSb4ospPH02P9ARAmqKxZnMU7sb3bmb/dCOrjM3UYVniDaoPbz68AJH1aEFOgR/gBAdTNw8PGUXSJTkJC94QQhaoHuld66HbH2bPXRpYaUFYWBKLMEazCRVWLKyxKasVF1wW9eFRQt0B6GH7qrrvdPBD97GbKY8+zBn5fvo+tGPQ7buIzwHFU91n1U3AFqgzdh2btg9gjJvW/84pn/92KVdVmMI0Mdso+Iu+1MNO9QgqYSmSiqpFNfaQkXAEkMUJ7UBbjgYVpjaMMkMZ5JozlRLmVEVbW2KoAV6GONhjGf+v1+B2Aa334O5INhOHG86Fa0P+7WBPjrrkjTnjMlQXTdtlc09V52DPn4gGTWIUChMKQDbgnLMhFFYVaXEnJfcAtWyLuiJMOuex588EWsNK20lBG9lSwkUthaM1LYlRttSS1O1UBc1qQUIqQlpK0ZFKzUxQCtm0nbBm1HHX61QTgI9jn26zmyd1n51Pf7O2YWRTfDRa98lwVwHT29DhP3vVFv5/cEPLqb97SzttwHufZ/+zvc/czYXL328xCDdQTadHjChLVUMM2FrzLQlWIlS4lobLaTmrGUFWqBbr18gZMlgyvWTJmdPPTxly6B3LoKOY0iT/5mWOsn5+XwyFz7yr5+2Kx9geTgs5ugfx0TSqdnZAB/elW9DSt/5IW0Rw3jloyfo7BnXlZsMHE8vlUtBbowAwykuGK0wk5JiIasat7VoZcsFVPri1uwRjAug49Rve5o2E/s7Wy5DdFbpOKTb+eL15KbZnnPKryCvVPtZfRp+pdzspLkTJcmxT6d3czLQ/3o239/f3/8DAAD//wMAkF/C9ycGAAA=
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUT2/cNhDFv4rAsyjzP0XdDLtFF27qRbxog94ozigWoqUWFLVpYOx3L6jdInFvuQkDveHjb97wjUR/RNKR+9PplzPGvJCa5HErCSYMZYYyfmBtx03H2kZqYSQzf5OajE/4jXTEqhB4cEglGqQKmKa9HAIFQMcZBya0Kj3954V0b8SPDeB5DNjMy5+YlnGOpCN/jRHmr+VsPzZhmldo0jzhLi7Zx1DM/LF7eKK/3+8Pz/vrXwsuRdyMQDoCxnKGpqUtOEuVYkD7AQTVWnHkLerWh6tsjBlT9FOzwJfv58OcI+bQiUbIhlHhyKUm4LMvlnu/4OHbqbjYED2Wer2Vt8/ujZwxkU7U/8G8trsL03i3rP2EZ5xOPi2Y7sJ8PPoIpCanNJ8w5RE3LGdM/byMuRANwfq25Rxab5W20EotpAXJEFpmNThw3igziKD1AH3rrBoUCK7AoFK9JTXZwE2Tz+Mcq5v3h2nEmElNnl+q/eTzMKfjO/QvV6K7nwBabJOOSKalsRaVlNI70wuGYE0wRvTBMzv0AnlApp2yjlvGpRPMSW1sa8EzVzp98OF1jFjtHklHGEcBskU6CG6oasFTr6WjxkgzIA/OCk5q8jDHPMZ1XpdqFzN+Ttt9SUd+9dOC15vep/A6Zgx5TQXCJ1PCuE8zrCFX3wPAWcMayVgRrfm05uojwpgwZCw4Dmm99fufRBixiR7WlDDmau/za/WbX15JR5xvA/fOO+9NCANqhgMD5g2zgAYMghJgQTkFRjkWjPI9V+A174dSITXBkrdqi3gYQLlgy4oZSVWvgPa9GGiv0QYuuIbwnmP1PBWdDy1TCJI6HIAqxz11SkoKUnsrzGCtGH5AcgsLZwUw4Pm6rJsDA9AiGE6F4poq5zhtnba0t23vetOiDsXzE6aI0w+gPowhzcs85OqWteoduo9rLA9OteXu6xjpP9uMHufwBVNVZuzHWPbrNtZLTY7olzXhcXuvujeyL8tVHbZnS4hGOalq8pJ9yuvpVtayUdpcLpfLvwAAAP//AwCQDjtA8wQAAA==
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTTW/UMBD9K9Gc16ntOE6cW7UFsSrQFawAcXM8E2o1dVaOU0DV/nfk7CIoNy6WNfKbeR/jZwj2kaCD6+Px1ROFNMMGkl9LkkvNuGZcHHjbCd0JWbZtY5SSX2ED/pZ+QgeNck44Q6wiTUwhr1lfDY4hkhFcIJe1yj3ttxm6Z7C+RHryjspp/kRx9lOADj77gNP3PNv60o3TgmWcRtqFOdngMpn3u+0te3u9P9ztz69mmjO49AgdoG4EJ92yFk3DlOLI+gElq2slSLRUt9adYT4kisGO5YwPf+bjlAIl18lSViVn0sBpA2iTzZR7O9Ph5zGzWC26yfXNWl6v3TM8UYRObn6beW535UZ/9Tj3ix/x6nwmOz9klcc4HSkmT6snq8xxtMlPobhM2o6eQoIN3H0s9qNNwxQfXxj18ax/9x/yD3n6qx/klkS4nZaQoAOhBGzgnXX3PlCxu4EOuCCJVUtskEIz1aJltq4M07rSAwlnGpkx2ykkH5ZpmYtdSPQtrgKgg9d2nOlM/Tq6e5/IpSVmVV903oV9nHBxqfjjv+AlLyvOM2hJxyUVHwh9JJco6zvE5dLvH4jUcgVtlxgppGJv033xxs730IGxrRPWWGOtdm6gmtPAkVvNGySNmlBJbFAZhVoZ7rSyvVBoa9EPuQIboBx3sW6YrlBKRQ0joxumGj0w2/Ka1a6ukUvVV3J44WNxN2acdS1XhBUzNCBTRlhmVFUxrGrbSD00zYr7bcklfcGzwUhP579yZoDYEmrBpBI1U8YI1pq6YX3T9qbXLdUuc76lGGj8y6h33sVpnoZUXJaneGHdhyXk/16si/TdB/Zjzehmcg8Ui5yx9SGv9yXW0+l0+gUAAP//AwB3KvwmNgQAAA==
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRU0W7cKBT9Fes+GwcwxuC3aNJqR9luRu2oW/UNw3WD4uAR4LRRlH9f4ZlVmqfVviB0zbmce87BLxDMI8IA16fThycMOUEN2W8lTrkkVBLKjlQNTA6MN0p1fU/pd6jB3+IzDNALa5nVSFqUSISjHRnbyRLnUDPKHOWdKD3NjwTDCxjfOHzyFpslfcWY/BJggL99cMvPcrfxjZ2X1TVxmXEfUjbBFjJ/7Xe35M/rw/HucD6VMBVw4x0M4GTPKEpFlNM9EYI6Mk6Ok64TDJnCThl7hvmQMQYzN8k9vN3vlhww24E3vG0o4Rpea3Amm0J5NAmPz6fCYpPoptTrrbxthxd4wggDr/8V89zuys7+6jGNq5/d1XnNJj2kbZ2MzUt8hhpOcTlhzB43fa5Twsdxfj6a9PDxfKZs04dfaNeMbresIcMArFVQwybQPJvsl1BdOO5mjyFvH3P0IXn7X71aqOHuS3WYTZ6W+PjOjy9nmff/Q+VPxt77gNX+BgagDLlrFZKJM0mEcoaYrtVEylZOyKzuOYMadkvIPqzLmqp9yPgjbiPBAB/NnPDM7zrae5/R5jWWOb/JkqtDXNxqc/XmJaMNbVpKC2jNpzVXn9H5iDZjGeIY10u/r+8hXPINtFtjxJCrg8n31R8m3cMA2ijLjDbaGGnthB3FiTpqJO0dSifRCe56J7RwUmhqpTAjE850bJxKBWrAEp1qS6swlis9dURaPRJh+o6M1o5komPPuFLj1BYibzpWd3PBGauoQNcSjZMjQjNDtGhb4trO9FxOfc+n3yS55IHRIrDDp/O72xhI5xQ6yQgXrCNCa0aU7noy9mrUo1TY2cL5FmPA+TehPnkbl7RMubokpHon3ec1lH9HtaXlpw/k1+bRzWIfMFbFY+NDeSoXW19fX1//AQAA//8DAIIp/ySCBAAA
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACoxUTW/jNhD9KwKvFRWSoihSt9TJokaaxFgbbdHbiBzFbGTKoKhkF0H+eyE52XwsCvQiCEPOvMc3b+aJBDggacj58Xj5gCGNJCfJLyHBhKJMUcZ3TDdcNVwU2rBSK/E3yYm/wu+kIbW0lluDtESFVDpW0bbsLHUODWfcMVHJuSbcjaR5IuALhw/eYjGMf2Ac/RBIQ/70wQ2PMzb4wvbD5Io49LgOY4JgZzI369UV/f18s7vdnG6NOM7JhXekIU7VnKHSVDtTUymZo23nBK0qyZFrrDTYU5oPCWOAvhjd/Ru+G1LAZBtRiLJgVBjynBMHCWbKLYy4+36cWSwSXczxfAkvv80TecBIGpG/inkqd2Z7f3YY28n37mz5kpwc43DEmDwuatxuszcSnBWsEEowRnJyDXbvA2bri+y2n58IVjOJrqQGO0el4UCNLEvqygpqobq6Fh3JyRXGgP27qtfexmEcupS9iJx9wNmery4DtD3OGF+gH5HkZD1uEwQH/RDw8hvaKZ1q7eI0H6+GkHyYhmnM1iHhXYSX89f81RQjhpRtIO2z32Dck4YY0JaDAQOgrO2wYtgxx0Cx2qFyCp0UrnbSSKekYVZJaLl0UPG2myMkJ7/OGm4na3Ec39gsJun7hUP20qdV7zGkDzKShjCOwpUaaSe4olI7oFCVhipVqg65NbXgrzA7iHeYSEM63bYts7YV0nALwkDpOlY61Rkh6hpkV1a1FEpVrAbr6lpw6coaLZRdybQgOdnEwU02fW51uTRgE4d/0KZZKtIQxapKW2hboSTXYDRKrqRWBmsjO2CytC3wSkPbcgYVs0YLC1wZkK2ruh8qXYY7H/DCj8cevr/D1YUqyl8sgOYdyE/Xv0Q44OMQ729ONi5uLnckJ1+nMG+EbD1b5NEH+k3NE3099cnv9hHBobseHP7sI5zHJVsmlAkUlbSKMqcZlSUqCpZz2iFjqmSWt3bOuBjsPcZsdhj4ME/Vj2K32+w82r1PaNMUZ35/LTxut9mmh9QN8fBhk7x72GcBClFyNnfG4cNpGy0clXManeJUSF5RaQyn2lQ1bWvdmlZprOys2O2UjlPKvqLzEW1aXvzixQVztUd7/7MYry54sShns9e2py22SPu/lthzTg4I4xTxsGzr5omsQ8C4IF9Mp1Fch2vf935EOwQ3kkZWghVcK/XC8L/vKVMwWern5+fnfwEAAP//AwAzeLKQGwYAAA==
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTwY7cKBD9FYuz8QDGGHwb9ewqrUl2WskoWe0NU+U0igdaGE8SjfrfV7g7Sia33NCDV/XqveKFBPuEZCC3p9NfzxjyQmqS/QYJJhRlijL+yPTA1cBFo7XRSvL/SE38PX4nA+mlc9wZpC0qpBJYR8d2chQADWccmOhkqWk/L2R4IdY3gM/eYROXj5gWHwMZyCcfIH4tva1v3BxXaFKccR+WbIMrYv7Z7+7p29vD48Ph8mrBpZAbD2QgoHrOUGmqwfRUSgZ0nEDQrpMcucZOW3eh+ZAxBTs3C3z52R9iDpjdIBrRNowKQ841AZttkTzaBR+/n4qKzaK7gtcbvB2HF/KMiQyi/mHmpdyNm/2Ni09PNsDN5INfjqQmpxRPmLLHzY5twnm22cdQXZvsZo8hk5o8fKgOs81TTE+vPPpwGX3/B5O/s+7oA1b7OzIQxlFAq5FOgisqNVhqu9ZQpVo1IXemF5zUZBdD9mGN61LtQ8bPaVNJBvK3nRe86LtN7ugzurymIv1fVbLGbz7vIhSAkZocUoTV5eqn3Zw1rGlZuXxY82nN1XsEn9BlLDM9pvVa/jeKUGIj7daUMOTqYPOxemOXIxmIsdpxa6yxVjk3YcdwYsCsYj2gAoUgBfQgjQQlDXNK2pFLsB0fp4IU3SXdaluo3ox25I5RbSZDpQSguuOacrBWt1b0QohXtlYPc+FZp5lEaKnBCag03FIj25ZC29leqKnvxfSLJdfEOSt+Az5fvsamQAFoBMWpkLyj0hhOtel6OvZ6NKPS2Lmi+R5TwPkXo955l+ISp1xdF6Z6Zd37NZTvXW3L89UH+m2L7C66L5iqErn1oWzzNeXz+Xz+HwAA//8DAFXn8oElBAAA
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS6Som2G3qOumXsSLtOhtxBnZQrTSgqI2DYz97wG1GyTurTdhoHdm+Mw788YmOBBr2e3x+MuJprSwkqVhCymhLBeWC7kXTStda1RVW1tL3/zDSjY80lfWMmdCkMET12SJGxQ173QfOCJ5KSQKVZucE14W1r4xGCqk0xCompdPFJdhnljL/homnL/k2jBUYZxXrOI80sO0JJhCbubPh7tH/sftbv+0u/y10JLF1YCsZYRW9p0Q3AVCbpoOOGCN3IL2tledsK67yIYpUZxgrBb8/KM+zmmiFFpVKV0Jrjw7lwwhQW65g4X2X4+5iw3RfY6XW3j7bN/YiSJrVfkd5iXdTRiHmzQfRzrReIS4ULwJ8+EAE7KSHeN8pJgG2rBsLx1HSMM8Fddid+NAU2Ile3oudiOkfo6Hd6yeLwge/geBE8WOtUyLWlvnyGitwdtOCUJng7WqCyBc3ymSgUTtjfPSCam9El7X1jUOQfjASvYyzh2Mvy8bP69E16Bwyioy5JWx1gdBuqG+diQQrO1sEzzqxinTYKPBA0jfkQvehmyQDxBeh4mKh3vWMiFJoW6I90pabhoEDrX23Fpte5LBOyVZye7mKQ3TOq9L8TAleokbP9ayX2Fc6ELuNobXIVFIa8xQ/7a52C7OuIZU/HCAFJWotBBZtKbjmoqPhEOkkCjj3cf1mu8/EmXVJrpbY6QpFTtIr8VvsLxmKtAECT4/1YbQUy2oFyjACodk0RIahQ6NN2iNF8Ea6KRBqGXX5wgrGWXDFZvHmxCEbJTg2iJcF42k4gGdlb3uAIJ/x7F4GrMOQiMMoeaeeuTGS+DeaM1R1+CU7Z1T/U9IruaTIgNGOl22devAIjbZZ1wZWXPjveSNrx3vXNP5zjZUbx08Upxo/AnUhyHEeZn7VFy9W7xD93Gd8sUpNh9/GSb+7zaj+zl8pljkGcMw5QW7jvVcsgPBskY6bAerfWO7vF3Ffrtbqq5qqWTJnhPEtB6vYV1XWtTyfD6fvwEAAP//AwB9Sf0i9QQAAA==
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUT2/cNhDFv4rAsyjzP0XdDLtFDTfNIjbaorcRZxQL0UoLito0MPzdC2q3SNxbb8JAb/j4mzd8ZTMciXXs9nT66UxzXlnN8riXlFCOC8eFfBZtJ31nVOOEFcGqv1jNxkf6xjrmTYwyBuKaHHGDwvJeD5EjUpBColDWlJ7weWXdK4OxQTqPkZpl/Z3SOi4z69gf44zL13I2jE2clg2btEz0MK8Z5ljM/PZw98h/vT08fzxc/lppLeJmRNYxQieHXgjuIyE3bQ8c0CJ3oIMbVC+c7y+ycc6UZpiaFb98Px+XPFOOnWqUbgRXgb3VDCFDsdzDSs/fTsXFjui+1Ou9vH92r+xMiXWq/hfmpd1NnMabdesnOtN0grRSuonL8Qgzspqd0nKilEfasZwp9cs65kI0Rg9tKyW24I312GqrtEctCFvhLQYM4IwbVLR2wL4N3gwGlTToyJjes5rt4KYJ8rjM1dX73TTSnFnNPj5VhwnysKTjO/RPF6IP/wNosc06poXVznsyWmsIrleC0LvonOojCD/0imQkYYPxQXohdVAiaOt86xFEiKxmHyC+jDNVD/esY0KSQt0SH5R03LQIHKwO3DntBpIxeCVZze6WOY/ztmxr9TBn+pz2+7KO/QzTSpeb3qb4MmaKeUsFwp+uhPGQFtxirr4HQIpGNFqIItryacvVJ8IxUcxUcDyn7drvPxLl1C6621KiOVcHyC/VL7C+sI4FaKOEAAHAxTiQFTQIFOCER3LoCI1CjyYYdCaI6Az00iBY2Q+lwmpGJW/VHvFBOhVBIxcGFDfDQLwV0vAgWgAysoc+vONYfZyKDmIrDKHmgQbkJkjgwWjNUVvwyg3eq+EHJNewSFEAI50vy7o7cIhtyQVXRlpuQpC8Ddbz3rd96F1LNhYHj5Rmmn4A9WGMaVmXIVfXrFXv0H3a5vLgVHvuvo4z/3uf0f0Sv1CqyoxhnMt+Xcf6VrMjwbolOu7vVffKDmW5quf92VK2sVLJmj1lSHk7XcvaNlpY+fb29vYPAAAA//8DAGVfs2P0BAAA
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUTW/jNhCG/4rAs6iQFEV93FKnixrZbIyNuy16G5Ejh4hMGhSVbRD4vy8oO4izix56E0Z85+OZl3wlDvZIOnJ9OPz+jC5OJCfRLiHBhKJMUca3rOl43UlRqFJWrOH/kJzYW3whHaml1ly3SEtUSKVhFe3LQVNjsOWMGyYqmXLCbiLdKwFbGHy2Ggs/fcMwWe9IR/6yzvjvqTbYQo9+NkXwI67dFMHp1MyX9eqWfr7ebO83p1MTTklcWEM6gkbxoWeM1hoNlU0PFExlqIKyVYPomar7k8y6iMHBWEzm6b2+8dFh1J0oRFkwKlpyzImBCKnlHibcvhxSFwuimxTPl/Dy2b2SZwykE/kbzFO6Kz3aq/3Uz3Y0V6Pf7azbrbwb7G4OEFPhnByCP2CIFhc4v1kH4eWz3+1SPvIJxglJTrYY9tbBePpxgwPMY3zwc1jITObpv86QjsAcPcnJQnIcl7LZeZjVaNHFX7QXhe8fss0IcfBh/2FJDyf26/+B/g70o3WYrW9IRxhHYcoG6SC4orIxQKEqW6pUqQbkuq0FJzlZeRetm/08ZWsXcXem9qG/66AfbUQd55Bm+lsls22CN7OO2fuCOStYUTKWRHM8zDH7isYG1BHTENswn/P9JBFKLKJPdsRf8KzmENDFbAPxMfsDpkfSkRYazaGFFkBpPWDFcGCGgWK1QWUUGilMbWQrjZIt00pCz6WBivdDipCcYDJZtvi6VVojlDWtS5HgNoL2VWOo4IzLpi4la4YPcLP7MelAN0yiKWmLg6Gy5UBbWZbUlBXUQg11LYYLTmdDcJaoX5rwzwnN2UtfTtZ+m93g8+keL30qY5pkBCokr6hsW06btqppXzd926sGK50mW3k3+TeQ3zD0frIxPSJ31tk9jD8fed/MLQaH48V27qwOfvJDzM62zD7s6+vs0iuWLRb9bh39dzHGjddPGLJkLLDuYpnH4/H4AwAA//8DAPdjXrgMBQAA
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACoxUwW7bOBD9FYFnUSElihIF7MFwWqyRpjESd3exN4ocxkRk0aAop0GRf19Qkmun6GFvwogz8+a9N/MD9fIAqEGr4/HTCfowoBQFO4VyknNMOCZ0R+qGVg0rMkIKQqv6X5QiewdvqEEVU4oqAbgADphpUuK2MAprDYISqkleslhTPg+o+YGkzTScrILMDX+BH6zrUYP+tr12r7G3tJnq3Kgz7zrY9EOQvYpgvm7Wd/jLart72M6vBhhicmY1ahBoTk1LCK4UaMzqVmKpS425LAQ3eUt41c5ptg/ge9llg3659Ncu9BBUk2d5kRGcC/SeIi2DjJBbOcDu7RhRTBTdxng6hafP5gc6gUdNnp7JnMvdqM7eHIZ2tJ2+CdI/QzBeHuDV+Rc4yQ6l6OjdEXywMHHz8JRcIFGSkSznOSEoRfdS7W0PyeY2eejiwFLVhIEusACjMRNUYsGKAuuilFXOTVXlBqXoDnwP3VXVe6u8G5wJyUJ58qHP2vXB9qMbh2TTB3j2Msx5n2U3AErRdmw7O+wfQeq3nXsc479+7OIs69F76EOylWGf/CmHPWqQkLWiUkghJVfKQEnAEE0kJ5UGrjlolutKM8E0Z4IozmRLmZYlbU2MoBQ9jOE4hjP/369A7Lw9HEBfEOwmjredDMb5w0ZDH6yxUZrzi8lQXTdNlSw1152FPnwgGTWIUMh1UQM2OeWY1VpiWRYCc15wA1SJKqczYcY+jz95IsZoVpiyrnkrWkogN1XNSGVaopUplNBlC1VekaqGWihC2pLRuhWKaKAl03E67/Sowq9WKCaBHsc+bmeyiWO/2h5/5+zCyNa74JTromC2g6e3IcDhd6qt3eHoBhvi/GaR9tsA966Pf5f9XzhbklcuXGIQ9yCZVq+VUAsiWqyKimHGCo5bCQTzStVEVXmeG4VSdOvUC/gkGkzaftLk7KmHp2Tl1d4GUGH0sfM/01CznJ/PK3PhI/v6abd2HlbHY7pE/zhFkuZiZwN8uCvfhvh874Y4RfDjlY+eoDNnXFdu0nCaL5WNQa51Ha8MzhktMROC4lqUFW6ruhUtr6FUF7cmj6CtBxWmeru520Ls72y58sEaqcIQd+eLU5ObFnsuT34FeaXaz+y5+ZVyi5OWSpRExz7Nd3My0P86m+/v7+//AQAA//8DACUyB6cnBgAA
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTTW/cNhD9KwLPS5mkKFLUzVin6MJNvUgWbdHbiDOKCcvUgqKcBIb/e0HtFk5664UgBnwz72P4yiI8E+vZ7fn84YViXtiO5bCVlFCGC8OFPImul7bXtm6dUMq6v9mOhXv6znpmtffSO+INGeIaRcuHZvQckZwUEoVqdekJXxbWvzIINdJL8FTPyx+UljBH1rM/Q8T5a5kNofbTvGKd5okOcckQfSHz+2F/z3+7PZ4ejpdXCy0FXAdkPSM0chyE4NYTct0NwAFb5AYaZ0Y1CGOHCyzETCnCVC/49D4f5xwp+17VqqkFV4697RhChkJ5gIVO38+FxWbRXanvtvJ27V/ZCyXWq92/Zl7a3fgp3DwvwxomvLmcGZanovKc5jOlHGjzZJM5TZDDHKvrpP0UKGa2Yw+fq+MEeZzT809Gfb7oP/wP+acy/cM38msm3M9rzKxnUku2Yx/BP4ZI1eGO9UxIUth0xEclDdcdAoe2cdyYxowkvbOqYPZzzCGu87pUh5jpS9oEsJ79AtNCF+q3yT+GTD6vqaj6y5RdOKYZV5+rd/+lqEXdCFFAaz6vufpEGBL5TEXfKa3Xfv+BKKM20H5NiWKujpAfq19heWQ9c9B5CQ4cgPF+pFbQKFCAERbJoCHUCi1qp9FoJ7zRMEiN0MphLBW2Y1TirrYNE9ZJbTvLSUnHNYLlTg3ArTWeCIVzWIi8+1g9TAUHvhOasOGORuTaSeBONw3HpgWrzGitGn+w5Jq+FMVgpJfLX9kYGMSuBM2Vli3XzkneudbywXaDG0xHrS+c7ylFmn4w6mPwaV7mMVfX5al+su7TGst/r7ZF+hoi/7ZldDf7J0pVyRhCLOt9jfXt7e3tHwAAAP//AwDJ0IwBNgQAAA==
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTwW7bOBD9FYFnUSEpihJ1C5xd1Ei7MdqgXextxBnVRBXKoKi0ReB/X1B20aa33ohHvpk37w1fWIAnYj27PZ3+eqaQFlay5DdICWW4MFzIR9H1su11WzVWdVqJ/1jJ/D19Zz1rtXPSWeI1GeIaRcOHenQckawUEoVqdK4JnxfWvzDwFdKzd1TNy0eKi58D69knH3D+mnuDr9w0r1jFeaJ9WBIEl8X8s9/d87e3h8eHw+XVQksmVx5ZzwiNHAcheOsIue4G4IANcgO1NaMahGmHC82HRDHAVC345Wd/nFOg5HpVqboSXFl2LhlCgix5gIUev5+yis2iu4yXG7wd+xf2TJH1qvxh5qXcjZv8jZufniDgzeiDX46sZKc4nygmT5sd24TTBMnPobg22U2eQmIle/hQHCZI4xyfXnn04TL6/g8mfwfu6AMV+zvWMyFJYd0RH5U0XHcIHJracmNqM5J0tlWSlWw3h+TDOq9LsQ+JPsdNJevZ3zAtdNF3G93RJ3JpjVn6vyZnTd982s2YAcFKdogzri4VP+2WohJVLfLlw5pOayreE/pILlGe6TGu1/K/UZRRG2m3xkghFQdIx+INLEfWMwudk2DBAhjnRmoEjQIFGNEiGTSEWmGL2mo02gpnNAxSIzRyGDOSded0i22hwOihHYThjRuA604PHFqpuTbWNZ3WtZXqla3Fw7TxXCc0Yc0tjci1lcCtrmuOdQOtMmPbqvEXS66JS5H9Rnq+fI1NgUHscrhcadlwba3knW1aPrTdYAfTUeOy5nuKgaZfjHrnXZyXeUzFdWGKV9a9X0P+3sW2PF994N+2yO5m94VikSMHH/I2X1M+n8/n/wEAAP//AwA4KzhMJQQAAA==
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRUwW7cNhD9FYFnUSYlihJ1M9YJunBTL5JFGuQ24oxiwjK1ICknhuF/L6jdwvGp6IUgRnzDN+896oV5eCQ2sOvT6cMT+RRZyZLbSrWoNReaC3kU/SC7QXVV23ddI8V3VjJ3S89sYJ2yVlpDvCFNXKFo+dhMliOSkUKiqFuVe8KPyIYXBq5CenKWqiV+pRDd4tnA/nYel5/5bnCVnZcVq7DMtPcxgbeZzF/73S3/8/pwvDucT0WKGVw5ZAMj1HIaheCdJeSqH4EDtsg1NEZP9Sh0N55hzicKHuYq4sPb/bgkT8kOdVU3leC1Ya8lQ0iQKY8Q6fh8yiw2iW5yvdzK23Z4YU8U2FCX/4p5bndlZ3f1GMfVzXh1XhPEh7itE9i0hGdWslNYThSSo02f6xjpcZyfjxAfPp7P5G388Ivsmgh3y+oTG5hselayTaB5huQWX1w47mZHPm0fU3A+OvtfvRpWsrsvxWGGNC3h8Z0fX84y7/+Hyp/A3jtPxf6GDUxIqrHpiU+11Fz1CBzaxnCtGz2RtKarJSvZbvHJ+XVZY7H3iX6EbSQ2sI8wRzrzuw723iWyaQ15zm865+oQFlxtKt68lKISVSNEBq3ptKbiM6ELZBPlIY5hvfT7+h5S63oD7dYQyKfiAOm++APiPRuYgd5KMGAAtLUTtYImgQK06JA0akJVY4fKKNTKCKsVjFIhtHKccoWVjHJ0ii2tPUHb2ElzM0rJVYcNH7EGjgCd0FMPasyjvelY3M0ZB7YXirDhhibkykjgRjUNx6aFrtZT19XTb5Jc8iBFFhjp6fzuNgYasc9u8lrJlitjJO9N2/Gx60cz6p5amznfUvA0/ybUJ2fDEpcpFZeEFO+k+7z6/O8otrT8dJ7/2jy6WewDhSJ7DM7np3Kx9fX19fUfAAAA//8DALXdmSSCBAAA
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS/zQzbBb1HVTL+JFWvQ2JEe2EK20oKhNA2P/e0DtBol7600YaGZePvPOvJEJDkg6cns8/nLCKS+kJnnYQoIJTZmmjO+Z7bjtmGu4MIIr8Q+pyfCIX0lHjAqBB4dUokaqImupl32gMaLjjEcmWlVqwstCujcCQxPxNARs5uUTpmWYJ9KRv4Ypzl9KbxiaMM5rbNI84sO0ZJhCEfPnw90j/eN2t3/aXf5acCnJzRCLVO0db7mngclNRE8dj0CjaNtoZKtAi0vaMGVME4zNEj//6B/nPGEOnWiEbBgVjpxrEiFDkexhwf3XY1GxIbov8XoLb5/dGzlhIp2ov8O8lLsJ43CT5+OIJxyPkBZMN2E+HGCKpCbHNB8x5QE3LNtLxxHyME/VtdndOOCUSU2enqvdCLmf0+Edq+cLgof/QeCEyZOOSNZKbQwqKSU47QXDaHTQWvgAzPReIA/IWqeM44Zx6QRzstXGmgjMBVKTl3H2MP6+bPycYN5GZoQWqNAJpbULDKXFvjXIImjttQ0uSmuEstFKcADceTTB6VAM8gHC6zBh9XBPOsI4iigt0l5wTZWNQKGVjmotdY88OCM4qcndPOVhWud1qR6mjC9p40c68iuMC17I3abwOmQMeU0F6t+6NNulOa4hVz8cwFnDGslYSVrzcc3VR4xDwpCx4N2n9VrvPylCiy3pbk0Jp1ztIL9Wv8HyWqiADRxceaoOoceWYc8iA81MRB01RiWiicqpqJVjQSvwXEVoue9LhNQEi+GqzeMWTBDSSSrAOKo0s9QLj9RLq4D3HgK851g9jSUPgmUKo6QO+0iV40CdkpJG2YIRujdG9D8huZqPswI44umyrZsCHaPFqDkVirdUOcepda2h3ljvvLbYhqL5EdOE40+gPgwhzcvc5+rq3eoduo/rVC5Otfn4yzDRf7cZ3c/hM6aqzBiGqSzYdaznmhwQljXhYTtY3RvZle2q9tvdErwxzLqaPGdIeT1ew1I0znB1Pp/P3wAAAP//AwCSsevS9QQAAA==
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACoxUTW/bOBD9KwLPokJS1AcF7MFwWqyRpjESd3exN4ocxkRk0aAop0GR/76gJNdO0cPehNF8vHnvDX+gXh4ANWh1PH46QR8GlKJgpxAjrMSkxITuSN3QuiEiK6goRFX/i1Jk7+ANNajiSlElAOdQAuaaFLjNjcJag6CEasIKHnvK5wE1P5C0mYaTVZC54S/wg3U9atDfttfuNc6WNlOdG3XmXQebfgiyVxHM1836Dn9ZbXcP2zlrgCEWZ1ZHqGUraEFbrEg+gTBYUC2xZkWhq7zgsmRzme0D+F522aBfLvO1Cz0E1bCM5RnBTKD3FGkZZITcygF2b8eIYqLoNsbTKTx9Nj/QCTxqWHomc253ozp7cxja0Xb6Jkj/DMF4eYBX51/gJDuUoqN3R/DBwsTNw1NygURJRjJWMkJQiu6l2tseks1t8tDFhaWqCQedYwFGYy6oxILnOdZ5IStWmqpiBqXoDnwP3VXXe6u8G5wJyUJ58mHO2vXB9qMbh2TTB3j2Msx1n2U3AErRdmw7O+wfQeq3nXsc479+7OIu69F76EOylWGf/CmHPWqQkLWiUkghZamUgYKAIZrIklQaSl2C5kxXmguuSy6IKrlsKdeyoK2JEZSihzEcx3Dm//sViJ23hwPoC4LdxPG2k8E4f9ho6IM1NkpzzpgM1XXTVsnSc91Z6MMHklGDCAWm8xqwYbTEvNYSyyIXuCzz0gBVomJ0JszY5/EnT8QYzXNT1HXZipYSYKaqOalMS7QyuRK6aKFiFalqqIUipC04rVuhiAZacB23806PKvxqhXwS6HHs43Umm7j2q+3x95JfGNl6F5xyXRTMdvD0NgQ4/E61tTsc3WBD3N8s0n4b4N718e9y/wtnS/HKhUsM4h0k0+mpvFVVWzGsas4xb3OBhWoZrkUlWKFl3kK8llunXsAn0WDS9pMmZ089PCUrr/Y2gAqjj5P/mZaa5fx8PpkLH9nXT7u187A6HtMl+scpkjQ3Oxvgw7vybYjpezfELYIfr3z0BJ0547pyk4bT/FLZGCy1rkGXFDNOC8yFoLgWRYXbqm5FW9ZQqItbk0fQ1oMKU7/dPG0h9ne2XPlgjVRhiLfzxanJTYs9l5RfQV6p9rN6Hn6l3OKkpRMl0bFP87s5Geh/PZvv7+/v/wEAAP//AwB74N4kJwYAAA==
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRUwW7jNhD9FYFnUSEpiRJ1S50uamSzMTbutuiN4owcIjJpUFS2QeB/X1B2EGcXPfQmjPhm3rz3yFfi9B5JR64Ph9+f0cWJ5CTapSSYkJRJyviWtR1vO6YK3taKSf4PyYm9xRfSkaYyhhuFtESJtAJW074cDAVAxRkHJuoq9dS7iXSvRNsC8NkaLPz0DcNkvSMd+cs68N/TbG0LM/oZiuBHXLspamcSmS/r1S39fL3Z3m9OpyacEriwkKjKXvGa99SwciExUMVBUxB1DU1ZV1qKE8y6iMHpsZjg6X0++Ogwmk4UoiwYFYoccwI66kS51xNuXw6JxSLRTarnS3n57F7JMwbSifxNzFO7KzPaq/3Uz3aEq9HvdtbtVt4NdjcHHdPgnByCP2CIFhdxfrNOh5fPfrdL/cgnPU5IcrLFsLdOj6cfNzjoeYwPfg6LMhM8/dcZ0hE9R09ysig5jsvY7LzMarTo4i/Yi8H3D9lm1HHwYf/BpIeT9uv/If2dNo/WYba+IR1hHAWULdJBcEmrFjTVdamolKUckBvVCE5ysvIuWjf7ecrWLuLurNoHftfBPNqIJs4h7fS3TGHbBA+zidm7wZwVrCgZS6A5HuaYfUWwAU3EtMQ2zOd+P0GEFAvokx3xF3lWcwjoYrbR8TH7Q0+PpCNKt4ZrpZXW0pgBa4YDA6YlawAlSIRKQAOVqkBWihlZ6Z5XoGveD6lCcoIpZNmS6x5kDQicctn0tBJoaN8C0KrhopRSaibhg7jZ/Zhw2rSsQiipwgFopbimqipLCmWtGyGHphHDhU7nQHCWVL8M4Z8TwjlLX07Rftsd8Pl0jxeeEqBFkJyKite0UorTVtUN7Zu2V71ssTZps5V3k38T8huG3k82pkfkzjq71+PPR96ducXgcLxw586a4Cc/xOwcy+yDX19nl16xbInod+vov0swbrx5wpClYGnrLsw8Ho/HHwAAAP//AwAfRIPiDAUAAA==
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUT2/cNhDFv4rAsyjzP0XdDLtFDTfNIjbaorcRZxQL0UoLito0MPzdC2q3SNxbb8JAb/j4mzd8ZTMciXXs9nT66UxzXlnN8riXlFCOC8eFfBZtJ9tOhEY65Vtl/2I1Gx/pG+uYNzHKGIhrcsQNCst7PUSOSEEKiUJZU3rC55V1rwzGBuk8RmqW9XdK67jMrGN/jDMuX8vZMDZxWjZs0jLRw7xmmGMx89vD3SP/9fbw/PFw+WultYibEYtV1wdpZc+j0LuJgQeJwFFZi15bA05dZOOcKc0wNSt++X4+LnmmHDvVKN0IrgJ7qxlChmK5h5Wev52Kix3RfanXe3n/7F7ZmRLrVP0vzEu7mziNN+vWT3Sm6QRppXQTl+MRZmQ1O6XlRCmPtGM5U+qXdcyFaIwe2lZKbMEb67HVVmmPWhC2wlsMGMAZN6ho7YB9G7wZDCpp0JExvWc128FNE+Rxmaur97tppDmzmn18qg4T5GFJx3fony5EH/4H0GKbdUwLq533ZLTWEFyvBKF30TnVRxB+6BXJSMIG44P0QuqgRNDW+dYjiBBZzT5AfBlnqh7uWceEJIW6JT4o6bhpEThYHbhz2g0kY/BKsprdLXMe523Z1uphzvQ57fdlHfsZppUuN71N8WXMFPOWCoQ/XQnjIS24xVx9D4AUjWi0EEW05dOWq0+EY6KYqeB4Ttu1338kyqlddLelRHOuDpBfql9gfWEdC9BGCQECgItxICtoECjACY/k0BEahR5NMOhMENEZ6KVBsLIfSoXVjEreqj3iAFZI0Ja3WgE3PiIPLbW8d857EBCGIN5xrD5Ouy62whBqHmhAboIEHozWHLUFr9zgvRp+QHINixQFMNL5sqy7A4fYEjrJlZGWmxAkb4P1vPdtH3rXko3F8yOlmaYfQH0YY1rWZcjVNWvVO3Sftrk8ONWeu6/jzP/eZ3S/xC+UqjJjGOeyX9exvtXsSLBuiY77e9W9skNZrup5f7aUbLxoQ82eMqS8na5lrZrgpXl7e3v7BwAA//8DAEKX/830BAAA
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRUTW/cNhT8KwLPoswvUaJuxjpBF27qRbJIi94ovqeYsEwtSMqJYex/L6jdIvGp6IUgnjiP82aGeiPBPiMZyO3p9OEFQ06kJtlvJcGEpkxTxo+sH3g/cNVwKbTu+d+kJv4eX8lAOuUcdwapRI1UAWvpKCdHAdBwxoGJVpWe9lsiwxuxvgF88Q6bJX3FmPwSyED+9AGW7+Vu6xs3Lys0cZlxH1K2wRUyf+x39/T328Px4XA5lTAVcOOhUNWj4S0fqWNyIzFRw8FSEG0LnWyV1eIC8yFjDHZuEjz9vB+WHDC7QTRCNowKQ841AZttoTzahMfXU2GxSXRX6vVW3rbDG3nBSAZR/yvmpd2Nm/3NcxpXP8PNZc02PaVtnazLS3wlNTnF5YQxe9z0uU0Jn8f59WjT08fLmbJNH36gWzPCbllDJgPhsic12QSaZ5v9Eqorx93sMeTtY44+JO/+q5ckNXn4Uh1mm6clPr/z48tF5v3/UPmTdY8+YLW/IwNhHAXIHukkuKaqB0ttKw3VWuoJuTOd4KQmuyVkH9ZlTdU+ZPwWt5HIQD7aOeGF3210jz6jy2ssc/6lS64OcYHV5eqnl5w1rJGMFdCaT2uuPiP4iC5jGeIY12u/r+8hQosNtFtjxJCrg82P1W82PZKBGNs7bo011mrnJmwZTgyY1awD1KARlIAOlFGglWFOKztyBbbl41QqpCZYolNtaUXrWsU6R+XYS6qMsrR3XNBOShBGtcClfqdj9TAXnHU9UwiSGpyAKsMtNUpKCrK1ndBT14npF0mueeCsCAz4cnl3GwMN0CNoToXiLVXGcNqbtqNj149m1D22rnC+xxhw/kWoT97FJS1Trq4Jqd5J93kN5d9RbWn57gP9sXl0t7gnjFXx2PpQnsrV1vP5fP4HAAD//wMAGpaA+4IEAAA=
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACoxUTW/cNhD9KwKvXcr8Einq5toOunAdG9lFW/Q2Ikc2a5laUJSTwPB/L6i1EztBgV4EYUjOe/PmzTyRCA9IOnJ6OFw8Yswz2ZAc1pBgQlOmKeN71na87biquWwbpfTfZEPCJX4lHTHKOe4sUokaqfKsob0cHPUeLWfcM9GokhNuZ9I9EQi1x8fgsJ7mPzDNYYqkI3+G6KfPBRtC7cZp8XWaRtzGOUN0hczH7dkl/f30Zn99c7w141we18EXqrq3vOE9dUyuJAZquQfqRdN4IxsFWhyfhZgxRRjr2d9/x/dTjphdJ2oha0aFJc8b4iFDodzDjPuvh8Jilei8xDdreP3tnsgjJtKJzauYx3QnbgwnD3O/hNGfrF+yIYc0HTDlgKsa17vqOwnOalYLLRgjG3IF7i5ErLbn1fVYSgTXMoVeUouDp8pyoFZJSb1swAg9GCMGsiGXmCKOb7JeBZemeRpy9SJy9Q5nd3p2EaEfsWB8gHFGsiHbeZchehiniBdf0C35mGuflnJ8NsUc4jItc7WNGW8TvJy/vj9bUsKYqxvId9VvMN+RjlhoHQcLFkA7N2DDcGCegWbGo/YavRLeeGWV18oypxX0XHloeD+UCNmQX4uGu8U5nOfvbFaTjOPKoXrp09kYMOZ3MpKOMI7CyxbpILimqvVAoZGWai31gNxZI/grzB7SLWbSkaHt+5451wtluQNhQfqBSa8HK4QxoAbZGCW0bpgB540RXHlp0IEcJGuL7W7S5BeXf2y1XBtwk6Z/0OUiFemIZk3TOuh7oRVvwbaouFattmisGoAp6XrgTQt9zxk0zNlWOODagup9M3xT6SLehojnYT6M8PUNblvrWv7iAFo+gPrh+ocED/h5SvcfjzauP17syYZ8WmLZCNW2WORziPSLLhN9tYw57O8Sgkd/NXn82UdYxqVaJ9QraZVhjlpjkCrrOO0lADUWemNK0UMZqvPJ3WOqisMgxDJV35Jd76rT5O5CRpeXVPj9tfK43lU3I+RhSg/vNsmbwn4UoBaSs9IZj4/HbbRy1N636DWnQvGGKms5bW1jaG/a3va6xcYVxa6XfFhy9Ql9SOjyWvGLF1fMszt09z+L8eqCF4tyVry2O26xVdr/tcSeN+QBYV4SPqzbunsi2xgxrcjny3EUt/EqjGOY0U3Rz6RTVrNaq0a+MPyvew1jpm6sFM/Pz8//AgAA//8DAJAvYc0bBgAA
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTyW7bMBD9FYFnU+EmStQtcFrUSNsYidEWvVGcUU1EoQyKyoLA/15QdtCmt14IYsA385bhKwn2AUlLLg+HD48Y0kRWJPmlJJjQlGnK+I41LW9arkouK6mb6idZEX+NL6QltXKOO4NUokaqgFW0k72jAGg448BEpXJP+2si7SuxvgR89A7LcfqGcfJjIC357gOMT3m29aUbxhnKOA64CVOywWUyXzfra/r5cru72Z5eTThlcOkhU9Wd4RXvqGNyIdFTw8FSEFUFtayU1eIE8yFhDHYoJ7j/Mx/GFDC5VpRClowKQ44rAjbZTLmzE+5eDpnFYtFVrq+W8nJtX8kjRtKK1ZuZp3YXbvAXD1M3+wEuTmey031WeYjjAWPyuHiyyBwGm/wYivOk9eAxJLIiN3fFdrCpH+PDO6PuTvo3/yF/l6d/eEY3J4T1OIdEWsIVJyvyxbq9D1hsrkhLGEcBskHaC66pasBSW0lDtZa6R+5MLTJmPYbkwzzOU7EJCX/FRQBpyUc7THiifhnd3id0aY5Z1Q+dd2EbR5hdKv74z1nJSslYBs3pMKfiFsFHdAmzvl2cz/3+gQgtFtB6jhFDKrY27YtPdtqTlhjbOG6NNdZq53qsGPYMmNWsBtSgEZSAGpRRoJVhTivbcQW24l2fK2RFMMddLBumK5CubhzVtZNUMe5o5zpHXd9YxVhTC96/87G4GTLOuoYpBEkN9kCV4ZYaJSUFWdla6L6uRca9WXJOn7NsMODj6a+cGAA0CJpToXhFlTGcNqaqaVc3nel0g5XLnK8xBhz+MuqLd3Gcxj4V5+Up3ll3O4f834tlkZ58oM9LRleju8dY5IytD3m9z7Eej8fjbwAAAP//AwBleZTlNgQAAA==
|
||||
@ -1,13 +1,50 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using PlotLine.Models;
|
||||
using PlotLine.Services;
|
||||
using PlotLine.ViewModels;
|
||||
|
||||
namespace PlotLine.Controllers;
|
||||
|
||||
public sealed class TimelineController(ITimelineService timeline) : Controller
|
||||
{
|
||||
public async Task<IActionResult> Index(int projectId, int? bookId, int? selectedSceneId)
|
||||
public async Task<IActionResult> Index([FromQuery] TimelineFilterViewModel filter)
|
||||
{
|
||||
var model = await timeline.GetAsync(projectId, bookId, selectedSceneId);
|
||||
if (filter.ProjectID == 0)
|
||||
{
|
||||
return BadRequest("ProjectID is required.");
|
||||
}
|
||||
|
||||
var model = await timeline.GetAsync(filter);
|
||||
return model is null ? NotFound() : View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> SavePreset(TimelinePresetSaveViewModel model)
|
||||
{
|
||||
await timeline.SavePresetAsync(model);
|
||||
return RedirectToAction(nameof(Index), new { projectId = model.ProjectID, bookId = model.BookID });
|
||||
}
|
||||
|
||||
public async Task<IActionResult> LoadPreset(int id)
|
||||
{
|
||||
var preset = await timeline.GetPresetAsync(id);
|
||||
if (preset is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var filter = System.Text.Json.JsonSerializer.Deserialize<TimelineFilterViewModel>(preset.FilterJson) ?? new TimelineFilterViewModel();
|
||||
filter.ProjectID = preset.ProjectID;
|
||||
filter.BookID = preset.BookID;
|
||||
filter.FocusType = preset.FocusType;
|
||||
filter.FocusID = preset.FocusID;
|
||||
return RedirectToAction(nameof(Index), filter);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> DeletePreset(int id, int projectId, int? bookId)
|
||||
{
|
||||
await timeline.ArchivePresetAsync(id);
|
||||
return RedirectToAction(nameof(Index), new { projectId, bookId });
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,6 +53,14 @@ public interface ITimelineRepository
|
||||
Task<TimelineData> GetByProjectAsync(int projectId, int? bookId);
|
||||
}
|
||||
|
||||
public interface ITimelinePresetRepository
|
||||
{
|
||||
Task<IReadOnlyList<TimelineViewPreset>> ListAsync(int projectId, int? bookId);
|
||||
Task<TimelineViewPreset?> GetAsync(int timelineViewPresetId);
|
||||
Task<int> SaveAsync(TimelineViewPreset preset);
|
||||
Task ArchiveAsync(int timelineViewPresetId);
|
||||
}
|
||||
|
||||
public interface IPlotRepository
|
||||
{
|
||||
Task<PlotLookupData> GetLookupsAsync();
|
||||
@ -803,6 +811,56 @@ public sealed class TimelineRepository(ISqlConnectionFactory connectionFactory)
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TimelinePresetRepository(ISqlConnectionFactory connectionFactory) : ITimelinePresetRepository
|
||||
{
|
||||
public async Task<IReadOnlyList<TimelineViewPreset>> ListAsync(int projectId, int? bookId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
var rows = await connection.QueryAsync<TimelineViewPreset>(
|
||||
"dbo.TimelineViewPreset_List",
|
||||
new { ProjectID = projectId, BookID = bookId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
return rows.ToList();
|
||||
}
|
||||
|
||||
public async Task<TimelineViewPreset?> GetAsync(int timelineViewPresetId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<TimelineViewPreset>(
|
||||
"dbo.TimelineViewPreset_Get",
|
||||
new { TimelineViewPresetID = timelineViewPresetId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<int> SaveAsync(TimelineViewPreset preset)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<int>(
|
||||
"dbo.TimelineViewPreset_Save",
|
||||
new
|
||||
{
|
||||
preset.TimelineViewPresetID,
|
||||
preset.ProjectID,
|
||||
preset.BookID,
|
||||
preset.PresetName,
|
||||
preset.FilterJson,
|
||||
preset.VisibilityJson,
|
||||
preset.FocusType,
|
||||
preset.FocusID
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task ArchiveAsync(int timelineViewPresetId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
await connection.ExecuteAsync(
|
||||
"dbo.TimelineViewPreset_Archive",
|
||||
new { TimelineViewPresetID = timelineViewPresetId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PlotRepository(ISqlConnectionFactory connectionFactory) : IPlotRepository
|
||||
{
|
||||
public async Task<PlotLookupData> GetLookupsAsync()
|
||||
|
||||
@ -168,6 +168,21 @@ public sealed class TimelineMetricPoint
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public sealed class TimelineViewPreset
|
||||
{
|
||||
public int TimelineViewPresetID { get; set; }
|
||||
public int ProjectID { get; set; }
|
||||
public int? BookID { get; set; }
|
||||
public string PresetName { get; set; } = string.Empty;
|
||||
public string FilterJson { get; set; } = "{}";
|
||||
public string VisibilityJson { get; set; } = "{}";
|
||||
public string? FocusType { get; set; }
|
||||
public int? FocusID { get; set; }
|
||||
public DateTime CreatedDate { get; set; }
|
||||
public DateTime UpdatedDate { get; set; }
|
||||
public bool IsArchived { get; set; }
|
||||
}
|
||||
|
||||
public sealed class PlotLineType
|
||||
{
|
||||
public int PlotLineTypeID { get; set; }
|
||||
|
||||
@ -24,6 +24,7 @@ public class Program
|
||||
builder.Services.AddScoped<ISceneRepository, SceneRepository>();
|
||||
builder.Services.AddScoped<ILookupRepository, LookupRepository>();
|
||||
builder.Services.AddScoped<ITimelineRepository, TimelineRepository>();
|
||||
builder.Services.AddScoped<ITimelinePresetRepository, TimelinePresetRepository>();
|
||||
builder.Services.AddScoped<IPlotRepository, PlotRepository>();
|
||||
builder.Services.AddScoped<IAssetRepository, AssetRepository>();
|
||||
builder.Services.AddScoped<ICharacterRepository, CharacterRepository>();
|
||||
|
||||
@ -127,7 +127,10 @@ public interface ICharacterService
|
||||
|
||||
public interface ITimelineService
|
||||
{
|
||||
Task<TimelineViewModel?> GetAsync(int projectId, int? bookId, int? selectedSceneId);
|
||||
Task<TimelineViewModel?> GetAsync(TimelineFilterViewModel filter);
|
||||
Task<int> SavePresetAsync(TimelinePresetSaveViewModel model);
|
||||
Task<TimelineViewPreset?> GetPresetAsync(int timelineViewPresetId);
|
||||
Task ArchivePresetAsync(int timelineViewPresetId);
|
||||
}
|
||||
|
||||
public interface IAnalyticsService
|
||||
@ -738,12 +741,26 @@ public sealed class SceneService(
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TimelineService(ITimelineRepository timelineRepository, IBookRepository books, ISceneService scenes, IAssetRepository assets, ICharacterRepository characters, IWarningRepository warnings, ISceneDependencyRepository sceneDependencies) : ITimelineService
|
||||
public sealed class TimelineService(
|
||||
ITimelineRepository timelineRepository,
|
||||
ITimelinePresetRepository presets,
|
||||
IBookRepository books,
|
||||
ISceneService scenes,
|
||||
IAssetRepository assets,
|
||||
ICharacterRepository characters,
|
||||
ILocationRepository locations,
|
||||
IPlotRepository plots,
|
||||
ILookupRepository lookups,
|
||||
IWarningRepository warnings,
|
||||
ISceneDependencyRepository sceneDependencies) : ITimelineService
|
||||
{
|
||||
private static readonly string[] DefaultGraphMetrics = ["Overall Intensity", "Tension", "Emotional Weight", "Action"];
|
||||
|
||||
public async Task<TimelineViewModel?> GetAsync(int projectId, int? bookId, int? selectedSceneId)
|
||||
public async Task<TimelineViewModel?> GetAsync(TimelineFilterViewModel filter)
|
||||
{
|
||||
var projectId = filter.ProjectID;
|
||||
var bookId = filter.BookID;
|
||||
var selectedSceneId = filter.SelectedSceneID;
|
||||
var timeline = await timelineRepository.GetByProjectAsync(projectId, bookId);
|
||||
if (timeline.Project is null)
|
||||
{
|
||||
@ -751,6 +768,13 @@ public sealed class TimelineService(ITimelineRepository timelineRepository, IBoo
|
||||
}
|
||||
|
||||
var allBooks = await books.ListByProjectAsync(projectId);
|
||||
var lookupData = await lookups.GetAllAsync();
|
||||
var plotLines = await plots.ListPlotLinesAsync(projectId);
|
||||
var plotThreads = await plots.ListPlotThreadsByProjectAsync(projectId);
|
||||
var allAssets = await assets.ListAssetsAsync(projectId);
|
||||
var allCharacters = await characters.ListCharactersAsync(projectId);
|
||||
var allLocations = await locations.ListByProjectAsync(projectId);
|
||||
var warningLookups = await warnings.GetLookupsAsync();
|
||||
var chaptersByBook = timeline.Chapters.GroupBy(x => x.BookID).ToDictionary(x => x.Key, x => x.ToList());
|
||||
var scenesByChapter = timeline.Scenes.GroupBy(x => x.ChapterID).ToDictionary(x => x.Key, x => x.ToList());
|
||||
var purposeLabelsByScene = timeline.PurposeLabels.GroupBy(x => x.SceneID).ToDictionary(x => x.Key, x => x.OrderBy(p => p.SortOrder).ToList());
|
||||
@ -767,6 +791,9 @@ public sealed class TimelineService(ITimelineRepository timelineRepository, IBoo
|
||||
|
||||
var assetTimeline = await assets.GetTimelineAsync(projectId, bookId);
|
||||
var characterTimeline = await characters.GetTimelineAsync(projectId, bookId);
|
||||
var matchingSceneIds = await BuildMatchingSceneIdsAsync(filter, orderedScenes, timeline.ThreadEvents, assetTimeline.Events, characterTimeline.Appearances, projectId, bookId);
|
||||
var focusSceneIds = await BuildFocusSceneIdsAsync(filter, orderedScenes, timeline.ThreadEvents, assetTimeline.Events, characterTimeline.Appearances, projectId, bookId);
|
||||
var focusLabel = BuildFocusLabel(filter, plotLines, plotThreads, allAssets, allCharacters, allLocations);
|
||||
var selectedScene = selectedSceneId.HasValue ? await scenes.GetEditAsync(selectedSceneId.Value) : null;
|
||||
if (selectedScene is not null)
|
||||
{
|
||||
@ -787,7 +814,25 @@ public sealed class TimelineService(ITimelineRepository timelineRepository, IBoo
|
||||
SelectedBookID = bookId,
|
||||
SelectedSceneID = selectedScene?.SceneID,
|
||||
SelectedScene = selectedScene,
|
||||
Filter = filter,
|
||||
BookOptions = allBooks.Select(x => new SelectListItem($"Book {x.BookNumber}: {x.BookTitle}", x.BookID.ToString(), x.BookID == bookId)).ToList(),
|
||||
ChapterOptions = timeline.Chapters.Select(x => new SelectListItem($"Ch {x.ChapterNumber}: {x.ChapterTitle}", x.ChapterID.ToString())).ToList(),
|
||||
ChapterFromOptions = timeline.Chapters.Select(x => new SelectListItem($"Ch {x.ChapterNumber}: {x.ChapterTitle}", x.ChapterID.ToString(), x.ChapterID == filter.ChapterFromID)).ToList(),
|
||||
ChapterToOptions = timeline.Chapters.Select(x => new SelectListItem($"Ch {x.ChapterNumber}: {x.ChapterTitle}", x.ChapterID.ToString(), x.ChapterID == filter.ChapterToID)).ToList(),
|
||||
SceneOptions = orderedScenes.Select(x => new SelectListItem($"Scene {x.SceneNumber}: {x.SceneTitle}", x.SceneID.ToString())).ToList(),
|
||||
SceneFromOptions = orderedScenes.Select(x => new SelectListItem($"Scene {x.SceneNumber}: {x.SceneTitle}", x.SceneID.ToString(), x.SceneID == filter.SceneFromID)).ToList(),
|
||||
SceneToOptions = orderedScenes.Select(x => new SelectListItem($"Scene {x.SceneNumber}: {x.SceneTitle}", x.SceneID.ToString(), x.SceneID == filter.SceneToID)).ToList(),
|
||||
PlotLineOptions = plotLines.Select(x => new SelectListItem(x.PlotLineName, x.PlotLineID.ToString(), x.PlotLineID == filter.PlotLineID)).ToList(),
|
||||
PlotThreadOptions = plotThreads.Select(x => new SelectListItem(x.ThreadTitle, x.PlotThreadID.ToString(), x.PlotThreadID == filter.PlotThreadID)).ToList(),
|
||||
AssetOptions = allAssets.Select(x => new SelectListItem(x.AssetName, x.StoryAssetID.ToString(), x.StoryAssetID == filter.StoryAssetID)).ToList(),
|
||||
CharacterOptions = allCharacters.Select(x => new SelectListItem(x.CharacterName, x.CharacterID.ToString(), x.CharacterID == filter.CharacterID)).ToList(),
|
||||
PovCharacterOptions = allCharacters.Select(x => new SelectListItem(x.CharacterName, x.CharacterID.ToString(), x.CharacterID == filter.POVCharacterID)).ToList(),
|
||||
LocationOptions = allLocations.Select(x => new SelectListItem(x.LocationPath, x.LocationID.ToString(), x.LocationID == filter.LocationID)).ToList(),
|
||||
WarningSeverityOptions = warningLookups.Severities.Select(x => new SelectListItem(x.SeverityName, x.WarningSeverityID.ToString(), x.WarningSeverityID == filter.WarningSeverityID)).ToList(),
|
||||
WarningTypeOptions = warningLookups.WarningTypes.Select(x => new SelectListItem(x.TypeName, x.WarningTypeID.ToString(), x.WarningTypeID == filter.WarningTypeID)).ToList(),
|
||||
RevisionStatusOptions = lookupData.RevisionStatuses.Select(x => new SelectListItem(x.StatusName, x.RevisionStatusID.ToString(), x.RevisionStatusID == filter.RevisionStatusID)).ToList(),
|
||||
ScenePurposeOptions = lookupData.ScenePurposeTypes.Select(x => new SelectListItem(x.PurposeName, x.ScenePurposeTypeID.ToString(), x.ScenePurposeTypeID == filter.ScenePurposeTypeID)).ToList(),
|
||||
ViewPresets = await presets.ListAsync(projectId, bookId),
|
||||
Books = timeline.Books.Select(book => new BookTimelineViewModel
|
||||
{
|
||||
Book = book,
|
||||
@ -803,10 +848,137 @@ public sealed class TimelineService(ITimelineRepository timelineRepository, IBoo
|
||||
MetricGraphs = BuildMetricGraphs(timeline, orderedScenes),
|
||||
PlotLanes = BuildPlotLanes(timeline, orderedScenes),
|
||||
AssetLanes = BuildAssetLanes(assetTimeline.Assets, assetTimeline.Events, orderedScenes),
|
||||
CharacterLanes = BuildCharacterLanes(characterTimeline.Characters, characterTimeline.Appearances, orderedScenes)
|
||||
CharacterLanes = BuildCharacterLanes(characterTimeline.Characters, characterTimeline.Appearances, orderedScenes),
|
||||
MatchingSceneIDs = matchingSceneIds,
|
||||
FocusSceneIDs = focusSceneIds,
|
||||
FocusLabel = focusLabel
|
||||
};
|
||||
}
|
||||
|
||||
public Task<TimelineViewPreset?> GetPresetAsync(int timelineViewPresetId) => presets.GetAsync(timelineViewPresetId);
|
||||
|
||||
public Task ArchivePresetAsync(int timelineViewPresetId) => presets.ArchiveAsync(timelineViewPresetId);
|
||||
|
||||
public Task<int> SavePresetAsync(TimelinePresetSaveViewModel model)
|
||||
{
|
||||
var filter = model.Filter;
|
||||
filter.ProjectID = model.ProjectID;
|
||||
filter.BookID = model.BookID;
|
||||
return presets.SaveAsync(new TimelineViewPreset
|
||||
{
|
||||
ProjectID = model.ProjectID,
|
||||
BookID = model.BookID,
|
||||
PresetName = string.IsNullOrWhiteSpace(model.PresetName) ? "Untitled view" : model.PresetName.Trim(),
|
||||
FilterJson = System.Text.Json.JsonSerializer.Serialize(filter),
|
||||
VisibilityJson = string.IsNullOrWhiteSpace(model.VisibilityJson) ? "{}" : model.VisibilityJson,
|
||||
FocusType = filter.FocusType,
|
||||
FocusID = filter.FocusID
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<IReadOnlySet<int>> BuildMatchingSceneIdsAsync(
|
||||
TimelineFilterViewModel filter,
|
||||
IReadOnlyList<Scene> orderedScenes,
|
||||
IReadOnlyList<ThreadEvent> threadEvents,
|
||||
IReadOnlyList<AssetEvent> assetEvents,
|
||||
IReadOnlyList<SceneCharacter> characterAppearances,
|
||||
int projectId,
|
||||
int? bookId)
|
||||
{
|
||||
var matches = orderedScenes.Select(x => x.SceneID).ToHashSet();
|
||||
void Intersect(IEnumerable<int> sceneIds) => matches.IntersectWith(sceneIds.ToHashSet());
|
||||
|
||||
if (filter.ChapterFromID.HasValue)
|
||||
{
|
||||
var fromIndex = orderedScenes.Select((scene, index) => new { scene, index }).FirstOrDefault(x => x.scene.ChapterID == filter.ChapterFromID)?.index;
|
||||
if (fromIndex.HasValue) Intersect(orderedScenes.Skip(fromIndex.Value).Select(x => x.SceneID));
|
||||
}
|
||||
if (filter.ChapterToID.HasValue)
|
||||
{
|
||||
var toIndex = orderedScenes.Select((scene, index) => new { scene, index }).LastOrDefault(x => x.scene.ChapterID == filter.ChapterToID)?.index;
|
||||
if (toIndex.HasValue) Intersect(orderedScenes.Take(toIndex.Value + 1).Select(x => x.SceneID));
|
||||
}
|
||||
if (filter.SceneFromID.HasValue)
|
||||
{
|
||||
var fromIndex = orderedScenes.Select((scene, index) => new { scene, index }).FirstOrDefault(x => x.scene.SceneID == filter.SceneFromID)?.index;
|
||||
if (fromIndex.HasValue) Intersect(orderedScenes.Skip(fromIndex.Value).Select(x => x.SceneID));
|
||||
}
|
||||
if (filter.SceneToID.HasValue)
|
||||
{
|
||||
var toIndex = orderedScenes.Select((scene, index) => new { scene, index }).FirstOrDefault(x => x.scene.SceneID == filter.SceneToID)?.index;
|
||||
if (toIndex.HasValue) Intersect(orderedScenes.Take(toIndex.Value + 1).Select(x => x.SceneID));
|
||||
}
|
||||
if (filter.RevisionStatusID.HasValue) Intersect(orderedScenes.Where(x => x.RevisionStatusID == filter.RevisionStatusID).Select(x => x.SceneID));
|
||||
if (filter.ScenePurposeTypeID.HasValue) Intersect(orderedScenes.Where(x => x.PurposeLabels.Any(p => p.ScenePurposeTypeID == filter.ScenePurposeTypeID)).Select(x => x.SceneID));
|
||||
if (filter.POVCharacterID.HasValue) Intersect(orderedScenes.Where(x => x.POVCharacterID == filter.POVCharacterID).Select(x => x.SceneID));
|
||||
if (filter.PlotLineID.HasValue) Intersect(threadEvents.Where(x => x.PlotLineID == filter.PlotLineID).Select(x => x.SceneID));
|
||||
if (filter.PlotThreadID.HasValue) Intersect(threadEvents.Where(x => x.PlotThreadID == filter.PlotThreadID).Select(x => x.SceneID));
|
||||
if (filter.StoryAssetID.HasValue) Intersect(assetEvents.Where(x => x.StoryAssetID == filter.StoryAssetID).Select(x => x.SceneID));
|
||||
if (filter.CharacterID.HasValue) Intersect(characterAppearances.Where(x => x.CharacterID == filter.CharacterID).Select(x => x.SceneID));
|
||||
if (filter.LocationID.HasValue)
|
||||
{
|
||||
var locationScenes = (await locations.ListScenesByLocationAsync(filter.LocationID.Value)).Select(x => x.SceneID);
|
||||
var characterLocationScenes = (await locations.ListCharactersByLocationAsync(filter.LocationID.Value)).Select(x => x.SceneID);
|
||||
var assetLocationScenes = (await locations.ListAssetsByLocationAsync(filter.LocationID.Value)).Select(x => x.SceneID);
|
||||
Intersect(locationScenes.Concat(characterLocationScenes).Concat(assetLocationScenes));
|
||||
}
|
||||
if (filter.WarningSeverityID.HasValue || filter.WarningTypeID.HasValue || filter.ShowWarningsOnly)
|
||||
{
|
||||
var warningScenes = (await warnings.ListAsync(projectId, bookId, null, filter.WarningTypeID, filter.WarningSeverityID, null, filter.IncludeDismissedWarnings, filter.IncludeIntentionalWarnings))
|
||||
.Where(x => x.SceneID.HasValue)
|
||||
.Select(x => x.SceneID!.Value);
|
||||
Intersect(warningScenes);
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
private async Task<IReadOnlySet<int>> BuildFocusSceneIdsAsync(
|
||||
TimelineFilterViewModel filter,
|
||||
IReadOnlyList<Scene> orderedScenes,
|
||||
IReadOnlyList<ThreadEvent> threadEvents,
|
||||
IReadOnlyList<AssetEvent> assetEvents,
|
||||
IReadOnlyList<SceneCharacter> characterAppearances,
|
||||
int projectId,
|
||||
int? bookId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filter.FocusType) || !filter.FocusID.HasValue)
|
||||
{
|
||||
return new HashSet<int>();
|
||||
}
|
||||
|
||||
var focusType = filter.FocusType.ToLowerInvariant();
|
||||
return focusType switch
|
||||
{
|
||||
"character" => characterAppearances.Where(x => x.CharacterID == filter.FocusID).Select(x => x.SceneID).ToHashSet(),
|
||||
"asset" => assetEvents.Where(x => x.StoryAssetID == filter.FocusID).Select(x => x.SceneID).ToHashSet(),
|
||||
"plotline" => threadEvents.Where(x => x.PlotLineID == filter.FocusID).Select(x => x.SceneID).ToHashSet(),
|
||||
"plotthread" => threadEvents.Where(x => x.PlotThreadID == filter.FocusID).Select(x => x.SceneID).ToHashSet(),
|
||||
"location" => (await locations.ListScenesByLocationAsync(filter.FocusID.Value)).Select(x => x.SceneID)
|
||||
.Concat((await locations.ListCharactersByLocationAsync(filter.FocusID.Value)).Select(x => x.SceneID))
|
||||
.Concat((await locations.ListAssetsByLocationAsync(filter.FocusID.Value)).Select(x => x.SceneID))
|
||||
.ToHashSet(),
|
||||
"scene" => orderedScenes.Where(x => x.SceneID == filter.FocusID).Select(x => x.SceneID).ToHashSet(),
|
||||
_ => new HashSet<int>()
|
||||
};
|
||||
}
|
||||
|
||||
private static string? BuildFocusLabel(TimelineFilterViewModel filter, IReadOnlyList<PlotLineItem> plotLines, IReadOnlyList<PlotThread> plotThreads, IReadOnlyList<StoryAsset> assets, IReadOnlyList<Character> characters, IReadOnlyList<LocationItem> locations)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filter.FocusType) || !filter.FocusID.HasValue) return null;
|
||||
var name = filter.FocusType.ToLowerInvariant() switch
|
||||
{
|
||||
"character" => characters.FirstOrDefault(x => x.CharacterID == filter.FocusID)?.CharacterName,
|
||||
"asset" => assets.FirstOrDefault(x => x.StoryAssetID == filter.FocusID)?.AssetName,
|
||||
"plotline" => plotLines.FirstOrDefault(x => x.PlotLineID == filter.FocusID)?.PlotLineName,
|
||||
"plotthread" => plotThreads.FirstOrDefault(x => x.PlotThreadID == filter.FocusID)?.ThreadTitle,
|
||||
"location" => locations.FirstOrDefault(x => x.LocationID == filter.FocusID)?.LocationPath,
|
||||
"scene" => "Selected scene",
|
||||
_ => null
|
||||
};
|
||||
return string.IsNullOrWhiteSpace(name) ? null : $"{filter.FocusType}: {name}";
|
||||
}
|
||||
|
||||
private static IReadOnlyList<MetricGraphViewModel> BuildMetricGraphs(TimelineData timeline, IReadOnlyList<Scene> orderedScenes)
|
||||
{
|
||||
var pointsByMetric = timeline.MetricPoints
|
||||
|
||||
109
PlotLine/Sql/011_Phase1L_TimelineViewPresets.sql
Normal file
109
PlotLine/Sql/011_Phase1L_TimelineViewPresets.sql
Normal file
@ -0,0 +1,109 @@
|
||||
SET ANSI_NULLS ON;
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.TimelineViewPresets', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.TimelineViewPresets
|
||||
(
|
||||
TimelineViewPresetID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_TimelineViewPresets PRIMARY KEY,
|
||||
ProjectID int NOT NULL,
|
||||
BookID int NULL,
|
||||
PresetName nvarchar(200) NOT NULL,
|
||||
FilterJson nvarchar(max) NOT NULL,
|
||||
VisibilityJson nvarchar(max) NOT NULL,
|
||||
FocusType nvarchar(50) NULL,
|
||||
FocusID int NULL,
|
||||
CreatedDate datetime2 NOT NULL CONSTRAINT DF_TimelineViewPresets_CreatedDate DEFAULT SYSUTCDATETIME(),
|
||||
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_TimelineViewPresets_UpdatedDate DEFAULT SYSUTCDATETIME(),
|
||||
IsArchived bit NOT NULL CONSTRAINT DF_TimelineViewPresets_IsArchived DEFAULT 0,
|
||||
CONSTRAINT FK_TimelineViewPresets_Projects FOREIGN KEY (ProjectID) REFERENCES dbo.Projects(ProjectID),
|
||||
CONSTRAINT FK_TimelineViewPresets_Books FOREIGN KEY (BookID) REFERENCES dbo.Books(BookID)
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_TimelineViewPresets_ProjectBook' AND object_id = OBJECT_ID(N'dbo.TimelineViewPresets'))
|
||||
CREATE INDEX IX_TimelineViewPresets_ProjectBook ON dbo.TimelineViewPresets(ProjectID, BookID, IsArchived, PresetName);
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.TimelineViewPreset_List
|
||||
@ProjectID int,
|
||||
@BookID int = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT TimelineViewPresetID, ProjectID, BookID, PresetName, FilterJson, VisibilityJson, FocusType, FocusID,
|
||||
CreatedDate, UpdatedDate, IsArchived
|
||||
FROM dbo.TimelineViewPresets
|
||||
WHERE ProjectID = @ProjectID
|
||||
AND IsArchived = 0
|
||||
AND (@BookID IS NULL OR BookID IS NULL OR BookID = @BookID)
|
||||
ORDER BY PresetName;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.TimelineViewPreset_Get
|
||||
@TimelineViewPresetID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT TimelineViewPresetID, ProjectID, BookID, PresetName, FilterJson, VisibilityJson, FocusType, FocusID,
|
||||
CreatedDate, UpdatedDate, IsArchived
|
||||
FROM dbo.TimelineViewPresets
|
||||
WHERE TimelineViewPresetID = @TimelineViewPresetID AND IsArchived = 0;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.TimelineViewPreset_Save
|
||||
@TimelineViewPresetID int = 0,
|
||||
@ProjectID int,
|
||||
@BookID int = NULL,
|
||||
@PresetName nvarchar(200),
|
||||
@FilterJson nvarchar(max),
|
||||
@VisibilityJson nvarchar(max),
|
||||
@FocusType nvarchar(50) = NULL,
|
||||
@FocusID int = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF @TimelineViewPresetID IS NULL OR @TimelineViewPresetID = 0
|
||||
BEGIN
|
||||
INSERT dbo.TimelineViewPresets (ProjectID, BookID, PresetName, FilterJson, VisibilityJson, FocusType, FocusID)
|
||||
VALUES (@ProjectID, @BookID, @PresetName, @FilterJson, @VisibilityJson, @FocusType, @FocusID);
|
||||
|
||||
SELECT CAST(SCOPE_IDENTITY() AS int);
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
UPDATE dbo.TimelineViewPresets
|
||||
SET ProjectID = @ProjectID,
|
||||
BookID = @BookID,
|
||||
PresetName = @PresetName,
|
||||
FilterJson = @FilterJson,
|
||||
VisibilityJson = @VisibilityJson,
|
||||
FocusType = @FocusType,
|
||||
FocusID = @FocusID,
|
||||
UpdatedDate = SYSUTCDATETIME()
|
||||
WHERE TimelineViewPresetID = @TimelineViewPresetID;
|
||||
|
||||
SELECT @TimelineViewPresetID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.TimelineViewPreset_Archive
|
||||
@TimelineViewPresetID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
UPDATE dbo.TimelineViewPresets
|
||||
SET IsArchived = 1,
|
||||
UpdatedDate = SYSUTCDATETIME()
|
||||
WHERE TimelineViewPresetID = @TimelineViewPresetID;
|
||||
END;
|
||||
GO
|
||||
@ -191,16 +191,79 @@ public sealed class TimelineViewModel
|
||||
public int? SelectedBookID { get; set; }
|
||||
public int? SelectedSceneID { get; set; }
|
||||
public SceneEditViewModel? SelectedScene { get; set; }
|
||||
public TimelineFilterViewModel Filter { get; set; } = new();
|
||||
public IReadOnlyList<BookTimelineViewModel> Books { get; set; } = [];
|
||||
public IReadOnlyList<SelectListItem> BookOptions { get; set; } = [];
|
||||
public IReadOnlyList<SelectListItem> ChapterOptions { get; set; } = [];
|
||||
public IReadOnlyList<SelectListItem> ChapterFromOptions { get; set; } = [];
|
||||
public IReadOnlyList<SelectListItem> ChapterToOptions { get; set; } = [];
|
||||
public IReadOnlyList<SelectListItem> SceneOptions { get; set; } = [];
|
||||
public IReadOnlyList<SelectListItem> SceneFromOptions { get; set; } = [];
|
||||
public IReadOnlyList<SelectListItem> SceneToOptions { get; set; } = [];
|
||||
public IReadOnlyList<SelectListItem> PlotLineOptions { get; set; } = [];
|
||||
public IReadOnlyList<SelectListItem> PlotThreadOptions { get; set; } = [];
|
||||
public IReadOnlyList<SelectListItem> AssetOptions { get; set; } = [];
|
||||
public IReadOnlyList<SelectListItem> CharacterOptions { get; set; } = [];
|
||||
public IReadOnlyList<SelectListItem> PovCharacterOptions { get; set; } = [];
|
||||
public IReadOnlyList<SelectListItem> LocationOptions { get; set; } = [];
|
||||
public IReadOnlyList<SelectListItem> WarningSeverityOptions { get; set; } = [];
|
||||
public IReadOnlyList<SelectListItem> WarningTypeOptions { get; set; } = [];
|
||||
public IReadOnlyList<SelectListItem> RevisionStatusOptions { get; set; } = [];
|
||||
public IReadOnlyList<SelectListItem> ScenePurposeOptions { get; set; } = [];
|
||||
public IReadOnlyList<TimelineViewPreset> ViewPresets { get; set; } = [];
|
||||
public IReadOnlyList<Scene> OrderedScenes { get; set; } = [];
|
||||
public IReadOnlyList<MetricGraphViewModel> MetricGraphs { get; set; } = [];
|
||||
public IReadOnlyList<PlotLineLaneViewModel> PlotLanes { get; set; } = [];
|
||||
public IReadOnlyList<AssetLaneViewModel> AssetLanes { get; set; } = [];
|
||||
public IReadOnlyList<CharacterLaneViewModel> CharacterLanes { get; set; } = [];
|
||||
public IReadOnlySet<int> MatchingSceneIDs { get; set; } = new HashSet<int>();
|
||||
public IReadOnlySet<int> FocusSceneIDs { get; set; } = new HashSet<int>();
|
||||
public string? FocusLabel { get; set; }
|
||||
public ContinuityValidationSummary? LastValidationSummary { get; set; }
|
||||
}
|
||||
|
||||
public sealed class TimelineFilterViewModel
|
||||
{
|
||||
public int ProjectID { get; set; }
|
||||
public int? BookID { get; set; }
|
||||
public int? SelectedSceneID { get; set; }
|
||||
public int? ChapterFromID { get; set; }
|
||||
public int? ChapterToID { get; set; }
|
||||
public int? SceneFromID { get; set; }
|
||||
public int? SceneToID { get; set; }
|
||||
public int? PlotLineID { get; set; }
|
||||
public int? PlotThreadID { get; set; }
|
||||
public int? StoryAssetID { get; set; }
|
||||
public int? CharacterID { get; set; }
|
||||
public int? LocationID { get; set; }
|
||||
public int? WarningSeverityID { get; set; }
|
||||
public int? WarningTypeID { get; set; }
|
||||
public int? RevisionStatusID { get; set; }
|
||||
public int? ScenePurposeTypeID { get; set; }
|
||||
public int? POVCharacterID { get; set; }
|
||||
public string? FocusType { get; set; }
|
||||
public int? FocusID { get; set; }
|
||||
public bool ShowSceneCards { get; set; } = true;
|
||||
public bool ShowPlotLines { get; set; } = true;
|
||||
public bool ShowAssets { get; set; } = true;
|
||||
public bool ShowCharacters { get; set; } = true;
|
||||
public bool ShowLocations { get; set; } = true;
|
||||
public bool ShowWarnings { get; set; } = true;
|
||||
public bool ShowMetrics { get; set; } = true;
|
||||
public bool ShowWarningsOnly { get; set; }
|
||||
public bool IncludeDismissedWarnings { get; set; }
|
||||
public bool IncludeIntentionalWarnings { get; set; }
|
||||
}
|
||||
|
||||
public sealed class TimelinePresetSaveViewModel
|
||||
{
|
||||
public int ProjectID { get; set; }
|
||||
public int? BookID { get; set; }
|
||||
public string PresetName { get; set; } = string.Empty;
|
||||
public TimelineFilterViewModel Filter { get; set; } = new();
|
||||
public string VisibilityJson { get; set; } = "{}";
|
||||
}
|
||||
|
||||
public sealed class BookTimelineViewModel
|
||||
{
|
||||
public Book Book { get; set; } = new();
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
sceneNumber = point.SceneNumber.ToString("g"),
|
||||
sceneTitle = point.SceneTitle,
|
||||
bookTitle = point.BookTitle,
|
||||
timelineUrl = Url.Action("Index", "Timeline", new { projectId = Model.Project.ProjectID, bookId = point.BookID, selectedSceneId = point.SceneID })
|
||||
timelineUrl = Url.Action("Index", "Timeline", new { ProjectID = Model.Project.ProjectID, BookID = point.BookID, SelectedSceneID = point.SceneID, FocusType = "scene", FocusID = point.SceneID })
|
||||
})
|
||||
.ToList();
|
||||
var metricShapePayload = new
|
||||
@ -132,9 +132,11 @@
|
||||
title="@graph.MetricName - Scene @point.SceneNumber: @point.SceneTitle (@point.Value)"
|
||||
asp-controller="Timeline"
|
||||
asp-action="Index"
|
||||
asp-route-projectId="@Model.Project.ProjectID"
|
||||
asp-route-bookId="@point.BookID"
|
||||
asp-route-selectedSceneId="@point.SceneID">
|
||||
asp-route-ProjectID="@Model.Project.ProjectID"
|
||||
asp-route-BookID="@point.BookID"
|
||||
asp-route-SelectedSceneID="@point.SceneID"
|
||||
asp-route-FocusType="scene"
|
||||
asp-route-FocusID="@point.SceneID">
|
||||
<span>@point.Value</span>
|
||||
</a>
|
||||
}
|
||||
@ -179,7 +181,7 @@
|
||||
<h3>Needs attention</h3>
|
||||
@foreach (var scene in Model.RevisionAttentionScenes.Take(8))
|
||||
{
|
||||
<a class="analytics-link-row" asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@scene.BookID" asp-route-selectedSceneId="@scene.SceneID">
|
||||
<a class="analytics-link-row" asp-controller="Timeline" asp-action="Index" asp-route-ProjectID="@Model.Project.ProjectID" asp-route-BookID="@scene.BookID" asp-route-SelectedSceneID="@scene.SceneID" asp-route-FocusType="scene" asp-route-FocusID="@scene.SceneID">
|
||||
<span>@scene.RevisionStatusName</span>
|
||||
<strong>Ch @scene.ChapterNumber / Scene @scene.SceneNumber: @scene.SceneTitle</strong>
|
||||
</a>
|
||||
@ -201,7 +203,7 @@
|
||||
<h3>Missing purpose/outcome</h3>
|
||||
@foreach (var gap in Model.SceneGaps.Take(8))
|
||||
{
|
||||
<a class="analytics-link-row" asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@gap.BookID" asp-route-selectedSceneId="@gap.SceneID">
|
||||
<a class="analytics-link-row" asp-controller="Timeline" asp-action="Index" asp-route-ProjectID="@Model.Project.ProjectID" asp-route-BookID="@gap.BookID" asp-route-SelectedSceneID="@gap.SceneID" asp-route-FocusType="scene" asp-route-FocusID="@gap.SceneID">
|
||||
<span>@(gap.MissingPurpose ? "No purpose" : "") @(gap.MissingOutcome ? "No outcome" : "")</span>
|
||||
<strong>Ch @gap.ChapterNumber / Scene @gap.SceneNumber: @gap.SceneTitle</strong>
|
||||
</a>
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
<strong>@Model.Project.ProjectName</strong>
|
||||
</div>
|
||||
<nav class="context-nav" aria-label="Project navigation">
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@firstAppearance?.SceneID">Show on timeline</a>
|
||||
<a 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">Show on timeline</a>
|
||||
<a asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story health</a>
|
||||
<a asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Warnings</a>
|
||||
<a asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Characters</a>
|
||||
@ -32,7 +32,7 @@
|
||||
</div>
|
||||
<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">Back to timeline</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-action="Edit" asp-route-id="@Model.Character.CharacterID">Edit character</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -82,7 +82,7 @@
|
||||
<strong>@appearance.BookTitle / Ch @appearance.ChapterNumber / Scene @appearance.SceneNumber: @appearance.SceneTitle</strong>
|
||||
<span>@appearance.RoleInSceneTypeName / @appearance.PresenceTypeName</span>
|
||||
<span>@appearance.OutfitDescription @appearance.PhysicalCondition @appearance.EmotionalState</span>
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@appearance.SceneID">Open in timeline</a>
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-ProjectID="@Model.Project.ProjectID" asp-route-SelectedSceneID="@appearance.SceneID" asp-route-FocusType="character" asp-route-FocusID="@Model.Character.CharacterID">Open in timeline</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@ -104,7 +104,7 @@
|
||||
<strong>@item.AttributeName: @item.AttributeValue</strong>
|
||||
<span>@item.BookTitle / Ch @item.ChapterNumber / Scene @item.SceneNumber: @item.SceneTitle</span>
|
||||
<span>@item.Description</span>
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@item.SceneID">Open in timeline</a>
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-ProjectID="@Model.Project.ProjectID" asp-route-SelectedSceneID="@item.SceneID" asp-route-FocusType="character" asp-route-FocusID="@Model.Character.CharacterID">Open in timeline</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@ -126,7 +126,7 @@
|
||||
<strong>@item.KnowledgeStateName: @(item.AssetName ?? item.ThreadTitle)</strong>
|
||||
<span>@item.BookTitle / Ch @item.ChapterNumber / Scene @item.SceneNumber: @item.SceneTitle</span>
|
||||
<span>@item.Description</span>
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@item.SceneID">Open in timeline</a>
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-ProjectID="@Model.Project.ProjectID" asp-route-SelectedSceneID="@item.SceneID" asp-route-FocusType="character" asp-route-FocusID="@Model.Character.CharacterID">Open in timeline</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@ -148,7 +148,7 @@
|
||||
<strong>@item.CharacterAName -> @item.CharacterBName: @item.RelationshipStateName</strong>
|
||||
<span>@item.BookTitle / Ch @item.ChapterNumber / Scene @item.SceneNumber: @item.SceneTitle</span>
|
||||
<span>Intensity @item.Intensity. @item.Description</span>
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@item.SceneID">Open in timeline</a>
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-ProjectID="@Model.Project.ProjectID" asp-route-SelectedSceneID="@item.SceneID" asp-route-FocusType="character" asp-route-FocusID="@Model.Character.CharacterID">Open in timeline</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
<strong>@Model.Project.ProjectName</strong>
|
||||
</div>
|
||||
<nav class="context-nav" aria-label="Project navigation">
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@firstScene?.SceneID">Show scene usage</a>
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-ProjectID="@Model.Project.ProjectID" asp-route-SelectedSceneID="@firstScene?.SceneID" asp-route-FocusType="location" asp-route-FocusID="@Model.Location.LocationID">Show scene usage</a>
|
||||
<a asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story health</a>
|
||||
<a asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Warnings</a>
|
||||
<a asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Locations</a>
|
||||
@ -32,7 +32,7 @@
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<a class="btn btn-outline-secondary" asp-action="Edit" asp-route-id="@Model.Location.LocationID">Edit</a>
|
||||
<a class="btn btn-outline-secondary" asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@firstScene?.SceneID">Back to timeline</a>
|
||||
<a class="btn btn-outline-secondary" asp-controller="Timeline" asp-action="Index" asp-route-ProjectID="@Model.Project.ProjectID" asp-route-SelectedSceneID="@firstScene?.SceneID" asp-route-FocusType="location" asp-route-FocusID="@Model.Location.LocationID">Back to timeline</a>
|
||||
<a class="btn btn-outline-primary" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">All locations</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -142,7 +142,7 @@
|
||||
<h3>Scenes</h3>
|
||||
@foreach (var scene in Model.Scenes)
|
||||
{
|
||||
<p><strong>Scene @scene.SceneNumber</strong> <a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@scene.SceneID">@scene.SceneTitle</a></p>
|
||||
<p><strong>Scene @scene.SceneNumber</strong> <a asp-controller="Timeline" asp-action="Index" asp-route-ProjectID="@Model.Project.ProjectID" asp-route-SelectedSceneID="@scene.SceneID" asp-route-FocusType="location" asp-route-FocusID="@Model.Location.LocationID">@scene.SceneTitle</a></p>
|
||||
}
|
||||
@if (!Model.Scenes.Any()) { <p class="muted">No scenes set here.</p> }
|
||||
</div>
|
||||
|
||||
@ -54,6 +54,7 @@
|
||||
<td>@(plotLine.BookTitle ?? "Project-wide")</td>
|
||||
<td>@(plotLine.IsVisibleOnTimeline ? "Visible" : "Hidden")</td>
|
||||
<td class="text-end">
|
||||
<a class="btn btn-outline-secondary btn-sm" asp-controller="Timeline" asp-action="Index" asp-route-ProjectID="@Model.Project.ProjectID" asp-route-FocusType="plotline" asp-route-FocusID="@plotLine.PlotLineID">Focus</a>
|
||||
<a class="btn btn-outline-secondary btn-sm" asp-controller="PlotThreads" asp-action="Create" asp-route-projectId="@Model.Project.ProjectID" asp-route-plotLineId="@plotLine.PlotLineID">Add thread</a>
|
||||
<a class="btn btn-outline-secondary btn-sm" asp-action="Edit" asp-route-id="@plotLine.PlotLineID">Edit</a>
|
||||
</td>
|
||||
|
||||
@ -56,7 +56,10 @@
|
||||
<td>@thread.ThreadTypeName</td>
|
||||
<td><span class="status-pill">@thread.ThreadStatusName</span></td>
|
||||
<td>@thread.Importance</td>
|
||||
<td class="text-end"><a class="btn btn-outline-secondary btn-sm" asp-action="Edit" asp-route-id="@thread.PlotThreadID">Edit</a></td>
|
||||
<td class="text-end">
|
||||
<a class="btn btn-outline-secondary btn-sm" asp-controller="Timeline" asp-action="Index" asp-route-ProjectID="@Model.Project.ProjectID" asp-route-FocusType="plotthread" asp-route-FocusID="@thread.PlotThreadID">Focus</a>
|
||||
<a class="btn btn-outline-secondary btn-sm" asp-action="Edit" asp-route-id="@thread.PlotThreadID">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
<strong>@Model.Project.ProjectName</strong>
|
||||
</div>
|
||||
<nav class="context-nav" aria-label="Project navigation">
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@firstSceneId">Show on timeline</a>
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-ProjectID="@Model.Project.ProjectID" asp-route-SelectedSceneID="@firstSceneId" asp-route-FocusType="asset" asp-route-FocusID="@Model.Asset.StoryAssetID">Show on timeline</a>
|
||||
<a asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story health</a>
|
||||
<a asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Warnings</a>
|
||||
<a asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Assets</a>
|
||||
@ -34,7 +34,7 @@
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<a class="btn btn-outline-secondary" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story assets</a>
|
||||
<a class="btn btn-outline-secondary" asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@firstSceneId">Back to timeline</a>
|
||||
<a class="btn btn-outline-secondary" asp-controller="Timeline" asp-action="Index" asp-route-ProjectID="@Model.Project.ProjectID" asp-route-SelectedSceneID="@firstSceneId" asp-route-FocusType="asset" asp-route-FocusID="@Model.Asset.StoryAssetID">Back to timeline</a>
|
||||
<a class="btn btn-outline-primary" asp-action="Edit" asp-route-id="@Model.Asset.StoryAssetID">Edit asset</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -127,7 +127,7 @@
|
||||
<strong>@assetEvent.EventTitle</strong>
|
||||
<span>@assetEvent.BookTitle / Ch @assetEvent.ChapterNumber / Scene @assetEvent.SceneNumber: @assetEvent.SceneTitle</span>
|
||||
<span>@assetEvent.FromStateName -> @assetEvent.ToStateName</span>
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@assetEvent.SceneID">Open in timeline</a>
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-ProjectID="@Model.Project.ProjectID" asp-route-SelectedSceneID="@assetEvent.SceneID" asp-route-FocusType="asset" asp-route-FocusID="@Model.Asset.StoryAssetID">Open in timeline</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@ -150,7 +150,7 @@
|
||||
<strong>@custody.CustodyEventTypeName</strong>
|
||||
<span>@custody.CustodianSummary</span>
|
||||
<span>@custody.BookTitle / Ch @custody.ChapterNumber / Scene @custody.SceneNumber: @custody.SceneTitle</span>
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@custody.SceneID">Open in timeline</a>
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-ProjectID="@Model.Project.ProjectID" asp-route-SelectedSceneID="@custody.SceneID" asp-route-FocusType="asset" asp-route-FocusID="@Model.Asset.StoryAssetID">Open in timeline</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
points = graph.Points.Select(point => new { sceneId = point.SceneID, value = point.Value })
|
||||
})
|
||||
};
|
||||
var hasActiveFilter = Model.MatchingSceneIDs.Count != Model.OrderedScenes.Count || !string.IsNullOrWhiteSpace(Model.FocusLabel);
|
||||
}
|
||||
|
||||
<section class="context-header">
|
||||
@ -49,8 +50,8 @@
|
||||
<h1>@Model.Project.ProjectName</h1>
|
||||
</div>
|
||||
<form method="get" class="timeline-filter">
|
||||
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
|
||||
<select name="bookId" asp-items="Model.BookOptions" class="form-select">
|
||||
<input type="hidden" name="ProjectID" value="@Model.Project.ProjectID" />
|
||||
<select name="BookID" asp-items="Model.BookOptions" class="form-select">
|
||||
<option value="">All books</option>
|
||||
</select>
|
||||
<button class="btn btn-outline-primary" type="submit">View</button>
|
||||
@ -74,6 +75,179 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="timeline-control-panel" data-timeline-controls>
|
||||
<details class="timeline-filter-drawer" @(hasActiveFilter ? "open" : "")>
|
||||
<summary>Filters and focus</summary>
|
||||
<form method="get" class="timeline-filter-grid">
|
||||
<input type="hidden" name="ProjectID" value="@Model.Project.ProjectID" />
|
||||
<div>
|
||||
<label class="form-label">Book</label>
|
||||
<select name="BookID" asp-items="Model.BookOptions" class="form-select form-select-sm">
|
||||
<option value="">All books</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">From chapter</label>
|
||||
<select name="ChapterFromID" asp-items="Model.ChapterFromOptions" class="form-select form-select-sm">
|
||||
<option value="">Start</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">To chapter</label>
|
||||
<select name="ChapterToID" asp-items="Model.ChapterToOptions" class="form-select form-select-sm">
|
||||
<option value="">End</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">From scene</label>
|
||||
<select name="SceneFromID" asp-items="Model.SceneFromOptions" class="form-select form-select-sm">
|
||||
<option value="">Start</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">To scene</label>
|
||||
<select name="SceneToID" asp-items="Model.SceneToOptions" class="form-select form-select-sm">
|
||||
<option value="">End</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Plot line</label>
|
||||
<select name="PlotLineID" asp-items="Model.PlotLineOptions" class="form-select form-select-sm"><option value="">Any</option></select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Plot thread</label>
|
||||
<select name="PlotThreadID" asp-items="Model.PlotThreadOptions" class="form-select form-select-sm"><option value="">Any</option></select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Story asset</label>
|
||||
<select name="StoryAssetID" asp-items="Model.AssetOptions" class="form-select form-select-sm"><option value="">Any</option></select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Character</label>
|
||||
<select name="CharacterID" asp-items="Model.CharacterOptions" class="form-select form-select-sm"><option value="">Any</option></select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Location</label>
|
||||
<select name="LocationID" asp-items="Model.LocationOptions" class="form-select form-select-sm"><option value="">Any</option></select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Warning severity</label>
|
||||
<select name="WarningSeverityID" asp-items="Model.WarningSeverityOptions" class="form-select form-select-sm"><option value="">Any</option></select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Warning type</label>
|
||||
<select name="WarningTypeID" asp-items="Model.WarningTypeOptions" class="form-select form-select-sm"><option value="">Any</option></select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Revision status</label>
|
||||
<select name="RevisionStatusID" asp-items="Model.RevisionStatusOptions" class="form-select form-select-sm"><option value="">Any</option></select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Scene purpose</label>
|
||||
<select name="ScenePurposeTypeID" asp-items="Model.ScenePurposeOptions" class="form-select form-select-sm"><option value="">Any</option></select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">POV character</label>
|
||||
<select name="POVCharacterID" asp-items="Model.PovCharacterOptions" class="form-select form-select-sm"><option value="">Any</option></select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Focus type</label>
|
||||
<select name="FocusType" class="form-select form-select-sm">
|
||||
<option value="">No focus</option>
|
||||
<option value="character" selected="@(Model.Filter.FocusType == "character")">Character</option>
|
||||
<option value="asset" selected="@(Model.Filter.FocusType == "asset")">Story asset</option>
|
||||
<option value="plotline" selected="@(Model.Filter.FocusType == "plotline")">Plot line</option>
|
||||
<option value="plotthread" selected="@(Model.Filter.FocusType == "plotthread")">Plot thread</option>
|
||||
<option value="location" selected="@(Model.Filter.FocusType == "location")">Location</option>
|
||||
<option value="scene" selected="@(Model.Filter.FocusType == "scene")">Scene</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Focus ID</label>
|
||||
<input name="FocusID" value="@Model.Filter.FocusID" class="form-control form-control-sm" placeholder="Entity ID" />
|
||||
</div>
|
||||
<div class="timeline-filter-checks">
|
||||
<label><input type="checkbox" name="ShowWarningsOnly" value="true" checked="@Model.Filter.ShowWarningsOnly" /> Warnings only</label>
|
||||
<label><input type="checkbox" name="IncludeDismissedWarnings" value="true" checked="@Model.Filter.IncludeDismissedWarnings" /> Include dismissed</label>
|
||||
<label><input type="checkbox" name="IncludeIntentionalWarnings" value="true" checked="@Model.Filter.IncludeIntentionalWarnings" /> Include intentional</label>
|
||||
</div>
|
||||
<div class="timeline-filter-actions">
|
||||
<button class="btn btn-outline-primary btn-sm" type="submit">Apply filters</button>
|
||||
<a class="btn btn-outline-secondary btn-sm" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Clear filters</a>
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
|
||||
<div class="timeline-visibility-panel">
|
||||
<span class="eyebrow">Visibility</span>
|
||||
<label><input type="checkbox" data-timeline-toggle="scene-cards" checked /> Scene cards</label>
|
||||
<label><input type="checkbox" data-timeline-toggle="plot-lines" checked /> Plot lines</label>
|
||||
<label><input type="checkbox" data-timeline-toggle="assets" checked /> Story assets</label>
|
||||
<label><input type="checkbox" data-timeline-toggle="characters" checked /> Characters</label>
|
||||
<label><input type="checkbox" data-timeline-toggle="locations" checked /> Locations</label>
|
||||
<label><input type="checkbox" data-timeline-toggle="warnings" checked /> Warnings</label>
|
||||
<label><input type="checkbox" data-timeline-toggle="metrics" checked /> Metrics</label>
|
||||
</div>
|
||||
|
||||
<div class="timeline-preset-panel">
|
||||
<form asp-action="SavePreset" method="post" data-timeline-preset-form>
|
||||
<input type="hidden" name="ProjectID" value="@Model.Project.ProjectID" />
|
||||
<input type="hidden" name="BookID" value="@Model.SelectedBookID" />
|
||||
<input type="hidden" name="VisibilityJson" value="{}" data-visibility-json />
|
||||
<input type="hidden" name="Filter.ProjectID" value="@Model.Project.ProjectID" />
|
||||
<input type="hidden" name="Filter.BookID" value="@Model.Filter.BookID" />
|
||||
<input type="hidden" name="Filter.SelectedSceneID" value="@Model.Filter.SelectedSceneID" />
|
||||
<input type="hidden" name="Filter.ChapterFromID" value="@Model.Filter.ChapterFromID" />
|
||||
<input type="hidden" name="Filter.ChapterToID" value="@Model.Filter.ChapterToID" />
|
||||
<input type="hidden" name="Filter.SceneFromID" value="@Model.Filter.SceneFromID" />
|
||||
<input type="hidden" name="Filter.SceneToID" value="@Model.Filter.SceneToID" />
|
||||
<input type="hidden" name="Filter.PlotLineID" value="@Model.Filter.PlotLineID" />
|
||||
<input type="hidden" name="Filter.PlotThreadID" value="@Model.Filter.PlotThreadID" />
|
||||
<input type="hidden" name="Filter.StoryAssetID" value="@Model.Filter.StoryAssetID" />
|
||||
<input type="hidden" name="Filter.CharacterID" value="@Model.Filter.CharacterID" />
|
||||
<input type="hidden" name="Filter.LocationID" value="@Model.Filter.LocationID" />
|
||||
<input type="hidden" name="Filter.WarningSeverityID" value="@Model.Filter.WarningSeverityID" />
|
||||
<input type="hidden" name="Filter.WarningTypeID" value="@Model.Filter.WarningTypeID" />
|
||||
<input type="hidden" name="Filter.RevisionStatusID" value="@Model.Filter.RevisionStatusID" />
|
||||
<input type="hidden" name="Filter.ScenePurposeTypeID" value="@Model.Filter.ScenePurposeTypeID" />
|
||||
<input type="hidden" name="Filter.POVCharacterID" value="@Model.Filter.POVCharacterID" />
|
||||
<input type="hidden" name="Filter.FocusType" value="@Model.Filter.FocusType" />
|
||||
<input type="hidden" name="Filter.FocusID" value="@Model.Filter.FocusID" />
|
||||
<input type="hidden" name="Filter.ShowWarningsOnly" value="@Model.Filter.ShowWarningsOnly.ToString().ToLowerInvariant()" />
|
||||
<input type="hidden" name="Filter.IncludeDismissedWarnings" value="@Model.Filter.IncludeDismissedWarnings.ToString().ToLowerInvariant()" />
|
||||
<input type="hidden" name="Filter.IncludeIntentionalWarnings" value="@Model.Filter.IncludeIntentionalWarnings.ToString().ToLowerInvariant()" />
|
||||
<input class="form-control form-control-sm" name="PresetName" placeholder="View name" />
|
||||
<button class="btn btn-outline-primary btn-sm" type="submit">Save view</button>
|
||||
</form>
|
||||
@if (Model.ViewPresets.Any())
|
||||
{
|
||||
<div class="preset-chip-row">
|
||||
@foreach (var preset in Model.ViewPresets)
|
||||
{
|
||||
<span class="preset-chip">
|
||||
<a asp-action="LoadPreset" asp-route-id="@preset.TimelineViewPresetID">@preset.PresetName</a>
|
||||
<form asp-action="DeletePreset" method="post">
|
||||
<input type="hidden" name="id" value="@preset.TimelineViewPresetID" />
|
||||
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
|
||||
<input type="hidden" name="bookId" value="@Model.SelectedBookID" />
|
||||
<button type="submit" title="Delete view">x</button>
|
||||
</form>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(Model.FocusLabel))
|
||||
{
|
||||
<section class="timeline-focus-banner">
|
||||
<strong>Focus: @Model.FocusLabel</strong>
|
||||
<span>@Model.FocusSceneIDs.Count matching scene@(Model.FocusSceneIDs.Count == 1 ? "" : "s")</span>
|
||||
<a class="btn btn-outline-secondary btn-sm" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Clear focus</a>
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (!Model.Books.Any())
|
||||
{
|
||||
<section class="empty-panel">
|
||||
@ -83,9 +257,9 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="workspace-layout">
|
||||
<div class="workspace-layout timeline-filtered-root" data-timeline-root>
|
||||
<main class="timeline-workspace">
|
||||
<section class="timeline-shell workspace-timeline">
|
||||
<section class="timeline-shell workspace-timeline" data-timeline-section="scene-cards">
|
||||
@foreach (var book in Model.Books)
|
||||
{
|
||||
<div class="timeline-book">
|
||||
@ -109,19 +283,24 @@ else
|
||||
@foreach (var scene in chapter.Scenes)
|
||||
{
|
||||
var selectedClass = scene.SceneID == Model.SelectedSceneID ? "selected" : "";
|
||||
<a class="timeline-scene-card @selectedClass"
|
||||
var matchClass = Model.MatchingSceneIDs.Contains(scene.SceneID) ? "filter-match" : "filter-dim";
|
||||
var focusClass = Model.FocusSceneIDs.Contains(scene.SceneID) ? "focus-match" : (Model.FocusSceneIDs.Any() ? "focus-dim" : "");
|
||||
<a class="timeline-scene-card @selectedClass @matchClass @focusClass"
|
||||
data-scene-id="@scene.SceneID"
|
||||
asp-controller="Timeline"
|
||||
asp-action="Index"
|
||||
asp-route-projectId="@Model.Project.ProjectID"
|
||||
asp-route-bookId="@Model.SelectedBookID"
|
||||
asp-route-selectedSceneId="@scene.SceneID">
|
||||
asp-route-ProjectID="@Model.Project.ProjectID"
|
||||
asp-route-BookID="@Model.SelectedBookID"
|
||||
asp-route-SelectedSceneID="@scene.SceneID"
|
||||
asp-route-FocusType="scene"
|
||||
asp-route-FocusID="@scene.SceneID">
|
||||
<span class="scene-number">Scene @scene.SceneNumber</span>
|
||||
<strong>@scene.SceneTitle</strong>
|
||||
<div class="scene-badge-row">
|
||||
<span class="status-pill">@scene.RevisionStatusName</span>
|
||||
@if (scene.WarningCount > 0)
|
||||
{
|
||||
<span class="warning-placeholder active">@scene.WarningCount warning@(scene.WarningCount == 1 ? "" : "s")</span>
|
||||
<span class="warning-placeholder active" data-warning-marker>@scene.WarningCount warning@(scene.WarningCount == 1 ? "" : "s")</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -135,7 +314,7 @@ else
|
||||
<span class="time-label">@scene.TimeLabel</span>
|
||||
@if (!string.IsNullOrWhiteSpace(scene.PrimaryLocationName))
|
||||
{
|
||||
<span class="location-pill" title="@scene.PrimaryLocationPath">@scene.PrimaryLocationName</span>
|
||||
<span class="location-pill" data-location-marker title="@scene.PrimaryLocationPath">@scene.PrimaryLocationName</span>
|
||||
}
|
||||
@if (scene.PurposeLabels.Any())
|
||||
{
|
||||
@ -159,14 +338,15 @@ else
|
||||
}
|
||||
@if (Model.PlotLanes.Any() && Model.OrderedScenes.Any())
|
||||
{
|
||||
<div class="plot-lane-board" style="--scene-count:@Model.OrderedScenes.Count">
|
||||
<div class="plot-lane-board" data-timeline-section="plot-lines" style="--scene-count:@Model.OrderedScenes.Count">
|
||||
<div class="plot-lane-board-heading">
|
||||
<span>Plot line lanes</span>
|
||||
<small>Thread event markers by scene</small>
|
||||
</div>
|
||||
@foreach (var lane in Model.PlotLanes)
|
||||
{
|
||||
<div class="plot-lane-row" style="--plot-colour:@lane.PlotLine.Colour">
|
||||
var laneFocusClass = Model.Filter.FocusType == "plotline" && Model.Filter.FocusID.HasValue && Model.Filter.FocusID != lane.PlotLine.PlotLineID ? "lane-dim" : "";
|
||||
<div class="plot-lane-row @laneFocusClass" style="--plot-colour:@lane.PlotLine.Colour">
|
||||
<a class="plot-lane-label" asp-controller="PlotLines" asp-action="Edit" asp-route-id="@lane.PlotLine.PlotLineID">
|
||||
<span class="colour-swatch" style="background:@lane.PlotLine.Colour"></span>
|
||||
<strong>@lane.PlotLine.PlotLineName</strong>
|
||||
@ -185,9 +365,11 @@ else
|
||||
<a class="thread-marker @markerClass"
|
||||
asp-controller="Timeline"
|
||||
asp-action="Index"
|
||||
asp-route-projectId="@Model.Project.ProjectID"
|
||||
asp-route-bookId="@Model.SelectedBookID"
|
||||
asp-route-selectedSceneId="@slot.Scene.SceneID"
|
||||
asp-route-ProjectID="@Model.Project.ProjectID"
|
||||
asp-route-BookID="@Model.SelectedBookID"
|
||||
asp-route-SelectedSceneID="@slot.Scene.SceneID"
|
||||
asp-route-FocusType="plotthread"
|
||||
asp-route-FocusID="@threadEvent.PlotThreadID"
|
||||
title="@threadEvent.ThreadTitle: @threadEvent.EventTitle">
|
||||
@threadEvent.MarkerText
|
||||
</a>
|
||||
@ -207,14 +389,15 @@ else
|
||||
}
|
||||
@if (Model.AssetLanes.Any() && Model.OrderedScenes.Any())
|
||||
{
|
||||
<div class="asset-lane-board" style="--scene-count:@Model.OrderedScenes.Count">
|
||||
<div class="asset-lane-board" data-timeline-section="assets" style="--scene-count:@Model.OrderedScenes.Count">
|
||||
<div class="plot-lane-board-heading">
|
||||
<span>Story asset lanes</span>
|
||||
<small>Asset event markers by scene</small>
|
||||
</div>
|
||||
@foreach (var lane in Model.AssetLanes)
|
||||
{
|
||||
<div class="asset-lane-row">
|
||||
var assetFocusClass = Model.Filter.FocusType == "asset" && Model.Filter.FocusID.HasValue && Model.Filter.FocusID != lane.Asset.StoryAssetID ? "lane-dim" : "";
|
||||
<div class="asset-lane-row @assetFocusClass">
|
||||
<a class="asset-lane-label" asp-controller="StoryAssets" asp-action="Details" asp-route-id="@lane.Asset.StoryAssetID">
|
||||
<strong>@lane.Asset.AssetName</strong>
|
||||
<span>@lane.Asset.KindName / @lane.Asset.Importance</span>
|
||||
@ -226,9 +409,13 @@ else
|
||||
@foreach (var assetEvent in slot.Events)
|
||||
{
|
||||
<a class="asset-marker"
|
||||
asp-controller="StoryAssets"
|
||||
asp-action="Details"
|
||||
asp-route-id="@assetEvent.StoryAssetID"
|
||||
asp-controller="Timeline"
|
||||
asp-action="Index"
|
||||
asp-route-ProjectID="@Model.Project.ProjectID"
|
||||
asp-route-BookID="@Model.SelectedBookID"
|
||||
asp-route-SelectedSceneID="@slot.Scene.SceneID"
|
||||
asp-route-FocusType="asset"
|
||||
asp-route-FocusID="@assetEvent.StoryAssetID"
|
||||
title="@assetEvent.AssetName: @assetEvent.EventTitle">
|
||||
@assetEvent.MarkerText
|
||||
</a>
|
||||
@ -248,14 +435,15 @@ else
|
||||
}
|
||||
@if (Model.CharacterLanes.Any() && Model.OrderedScenes.Any())
|
||||
{
|
||||
<div class="character-lane-board" style="--scene-count:@Model.OrderedScenes.Count">
|
||||
<div class="character-lane-board" data-timeline-section="characters" style="--scene-count:@Model.OrderedScenes.Count">
|
||||
<div class="plot-lane-board-heading">
|
||||
<span>Character appearances</span>
|
||||
<small>First twelve project characters</small>
|
||||
</div>
|
||||
@foreach (var lane in Model.CharacterLanes)
|
||||
{
|
||||
<div class="character-lane-row">
|
||||
var characterFocusClass = Model.Filter.FocusType == "character" && Model.Filter.FocusID.HasValue && Model.Filter.FocusID != lane.Character.CharacterID ? "lane-dim" : "";
|
||||
<div class="character-lane-row @characterFocusClass">
|
||||
<a class="character-lane-label" asp-controller="Characters" asp-action="Details" asp-route-id="@lane.Character.CharacterID">
|
||||
<strong>@lane.Character.CharacterName</strong>
|
||||
<span>@(lane.Character.ShortName ?? "Character")</span>
|
||||
@ -269,9 +457,11 @@ else
|
||||
<a class="character-marker"
|
||||
asp-controller="Timeline"
|
||||
asp-action="Index"
|
||||
asp-route-projectId="@Model.Project.ProjectID"
|
||||
asp-route-bookId="@Model.SelectedBookID"
|
||||
asp-route-selectedSceneId="@slot.Scene.SceneID"
|
||||
asp-route-ProjectID="@Model.Project.ProjectID"
|
||||
asp-route-BookID="@Model.SelectedBookID"
|
||||
asp-route-SelectedSceneID="@slot.Scene.SceneID"
|
||||
asp-route-FocusType="character"
|
||||
asp-route-FocusID="@appearance.CharacterID"
|
||||
title="@appearance.CharacterName: @appearance.RoleInSceneTypeName / @appearance.PresenceTypeName">
|
||||
@(!string.IsNullOrWhiteSpace(appearance.PresenceTypeName) ? appearance.PresenceTypeName.Substring(0, 1) : "C")
|
||||
</a>
|
||||
@ -291,7 +481,7 @@ else
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="pacing-panel">
|
||||
<section class="pacing-panel" data-timeline-section="metrics">
|
||||
<div class="pacing-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Pacing</p>
|
||||
|
||||
@ -157,9 +157,11 @@
|
||||
<a class="btn btn-outline-primary btn-sm"
|
||||
asp-controller="Timeline"
|
||||
asp-action="Index"
|
||||
asp-route-projectId="@Model.Project.ProjectID"
|
||||
asp-route-bookId="@warning.BookID"
|
||||
asp-route-selectedSceneId="@warning.SceneID">Open scene</a>
|
||||
asp-route-ProjectID="@Model.Project.ProjectID"
|
||||
asp-route-BookID="@warning.BookID"
|
||||
asp-route-SelectedSceneID="@warning.SceneID"
|
||||
asp-route-FocusType="scene"
|
||||
asp-route-FocusID="@warning.SceneID">Open scene</a>
|
||||
}
|
||||
@if (warning.IsDismissed || warning.IsIntentional)
|
||||
{
|
||||
|
||||
@ -1511,6 +1511,117 @@ a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.timeline-control-panel {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
border: 1px solid var(--plotline-line);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: rgba(255, 255, 255, 0.84);
|
||||
box-shadow: 0 10px 24px rgba(55, 48, 39, 0.05);
|
||||
}
|
||||
|
||||
.timeline-filter-drawer summary {
|
||||
color: var(--plotline-ink);
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.timeline-filter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(130px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.timeline-filter-checks,
|
||||
.timeline-filter-actions,
|
||||
.timeline-visibility-panel,
|
||||
.timeline-preset-panel,
|
||||
.preset-chip-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.timeline-filter-checks {
|
||||
grid-column: span 3;
|
||||
color: var(--plotline-muted);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.timeline-filter-actions {
|
||||
grid-column: span 3;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.timeline-visibility-panel label,
|
||||
.preset-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border: 1px solid var(--plotline-line);
|
||||
border-radius: 999px;
|
||||
padding: 4px 8px;
|
||||
color: var(--plotline-muted);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
background: rgba(255, 255, 255, 0.76);
|
||||
}
|
||||
|
||||
.timeline-preset-panel form {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.preset-chip a {
|
||||
color: var(--plotline-accent-dark);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.preset-chip button {
|
||||
border: 0;
|
||||
color: var(--plotline-muted);
|
||||
background: transparent;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.timeline-focus-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
border: 1px solid rgba(47, 111, 99, 0.22);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 16px;
|
||||
padding: 10px 12px;
|
||||
background: rgba(47, 111, 99, 0.08);
|
||||
}
|
||||
|
||||
.timeline-scene-card.filter-dim,
|
||||
.timeline-scene-card.focus-dim,
|
||||
.lane-dim {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.timeline-scene-card.focus-match,
|
||||
.timeline-scene-card.filter-match.focus-match {
|
||||
outline: 3px solid rgba(47, 111, 99, 0.32);
|
||||
box-shadow: 0 12px 26px rgba(47, 111, 99, 0.16);
|
||||
}
|
||||
|
||||
.timeline-section-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.hide-scene-cards .timeline-scene-card {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.analytics-metric-row {
|
||||
display: grid;
|
||||
grid-template-columns: 170px minmax(560px, 1fr);
|
||||
|
||||
@ -280,3 +280,65 @@
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
(() => {
|
||||
const root = document.querySelector("[data-timeline-root]");
|
||||
const controls = document.querySelector("[data-timeline-controls]");
|
||||
if (!root || !controls) {
|
||||
return;
|
||||
}
|
||||
|
||||
const storageKey = "plotline.timeline.visibility";
|
||||
const defaultState = {
|
||||
"scene-cards": true,
|
||||
"plot-lines": true,
|
||||
assets: true,
|
||||
characters: true,
|
||||
locations: true,
|
||||
warnings: true,
|
||||
metrics: true
|
||||
};
|
||||
|
||||
const readState = () => {
|
||||
try {
|
||||
return { ...defaultState, ...JSON.parse(localStorage.getItem(storageKey) || "{}") };
|
||||
} catch {
|
||||
return { ...defaultState };
|
||||
}
|
||||
};
|
||||
|
||||
const applyState = (state) => {
|
||||
Object.entries(state).forEach(([key, visible]) => {
|
||||
root.classList.toggle(`hide-${key}`, !visible);
|
||||
document.querySelectorAll(`[data-timeline-section='${key}']`).forEach((section) => {
|
||||
section.classList.toggle("timeline-section-hidden", !visible);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll("[data-location-marker]").forEach((marker) => {
|
||||
marker.classList.toggle("timeline-section-hidden", !state.locations);
|
||||
});
|
||||
document.querySelectorAll("[data-warning-marker]").forEach((marker) => {
|
||||
marker.classList.toggle("timeline-section-hidden", !state.warnings);
|
||||
});
|
||||
|
||||
document.querySelector("[data-visibility-json]")?.setAttribute("value", JSON.stringify(state));
|
||||
};
|
||||
|
||||
const state = readState();
|
||||
controls.querySelectorAll("[data-timeline-toggle]").forEach((toggle) => {
|
||||
const key = toggle.dataset.timelineToggle;
|
||||
toggle.checked = state[key] ?? true;
|
||||
toggle.addEventListener("change", () => {
|
||||
state[key] = toggle.checked;
|
||||
localStorage.setItem(storageKey, JSON.stringify(state));
|
||||
applyState(state);
|
||||
});
|
||||
});
|
||||
|
||||
controls.querySelector("[data-timeline-preset-form]")?.addEventListener("submit", () => {
|
||||
document.querySelector("[data-visibility-json]")?.setAttribute("value", JSON.stringify(state));
|
||||
});
|
||||
|
||||
applyState(state);
|
||||
})();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user