Timeline UI changes.
This commit is contained in:
parent
305446126e
commit
92aff32155
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTS2/cNhD+K8KcRZkvUaJuxjpFF27iRbJIi94ozigmLFMLiXISGPvfA2o3SNxbLwQx4DfzPYavEN0zQQe3p9O7F4ppgRJS2EqSS8O4YVwcRdvptuNNVSvBTV3/CyWEe/oOHTTae+EtMUWGmEZes14NniGSFVwgl7XOPd2XBbpXcKFCegmeqmn5TPMSpggd/B0iTl/zbBcqP04rVvM00j4uyUWfyXzY7+7ZX7eH48Ph8mqhJYOrgNBBzxVy6QdmOCqmB+9Zq5qaIdaNEo3vNZoLLMREc3RjteDTr/k4pUjJd7KSquJMWjiXgC65TLl3Cx2/nzKLzaK7XC+38nbtXuGFZuhk+dPMS7sbP4ab56Vfw4g3lzO55SmrPM3TieYUaPNkkzmOLoUpFtdJuzFQTFDCw6fiMLo0TPPzG6M+XfTv/4f8Y57+7hv5NRHupjUm6EBoCSW8d/4xRCr2d9ABFyRRtcQGKQzTLTrmamWZMcoMJLxtpIASdlNMIa7TuhT7mOjLvAmADv5w40IX6rezfwyJfFrnrOofk3fhME+4+lT88l/wileK8wxa02lNxUfCMJNPlPUd5/Xa7z8QaeQG2q3zTDEVB5ceiz/d8ggdWNd64ayzzhnvB6o5DRy5M7xBMmgItcQGtdVotOXeaNcLja4W/ZArUALluIttw5TwaBvnGSlEpt3gWK/QMGGUxMYZMTTqjY/Fw5hxzrdcEypmaUCmrXDMaqUYqto10gxNI4ffLLmmL3g2GOnl8lc2BgaxJTSCSS1qpq0VrLV1w/qm7W1vWqp95nxPc6TxN6PeBz9PyzSk4ro8xRvrPq4x//diW6SvIbJvW0Z3k3+iucgZuxDzel9jPZ/P5x8AAAD//wMAk4+m1jYEAAA=
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACoxU227jNhD9FYGvFRXeRIl6S50saqRJjLXRFn0bkaOYjUwZFJXsIsi/F5KTzWVRoC+CMCTnnDlzZp5IgAOShpwfj5cPGNJIcpL8EhJMaMo0ZXzH60bVDauKUiqmZf03yYm/wu+kIZWylluDVKJGqhwraSs7S51Dwxl3TJRqzgl3I2meCPjC4YO3WAzjHxhHPwTSkD99cMPjjA2+sP0wuSIOPa7DmCDYmczNenVFfz/f7G43p1sjjvPjwjvSkJZJx4TtqGZOUtVZS2tZldS5spK8sq1y+vTMh4QxQF+M7v4N3w0pYLKNKIQsGBWGPOfEQYKZcgsj7r4fZxaLRBdzPF/Cy2/zRB4wkkbkr2Ke0p3Z3p8dxnbyvTtbviQnxzgcMSaPixq32+yNBGcFK4QWjJGcXIPd+4DZ+iK77ecSwdZMoZPUYOeoMhyoUVJSJ0uohO6qSnQkJ1cYA/bvsl57G4dx6FL2InL2AWd7vroM0PY4Y3yBfkSSk/W4TRAc9EPAy29op3TKtYvTfLwaQvJhGqYxW4eEdxFezl/fr6YYMaRsA2mf/QbjnjTEQG05GDAA2toOS4Ydcww0qxxqp9Ep4SqnjHJaGWa1gpYrByVvuzlCcvLrrOF2shbH8Y3NYpK+XzhkL31a9R5D+iAjaQjjKJyskXaCa6pqBxRKaajWUnfIrakEf4XZQbzDRBrS1W3bMmtboQy3IAxI1zHpdGeEqCpQnSwrJbQuWQXWVZXgyskKLchOslqQnGzi4CabPrdaLg3YxOEftGmWijREs7KsLbSt0IrXYGpUXKtaG6yM6oApaVvgZQ1tyxmUzJpaWODagGpd2f1Q6TLc+YAXfjz28P0dbl3oQv5iAWregfp0/UuEAz4O8f7mZOPi5nJHcvJ1CvNGyNazRR59oN/0PNHXU5/8bh8RHLrrweHPPsJ5XLJlQq00bctKS23lHFXMKgraIu06KcqulBwX+14M9h5jNjsMfJin6key2212Hu3eJ7RpijO/vxYet9ts00Pqhnj4sEneFfZZgEJIzubOOHw4baOFo3auRqc5FYqXVBnDaW3KirZV3ZpW11jaWbHbKR2nlH1F5yPatFT84sUFc7VHe/+zGK8ueLEoZ7PXtqcttkj7v5bYc04OCOMU8bBs6+aJrEPAuCBfTKdRXIdr3/d+RDsEN5KmVlwVRtfqheF/36tUwStdPj8/P/8LAAD//wMAb1GgshsGAAA=
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACoxUS2/bOBD+KwLPokJKFCUK2IPhtKiRpjESd3fRG0UOYyKyaFCU0yDwf19Qlmun6GFvwmhe32P4jnq5A9SgxX7/6QB9GFCKgp1COck5JhwTuqF1U5KG0IzzvMgp+YFSZO/gDTWoYkpRJQAXwAEzTUrcFkZhrUFQQjXJSxZ7yucBNe9I2kzDwSrI3PA3+MG6HjXoH9tr9xpnS5upzo06866DVT8E2au4zLfV8g5/Xaw3D+tT1gBDLM6sRg3SRcu1AIIZlCVmbcWx4DnBFFrFqlJKWdWnMtsH8L3sskG/XOZrF3oIqsmzvMgIzgU6pkjLIOPKrRxg87aPW0wU3cZ4OoWnz+YdHcCjJk/PZJ7a3ajO3uyGdrSdvgnSP0MwXu7g1fkXOMgOpWjv3R58sDBx8/CUXFaiJCNZznNCUIrupdraHpLVbfLQRcBS1YSBLrAAozETVGLBigLropRVzk1V5Qal6A58D91V13urvBucCclMefJhztL1wfajG4dk1Qd49jKc6j7LbgCUovXYdnbYPoLUbxv3OMZ//dhFLMvRe+hDspZhm3yRwxY1SMhaUSmkkJIrZaAkYIgmkpNKA9ccNMt1pZlgmjNBFGeypUzLkrYmRlCKHsawH8OZ/59XS2y83e1AXzbYTByvOxmM87uVhj5YY6M054zJUF03oUrmnsvOQh8+kIwaRCjkuqgBm5xyzGotsSwLgTkvuAGqRJXTE2HGPo+/eCLGaFaYsq55K1pKIDdVzUhlWqKVKZTQZQtVXpGqhlooQtqS0boVimigJdMRnXd6VOF3KxSTQI9jH68zWUXYr7bHPzm7MLL2LjjluiiY7eDpbQiw+5NqS7fbu8GGiN/M0n4f4N718e98/zNnc/HChUsM4h0k0+mJmkrWmhqXVOSY6aLENSE1NoSxkiopeFGhFN069QI+iQaTtp80OXvq4SlZeLW1AVQYfZz87wTqJOfn88lc+Mi+fdosnYfFfp/O0b8OkaRTs7MBPrwr34eYvnVDuII8Q3uCzpwXu7KThsPpqbIxyLWuQXOKc0ZLzISguBZlhduqbkXLayjVxa7JI2jrQYWp38aPcdrM7J98ufDBGqnCEI/nq1OTnWZ/zim/L3kl26/q0/Ar6WYrzZ0oiZZ9Oj2ck4P+17t5PB6P/wEAAP//AwD7D0aXKAYAAA==
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUTW/jNhCG/4rAs6mQFEWJuqVOFzWy2RibdFv0NiJHDhGZNCgq2yDIf19QdhBnFz30Joz4zsczL/lCPOyRdOTycPj9CX2ayIokt4QEE4oyRRm/521Xs47xUqi6Ebz+h6yIu8Zn0pFGGsONRlqhQiotq2lfDYZai5ozbpmoZc4Ju4l0LwRcafHJGSzD9A3j5IInHfnLeRu+59rgSjOG2ZYxjLjxUwJvcjNfNutr+vlye3+7PZ6acMri0lnSEVv1ympkVGJdU9k3imolGOXYG9nUANC0R5nzCaOHsZzs43t9G5LHZDpRiqpkVGjyuiIWEuSWe5jw/vmQu1gQXeX4agkvn90LecJIOrF6g3lMd2FGd7Gf+tmN9mIMu53zu3Xwg9vNEVIuvCKHGA4Yk8MFzm/OQ3z+HHa7nI98gnFCsiL3GPfOw3j8cYUDzGO6C3NcyEz28b/OkI7AnAJZkYXkOC5li9Mw69GhT79ozwrf3hXbEdIQ4v7Dku6O7Df/A/0NmAfnsdhckY4wjsJWLdJBcEVla4FCXWmqVKUG5EY3gpMVWQefnJ/DPBUbn3B3ovahv8toHlxCk+aYZ/pbZbNtY7CzScX7gjkrWVkxlkVzOsyp+IrWRTQJ8xD3cT7l+0kilFhEn9yIv+BZzzGiT8UW0kPxB0wPpCMaWsNBgwZQxgxYMxyYZaBYY1FZhVYK21ippVVSM6Mk9FxaqHk/5AhZEcwmKxZfa2xQClFTJQWnUgPQvmUVBcaYwmFQCu0HuMXtmHVgWibRVlTjYKnUHKiWVUVtVUMj1NA0YjjjdDIEZ5n6uQn/nNCevPTlaO232S0+He/x0qeytkWrOBWS11RqzWmr64b2TdvrXrVYmzzZOvgpvIH8hrEPk0v5Eblx3u1h/PnI+2auMXocz7Zz40wMUxhScbJl8WFfX2efX7Fiseh35+m/izGugnnEWGRjgfNny3x9fX39AQAA//8DAMAt750MBQAA
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS6Som2G3qOumXsSLtOiN5IxsIVpqQVGbBsb+94DaDRL31psw0DszfOadeSPRHZD05PZ4/OWEMS+kJnncQoIJTZmmjO9517esZ7zh1mgm5T+kJuMjfiU9MSoEHixSiRqpAtZSL4dAAdByxoGJVpWc7mUh/RtxYwN4GgM28/IJ0zLOkfTkrzHC/KXUdmMTpnmFJs0TPsQluxhKM38+3D3SP253+6fd5a8FlyJuRiA9Aek1WGRUYdtS5Y2mVgtGOfqgTOucM91FNsaMKbqpWeDzj/ow54g59KIRsmFUWHKuCbjsSsveLbj/eixdbIjuS7zewttn/0ZOmEgv6u8wL+luwjTe5Pk44Qmno0sLppswHw4uAqnJMc1HTHnEDcv20mlyeZxjdS12N40YM6nJ03O1m1we5nR4x+r5guDhfxA4YfKkJ5K1UhuDSkrprPaCIRgdtBY+OGYGL5AHZK1VxnLDuLSCWdlq0xlwzAZSk5dp9m76fdn4WcF8B8wILVChFUprGxjKDofWIAOntdddsCA7I1QHnXTWOW49mmB1KAb54MLrGLF6uCc9YRwFyA7pILimqgNHXSst1VrqAXmwRnBSk7s55jGu87pUDzHjS9r4kZ786qYFL+RuU3gdM4a8pgL1b12K7dIMa8jVDwdw1rBGMlZEaz6uufqIMCYMGQvefVqv+f4jEVpsors1JYy52rn8Wv3mltdCxXWBO1ueqkMYsGU4MGBOMwOoQSMoAQaUVaCVZUEr57kC13I/lAipCRbDVZvHmWfDgIhU29ZSZb2gvms1tQDAEGHwSr/jWD1NRedCxxSCpBYHoMpyR62SkoJsnRF6MEYMPyG5mo+zAhjwdNnWrQMN0CFoToXiLVXWctrZ1lBvOm+97rANpedHTBGnn0B9GEOal3nI1dW71Tt0H9dYLk61+fjLGOm/24zu5/AZU1Vm7MZYFuw61nNNDuiWNeFhO1j9G9mV7ar2290SupFc6Jo8Z5fyeryGpW2U7OT5fD5/AwAA//8DAD65h/X1BAAA
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUT2/cNhDFv4rAsyjzP0XdDLtFDTfNIjbaorcRZxQL0UoLito0MPzdC2o3SN1bb8JAb/j4mzd8ZTMciXXs9nT66UxzXlnN8riXlFCOC8eFfJZtZ0UnZKO0dVqrv1jNxkf6xjrmTYwyBuKaHHGDwvJeD5EjUpBColDWlJ7weWXdK4OxQTqPkZpl/Z3SOi4z69gf44zL13I2jE2clg2btEz0MK8Z5ljM/PZw98h/vT08fzxc/lppLeJmRNYx1L3DQIIbspab3jsenBJcUh+NtwDg24tsnDOlGaZmxS8/zsclz5RjpxqlG8FVYG81Q8hQLPew0vO3U3GxI7ov9Xov75/dKztTYp2qv8O8tLuJ03izbv1EZ5pOkFZKN3E5HmFGVrNTWk6U8kg7ljOlflnHXIjG6KFtpcQWvLEeW22V9qgFYSu8xYABnHGDitYO2LfBm8GgkgYdGdN7VrMd3DRBHpe5unq/m0aaM6vZx6fqMEEelnR8h/7pQvThfwAttlnHtLDaeU9Gaw3B9UoQehedU30E4YdekYwkbDA+SC+kDkoEbZ1vPYIIkdXsA8SXcabq4Z51TEhSqFvig5KOmxaBg9WBO6fdQDIGrySr2d0y53Helm2tHuZMn9N+X9axn2Fa6XLT2xRfxkwxb6lA+NOVMB7SglvM1Y8ASNGIRgtRRFs+bbn6RDgmipkKjue0Xfv9R6Kc2kV3W0o05+oA+aX6BdYX1rEAbZQQIAC4GAeyggaBApzwSA4doVHo0QSDzgQRnYFeGgQr+6FUWM2o5K3aIz5I25P1hg8oem6U7zkEDBwwDLYkJPbDO47Vx6noILbCEGoeaEBuggQejNYctQWv3OC9KrrvSK5hkaIARjpflnV34BBbQie5MtJyE4LkbbCe977tQ+9asrF4fqQ00/QvUB/GmJZ1GXJ1zVr1Dt2nbS4PTrXn7us487/3Gd0v8QulqswYxrns13WsbzU7EqxbouP+XnWv7FCWq3reny3lGi2Vq9lThpS307WsQ2N0q9/e3t7+AQAA//8DAI0J9d70BAAA
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTTW/cNhD9K8KcRZmkKErUzVin6MJNvEgWadEbxRnFhGVqQVFOAmP/e0DtBk5664UgBnwz72P4CsE+E/Rwezq9e6GQFigh+a0kudSMa8bFUXR9w3veVXXHjebiXyjB39N36KFVzglniNWkiSnkDRvq0TFEMoIL5LJRuaf9skD/CtZXSC/eUTUvnykufg7Qw98+4Pw1z7a+ctO8YhXnifZhSTa4TObDfnfP/ro9HB8Ol1cLLRlceYQesB40GuJMUdMwNbSaGS05EzQ41TbW2ra7wHxIFIOdqgWf3ubjnAIl18tK1hVn0sC5BLTJZsqDXej4/ZRZbBbd5Xq5lbdr/wovFKGX5U8zL+1u3ORvnpdh9RPeXM5kl6es8hTnE8XkafNkkzlNNvk5FNdJu8lTSFDCw6fiMNk0zvH5N6M+XfTv/4f8Y57+7hu5NRHu5jUk6EHUBkp4b92jD1Ts76AHLkhi3REbpdBMdWiZbWrDtK71SMKZVgooYTeH5MM6r0uxD4m+xE0A9PCHnRa6UL+N7tEncmmNWdU/Ou/CIc64ulS8+S94xaua8wxa02lNxUdCH8klyvqOcb32+w9EarmBdmuMFFJxsOmx+NMuj9CDsZ0T1lhjrXZupIbTyJFbzVskjZpQSWxRGYVaGe60soNQaBsxjLkCJVCOu9g2bFCmUUYaJlupmHLcMYOETKtWtUrJpu2ytDcfi4cp46zruCKsmaERmTLCMqPqmmHd2FbqsW3l+Isl1/QFzwYjvVz+ysZAI3aEWjCpRMOUMYJ1pmnZ0HaDGXRHjcuc7ykGmn4x6r13cV7mMRXX5Sl+s+7jGvJ/L7ZF+uoD+7ZldDe7J4pFztj6kNf7Guv5fD7/AAAA//8DADYi/1M2BAAA
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRUwW7cNhD9FWHOokxKFCXqZqwTdOGmXiSLtOhtRI5iwjK1ICknhrH/XlC7ReJT0QtBjPiGb9571Bt4fCYY4PZ0+vBCPkUoIbmtVPNaMa4YF0fRDy0feF81Xd8o0fwNJbh7eoUBOmmMMJpYQ4qYtLxlYzMZZi1pwYXldStzT/wWYXgDdJWlF2eoWuJXCtEtHgb403m7fM93o6vMvKy2CstMex8TepPJ/LHf3bPfbw/Hh8PlVKSYwZWzMIBtRmU1cSapbZkcO8W0qjkTNBrZtYjY9ReY84mCx7mK9unn/XZJnpIZ6qpuKs5qDecSLCbMlEeMdHw9ZRabRHe5Xm7lbTu8wQsFGOryXzEv7W7M7G6e47i62d5c1oTxKW7rhCYt4RVKOIXlRCE52vS5jZGex/n1iPHp4+VM3sYPP8isiexuWX2CAUSjoIRNoHnG5BZfXDnuZkc+bR9TcD4681+9Gijh4UtxmDFNS3h+58eXi8z7/6HyJzSPzlOxv4MBuKDaNj2xqRaKyd4iw7bRTKlGTSSM7moBJewWn5xflzUWe5/oW9hGggE+4hzpwu82mEeXyKQ15Dn/UjlXh7DY1aTip5eCV7xqOM+gNZ3WVHwm6wKZRHmIY1iv/b6+h9Sq3kC7NQTyqThgeix+w/gIA2jsjUCNGlEZM1HLaeKWo+KdJWUVWVnbzkotrZKaGyVxFNJiK8YpV6AEytEptrRKO8mubg3rOimYxNYw5NgzoaVRvBsnqd7rWDzMGYem55JswzRNlkktkGnZNMw2LXa1mrqunn6R5JoHwbPAll4u725joKztySrBailaJrUWrNdtx8auH/WoempN5nxPwdP8i1CfnAlLXKZUXBNSvJPu8+rzv6PY0vLdefZj8+huMU8UiuwxOp+fytXW8/l8/gcAAP//AwCpOTG6ggQAAA==
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTwW7bOBD9FYFnUSEpihJ1C5xd1Ei7MdqgLXobcUY1UYUyKCptEfjfF5RdpN3b3ohHvpk37w1fWIAnYj27PZ3+eqaQFlay5DdICWW4MFzIR9n1jehFV9VdrVujvrCS+Xv6yXrWaueks8RrMsQ1ioYP9eg4IlkpJArV6FwTvi6sf2HgK6Rn76ial48UFz8H1rNPPuD8PfcGX7lpXrGK80T7sCQILov5Z7+7529vD48Ph8urhZZMrjyynmE9GLQkuKam4XpoDbdGCS5pcLptAKDtLjQfEsUAU7Xgt9f+OKdAyfWqUnUluLLsXDKEBFnyAAs9/jxlFZtFdxkvN3g79i/smSLrVfnLzEu5Gzf5Gzc/PUHAm9EHvxxZyU5xPlFMnjY7tgmnCZKfQ3Ftsps8hcRK9vChOEyQxjk+/eHRh8vo+/8x+TtwRx+o2N+xnglJCuuO+Kik4bpD4NDUlhtTm5Gks62SrGS7OSQf1nldin1I9DVuKlnP/oZpoYu+2+iOPpFLa8zSP5ucNf3waTdjBgQr2SHOuLpUvNotRSWqWuTLhzWd1lS8J/SRXKI802Ncr+X/Q1FGbaTdGiOFVBwgHYs3sBxZzyx0ToIFC2CcG6kRNAoUYESLZNAQaoUtaqvRaCuc0TBIjdDIYcxI1p3TLbaFasmJsQXiaCxyTfXAO20sh1aia5oRNGSJr7YWD1PmgeuEJqy5pRG5thK41XXNsW6gVWZsWzX+Zsk1cSmy30jPl6+xKTCIHaGRXGnZcG2t5J1tWj603WAH01HjsuZ7ioGm34x6512cl3lMxXVhij+se7+G/L2LbXm++8B/bJHdze4bxSJHDj7kbb6mfD6fz/8CAAD//wMA1TddHSUEAAA=
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRUTW/bOhD8KwLPpsIvUaJugdPiGXltjNZoi94o7qoholAGSaUNAv/3grKLNKeHdyGIFWc5OzPUCwn2EUlPro/Hd08YciIbkv1aEkxoyjRl4sBN38iet7XkmstWfCcb4m/xmfSkVc5xZ5BK1EgVsIYOcnQUAA1nHJhoVOlpfyTSvxDra8An77Ce0xeMyc+B9OSrDzD/LHdbX7tpXqCO84S7kLINrpD5uNve0n+v94e7/flUwlTAtYdCFUTHJHR0YK2iCnVLDQdFXWMG7Zi0QpgzzIeMMdipTvDwej/MOWB2vaiFrBkVhpw2BGy2hfJgEx6ej4XFKtFNqW/W8rrtX8gTRtKLzR8xz+2u3OSvHtOw+Amuzmu26SGt62hdnuMz2ZBjnI8Ys8dVn+uU8HGYng82Pbw/nynb9O4XuiUjbOclZNITrjjZkFWgabLZz6G6cNxOHkNeP+boQ/Luv3oVf+4+V/vJ5nGOj2/8+HyWefc/VP5g3b0PWO1uSE8YRwGyQzoKrqnqwFLbSEO1lnpE7kwryiDbOWQflnlJ1S5k/BHXkUhP3tsp4ZnfdXT3PqPLSyxzftOF9z7OsLhcvXrJWc1qyVgBLfm45OoTgo/oMpYhDnG59PvyFiK0WEHbJUYMudrbfF/9Y9M96YmxnePWWGOtdm7EhuHIgFnNWkANGkEJaEEZBVoZ5rSyA1dgGz6MpUI2BEt0qjWtbByN0cxRK1tGVdc6OnSOU9N2nInOgjAlYK86VndTwVnXMYUgqcERqDLcUqOkpCAb2wo9tq0Y/5LkkgfOisCAT+d3tzLQAB2C5lQo3lBlDKedaVo6tN1gBt1h4wrnW4wBp7+E+uBdnNM85uqSkOqNdJ+WUP4d1ZqWnz7QX6tHN7N7wFgVj60P5alcbD2dTqffAAAA//8DAH1Y7jeCBAAA
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSTTW/cOAyG/4qhs+Xoy7LlWzDZRQdpN4M2aIveaJHuCHXkgS2nLYL57wt5pki7t70JlEg+fF/qhUV4Itax29Ppr2eKaWElS2ELKaEsF5YL9ShdV+tONpVW2igjv7CShXv6yTrWGO+ld8Q1WeIGRc17PXiOSE4KiULVJteErwvrXhiECuk5eKqm5SPNS5gi69inEHH6nntDqPw4rVjN00j7uCSIPsP8s9/d87e3h8eHw+XVQktOrgJmVFSt0NjyXjSGG7INdxIN97XrrRcalHKXtBATzRHGasFvr/1xSpGS71SldCW4cuxcMoQEGbmHhR5/njLFJtFdjpdbeDt2L+yZZtap8peYl3I3fgw3fnp6gog3Q4hhObKSnebpRHMKtMmxTTiOkMIUi2uT3RgoJlayhw/FYYQ0TPPTHxp9uIy+/x+TvwN/DJGK/R3rmJCkULfEByUtNy0Ch1o7bq22A0nvGiVZyXZTTCGu07oU+5jo67xRso79DeNCF77b2R9DIp/WOaN/ttlr+hHSbsIcEKxkh3nC1afiVW4pKlFpkS8f1nRaU/GeMMzkE+WZHuf1Wv4/KcqqLWm3zjPFVBwgHYs3sBxZxxy0XoIDB2C9H6gWNAgUYEWDZNESGoUNGmfQGie8NdBLg1DLfsiRzJ3dLbaFGtABoTC8h6blpjaSO6uBSwutVVobkBnkVdbiYcx54FthCDV3NCA3TgJ3RmuOuoZG2aFp1PCbJFfHpch6Iz1fvsZGYBFbQiu5MrLmxjnJW1c3vG/a3vW2pdpn5nuaI42/CfUu+HlapiEV14Up/pDu/Rrz9y625fkeIv+xWXY3+W80F9lyCDFv89Xl8/l8/hcAAP//AwCN39GdJQQAAA==
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS6Som2G3qOGmWcRGW/Q25IxiIVppQVGbBob/e0HtFol7640YcIYvn3lnXtkMR2I9uz2dfjrTnFdWszzuISWU5cJyoZ6l71vXy7ZxznSik3+xmo2P9I31zJkYZfTENVniBkXLgx4iRyQvhUShWlNqwueV9a8MxgbpPEZqlvV3Suu4zKxnf4wzLl/L2zA2cVo2bNIy0cO8ZphjEfPbw90j//X28PzxcLm10lqSmxFZz8AHMN1gOanguAEgHlwAThGJWgKpTbykjXOmNMPUrPjl+/u45Jly7FWjdCO48uytZggZiuQAKz1/OxUVO6L7Eq/38H7sX9mZEutV/S/MS7mbOI036xYmOtN0grRSuonL8Qgzspqd0nKilEfasZwphWUdcyEao4OukxI7cKZ12OlWaYdaEHbCtejRgzV2ULFtBwydd2YwqKRBS8YEx2q2g5smyOMyV1ftd9NIc2Y1+/hUHSbIw5KO79A/XYg+/A+gRTbrmRatts6R0VqDt0EJQmejtSpEEG4IimQk0XrjvHRCaq+E1611nUMQvlT6APFlnKl6uGc9E5IU6o74oKTlpkPg0GrPrdV2IBm9U5LV7G6Z8zhvy7ZWD3Omz2n/L+vZzzCtdPnpbYovY6aYt1Qg/GmLGQ9pwS3m6rsBpGhEo4UoSVs+bbn6RDgmipkKjue0Xev9J0VZtSfdbSnRnKsD5JfqF1hfWM88dFGCBw9gYxyoFTQIFGCFQ7JoCY1Ch8YbtMaLaA0EaRBaGYYSYTWj4rdqtziBH2hAy8Fp4qZTinchOC6UNcHpIAWGdxyrj9PeydgJQ6i5pwG58RK4N1pz1C04ZQfn1PADkqtZpCiAkc6XYd0VWMSO0EqujGy58V7yzreOB9cFH2xHbSyaHynNNP0A6sMY07IuQ66uXqveofu0zWXhVLvvvo4z/3vv0f0Sv1CqSo9hnMt8Xdv6VrMjwbolOu77qn9lhzJc1fO+trRopNK6Zk8ZUt5O17DVjZGqfXt7e/sHAAD//wMAlFq6YfQEAAA=
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS6Som2G3qOumXsSLtOhtSI5sIVpqQVGbBsb+94DaDRL31psw0DszfOadeSMRDkh6cns8/nLCmBdSkzxuIcGEpkxTJvbc9q3pedsYqXkn+T+kJuMjfiU9Mcp77i1SiRqpCqylTg6ehoCWMx6YaFXJCS8L6d8IjE3A0+ixmZdPmJZxjqQnf40xzF9KbRgbP81raNI84UNcMkRfmvnz4e6R/nG72z/tLn8tuBRxMwbSE7AOVDdoisIZqgCQOuOAog+ILQKXyl9kY8yYIkzNEj7/qB/mHDH7XjRCNowKS841CZChtOxgwf3XY+liQ3Rf4vUW3j77N3LCRHpRf4d5SXfjp/Emz8cJTzgdIS2Ybvx8OEAMpCbHNB8x5RE3LNtLpwnyOMfqWuxuGjFmUpOn52o3QR7mdHjH6vmC4OF/EDhhcqQnkrVSG4NKSglWO8EwGO21Fs4DM4MTyD2y1ipjuWFcWsGsbLXpTABmS6aXaXYw/b5s/KxgrgvMCC1QoRVKa+sZyg6H1iALoLXTnbdBdkaoLnQSLAC3Do232heDfAD/OkasHu5JTxhHEWSHdBBcU9UFoNBKS7WWekDurRGc1ORujnmM67wu1UPM+JI2fqQnv8K04IXcbfKvY0af11Sg/q1LsV2aw+pz9cMBnDWskYwV0ZqPa64+YhgT+owF7z6t13z/kQgtNtHdmhLGXO0gv1a/wfJaqEDnOdjyVO39gC3DgQUGmpmAOmgMSgQTlFVBK8u8VuC4CtByN5QIqQkWw1WbxwdoUXZgqJC+parVnlqwnHZdkIDGO8b9O47V07Q5w3dMYZDU4hCoshyoVVLSIFswQg/GiOEnJFfzcVYABzxdtnXrQIfQYdCcCsVbqmwpbltDnemcdbrD1peeHzFFnH4C9WH0aV7mIVdX71bv0H1cY7k41ebjL2Ok/24zup/9Z0xVmTGMsSzYdaznmhwQljXhYTtY/RvZle2q9tvdkqzhQsqaPGdIeT1ew1o2iov2fD6fvwEAAP//AwBUXko/9QQAAA==
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUzW7jNhSFX0Xg2lRIiqJE7VKngxqZTIxJOi26uyKvHCIyaVBUpkGQdx9QdhBnBl10J1zx/n3nkC/Ewx5JRy4Ph9+f0KeJrEhyS0gwoShTlIl7rru66XhdtqzmrFL/kBVx1/hMOtJIY7jRSCtUSKVlNe2rwVBrUXPGLRO1zDVhN5HuhYArLT45g2WYvmGcXPCkI385b8P33BtcacYw2zKGETd+SuBNHubLZn1NP19u72+3x1MTTjm5dJZ0BHQPsh0URdE3VAIg7ZseKBqLWCPwSppjmvMJo4exnOzje38bksdkOlGKqmRUaPK6IhYS5JF7mPD++ZCnWBBd5fhqCS+f3Qt5wkg6sXqDeSx3YUZ3sZ/62Y32Ygy7nfO7dfCD280RUm68IocYDhiTwwXOb85DfP4cdrtcj3yCcUKyIvcY987DePxxhQPMY7oLc1zITPbxv85kMnMKZEUWkuO4tC1Oy6xHhz79knvW+Pau2I6QhhD3H0S6O7Lf/A/0N2AenMdic0U6wjgKW7VIB8EVla0FCnWlqVKVGpAb3QhOVmQdfHJ+DvNUbHzC3Ynah/kuo3lwCU2aY97pb5XNto3BziYV7wJzVrKyYiwnzekwp+IrWhfRJMxL3Mf5VO+nFKHEkvTJjfgLnvUcI/pUbCE9FH/A9EA6oqE1HDRoAGXMgDXDgVkGijUWlVVopbCNlVpaJTUzSkLPpYWa90OOkBXBbLJi8TUfBGhAQ3mLNZVVjxS0MLSqGylkpfUwDB/gFrfjIoppmURbUY2DpVJzoFpWFbVVDY1QQ9OI4YzTyRCcZernJvxzQnvy0pejtd92t/h0vMfLnMraFq3iVEheU6k1p62uG9o3ba971WJt8mbr4KfwBvIbxj5MLuVH5MZ5t4fx5yPvylxj9DieqXPjTAxTGFJxsmXxQa+vs8+vWLFY9Lvz9N/FGFfBPGIssrHA+TMxX19fX38AAAD//wMAr++TXgwFAAA=
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACoxUTW/bOBD9KwLPokJS1AcF7MFwWqyRpjESd3exN4ocxkRk0aAop0GR/76gJNdO0cPehNF8vHnvDX+gXh4ANWh1PH46QR8GlKJgpxAjrMSkxITtqGiKqqFlxipOKaP/ohTZO3hDDaq4UlQJwDmUgLkmBW5zo7DWICihmrCCx57yeUDNDyRtpuFkFWRu+Av8YF2PGvS37bV7jbOlzVTnRp1518GmH4LsVQTzdbO+w19W293Dds4aYIjFmdWoQVK0ktemxMDaCnMpAbdVKzEoDVCApDlXc5ntA/hedtmgXy7ztQs9BNWwjOUZwUyg9xRpGWSE3MoBdm/HiGKi6DbG0yk8fTY/0Ak8alh6JnNud6M6e3MY2tF2+iZI/wzBeHmAV+df4CQ7lKKjd0fwwcLEzcNTcoFESUYyVjJCUIrupdrbHpLNbfLQTQurmnDQORZgNOaCSix4nmOdF7JipakqZlCK7sD30F11vbfKu8GZkCyUJx/mrF0fbD+6cUg2fYBnL8Nc91l2A6AUbce2s8P+EaR+27nHMf7rxy7ush69hz4kWxn2yZ9y2KMGCVkrKoUUUpZKGSgIGKKJLEmlodQlaM50pbnguuSCqJLLlnItC9qaGEEpehjDcQxn/r9fgdh5eziAviDYTRxvOxmM84eNhj5YY6M054zJUF03bZUsPdedhT58IBk1iFBgOq8BG0ZLzGstsSxygcsyLw1QJSpGZ8KMfR5/8kSM0Tw3RV2XrWgpAWaqmpPKtEQrkyuhixYqVpGqhlooQtqC07oVimigBddxO+/0qMKvVsgngR7HPl5nsolrv9oefy/5hZGtd8Ep10XBbAdPb0OAw+9UW7vD0Q02xP3NIu23Ae5dH/8u979wthSvXLjEIN5BMp2eriSYghdYVBIwNzVgCdzgGog2NSeEqAKl6NapF/BJNJi0/aTJ2VMPT8nKq70NoMLo4+R/pqVmOT+fT+bCR/b1027tPKyOx3SJ/nGKJM3Nzgb48K58G2L63g1xi+DHKx89QWfOuK7cpOE0v1Q2Bkuta9AlxYzTAnMhKK5FUeG2qlvRljUU6uLW5BG09aDC1G83T1uI/Z0tVz5YI1UY4u18cWpy02LPJeVXkFeq/ayeh18ptzhp6URJdOzT/G5OBvpfz+b7+/v7fwAAAP//AwB0fDGlJwYAAA==
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTTW/cNhD9K8KcRZmkKErUzVin6MJNvEgWadHbiBzFhGVqQVFOAmP/e0DtBk5664UgBnwz72P4CgGfCXq4PZ3evVBIC5SQ/FaSXGrGNePyKEzftL2sq7btlNT1v1CCv6fv0EOrrBXWEKtJE1OON2yoR8ucIyO4cFw2KvfELwv0r4C+cvTiLVXz8pni4ucAPfztg5u/5tnoKzvNq6viPNE+LAmDzWQ+7Hf37K/bw/HhcHm10JLBlXfQA5oBVTdqRnJomUIkNrQDMrKOqCEUtbIXmA+JYsCpWtzT23w3p0DJ9rKSdcWZNHAuwWHCTHnAhY7fT5nFZtFdrpdbebv2r/BCEXpZ/jTz0u7GTv7meRlWP7mby5lwecoqT3E+UUyeNk82mdOEyc+huE7aTZ5CghIePhWHCdM4x+ffjPp00b//H/KPefq7b2TXRG43ryFBD0I1UMJ7tI8+ULG/gx64IOnqjtgohWaqc8iwqQ3TutYjCWtaKaCE3RySD+u8LsU+JPoSNwHQwx84LXShfhvto09k0xqzqn903oVDnN1qU/Hmv+AVr2rOM2hNpzUVH8n5SDZR1neM67XffyBSyw20W2OkkIoDpsfiT1weoQeDnRVo0CBqa0dqOI3ccdS8daSdJqeka50yymlluNUKB6EcNmIYcwVKoBx3sW2YNkRcaMtka5EprQc2KNuxQQ1167BrrMzS3nwsHqYtGttxRa5mhkbHlBHIjKpr5uoGW6nHtpXjL5Zc0xc8G+zo5fJXLgyc68hpwaQSDVPGCNaZpmVD2w1m0B01NnO+pxho+sWo997GeZnHVFyXp/jNuo9ryP+92Bbpqw/s25bR3WyfKBY5Y/Qhr/c11vP5fP4BAAD//wMA//ibKjYEAAA=
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTwW7bOBD9FYFnUSEpihJ1C5xd1Ei7MdqgLXobcUY1UYUyKCptEfjfF5RdpN3b3ohHvpk37w1fWIAnYj27PZ3+eqaQFlay5DdICWW4MFyoR2n7pu1VXbWd6FohvrCS+Xv6yXrWaueks8RrMsQ1ioYP9eg4IlkpJArV6FwTvi6sf2HgK6Rn76ial48UFz8H1rNPPuD8PfcGX7lpXrGK80T7sCQILov5Z7+7529vD48Ph8urhZZMrjyynoEdQHej4aSGlmsA4kM7ACeHRA2BrLW70HxIFANM1YLfXvvjnAIl16tK1ZXgyrJzyRASZMkDLPT485RVbBbdZbzc4O3Yv7BniqxX5S8zL+Vu3ORv3Pz0BAFvRh/8cmQlO8X5RDF52uzYJpwmSH4OxbXJbvIUEivZw4fiMEEa5/j0h0cfLqPv/8fk78AdfaBif8d6JiQprDvio5KG6w6BQ1NbbkxtRpLOtkqyku3mkHxY53Up9iHR17ipZD37G6aFLvpuozv6RC6tMUv/bHLW9MOn3YwZEKxkhzjj6lLxarcUlahqkS8f1nRaU/Ge0EdyifJMj3G9lv8PRRm1kXZrjBRScYB0LN7AcmQ9s9A5CRYsgHFupEbQKFCAES2SQUOoFbaorUajrXBGwyA1QiOHMSNZd0632BYKjdC21cglNQPXUozcggROgMKOrXCmzYxXW4uHaYvDdUIT1tzSiFxbCdzquuZYN9AqM7atGn+z5Jq4FNlvpOfL19gUGMSO0EiutGy4tlbyzjYtH9pusIPpqHFZwT3FQNNvRr3zLs7LPKbiujDFH9a9X0P+3sW2PN994D+2yO5m941ikSMHH/I2X1M+n8/nfwEAAP//AwAfRvRKJQQAAA==
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUwW7cNhCGX0XgeSmTFEWJuhl2gi7c1IvESIveRpxRTFimFiTlxDD23Qtqt0h8KnohiBFn+M/3D/XGAjwTG9j18fjhhUJObMey30JKKMOF4UI9SDu03aCauuta06j2b7Zj/o5e2cA67Zx0lnhDhrhG0fKxmRxHJCuFRKFaXWrCt8SGNwa+RnrxjuolfaWY/BLYwP70AZfv5W7wtZuXFeu4zLQPKUNwRcwf+5s7/vv14eH+cD6VKJXk2iMbGNgRdD8ZTmrsuAYgPnYjcHJI1BLIRrtzmg+ZYoC5Tvj0835ccqDsBlWrphZcWXbaMYQMRfIIiR5ej0XFhui2xHdbeNsOb+yFIhvU7l+Y53JXbvZXz2lc/YxX5zVDekrbOoHLS3xlO3aMy5Fi9rTxuU6Jnsf59QHS08fzmbJNH36QWzPhzbKGzAYmtWQ7tgGaZ8h+CdVF483sKeTtY44+JO/+q1bx5/5LdZghT0t8fufHlzPm/f+g/Ancow9U7W/ZwIQkhU1PfFLScN0jcGgby41pzETS2U6VRm6WkH1YlzVV+5DpW9xaYgP7CHOis77r6B59JpfXWPr8yxTdh7jg6nL100spalE3QpSkNR/XXH0m9JFcptLEQ1wv9b6+T1FGbUk3a4wUcnWA/Fj9BumRDcxC7yRYsADGuYlaQZNAAUZ0SAYNoVbYobYajbbCGQ2j1AitHKcSYTtGZXSqbVrbroHWTJr3I3VcS7QcJBreCbI4WWnHtn/HsbqfN/6uF5qw4ZYm5NpK4FY3DcemhU6ZqevU9AuSyzxIUQAjvZzf3abAIPaERnKlZcu1tZL3tu342PWjHU1PrSua7ygGmn8B9cm7uKRlytVlQqp36D6vofw7qm1avvvAf2we3S7uiWJVPAYfylO52Ho6nU7/AAAA//8DAIN+pxyCBAAA
|
||||
@ -95,10 +95,10 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
||||
AgeReferenceSceneID = (int?)null,
|
||||
Height = (string?)null,
|
||||
EyeColour = (string?)null,
|
||||
CharacterImportance = NormalizeCharacterImportance(character.Role, character.Importance),
|
||||
DefaultDescription = CombineText(
|
||||
character.Description,
|
||||
character.Notes,
|
||||
CharacterImportMetadata(character),
|
||||
ImportMarker)
|
||||
},
|
||||
transaction,
|
||||
@ -658,12 +658,6 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
||||
MetadataLine("Package description", package.Description)) ?? ImportMarker;
|
||||
}
|
||||
|
||||
private static string? CharacterImportMetadata(ImportCharacterDto character)
|
||||
{
|
||||
var role = NormalizeCharacterRole(character.Role, character.Importance);
|
||||
return role is null ? null : $"Imported character classification: {role}";
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> GetSkippedOptionalFields(PlotLineImportPackage package)
|
||||
{
|
||||
var skipped = new List<string>();
|
||||
@ -1059,7 +1053,7 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
||||
: lookupIds.ThreadEventTypeIds.Values.First();
|
||||
}
|
||||
|
||||
private static string? NormalizeCharacterRole(string? role, int? importance)
|
||||
private static int? NormalizeCharacterImportance(string? role, int? importance)
|
||||
{
|
||||
var normalized = Clean(role).ToLowerInvariant();
|
||||
if (normalized.Contains("protagonist", StringComparison.OrdinalIgnoreCase)
|
||||
@ -1068,33 +1062,33 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
||||
|| normalized.Contains("lead", StringComparison.OrdinalIgnoreCase)
|
||||
|| normalized.Contains("antagonist", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "Major";
|
||||
return 10;
|
||||
}
|
||||
|
||||
if (normalized.Contains("minor", StringComparison.OrdinalIgnoreCase)
|
||||
|| normalized.Contains("background", StringComparison.OrdinalIgnoreCase)
|
||||
|| normalized.Contains("cameo", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "Minor";
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (normalized.Contains("support", StringComparison.OrdinalIgnoreCase)
|
||||
|| normalized.Contains("secondary", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "Supporting";
|
||||
return 6;
|
||||
}
|
||||
|
||||
if (importance >= 8)
|
||||
{
|
||||
return "Major";
|
||||
return 10;
|
||||
}
|
||||
|
||||
if (importance >= 5)
|
||||
{
|
||||
return "Supporting";
|
||||
return 6;
|
||||
}
|
||||
|
||||
return importance.HasValue ? "Minor" : null;
|
||||
return importance.HasValue ? 2 : null;
|
||||
}
|
||||
|
||||
private static int ResolveSceneDependencyTypeId(ImportLookupIds lookupIds, string? dependencyType)
|
||||
|
||||
@ -1816,6 +1816,7 @@ public sealed class CharacterRepository(ISqlConnectionFactory connectionFactory)
|
||||
character.AgeReferenceSceneID,
|
||||
character.Height,
|
||||
character.EyeColour,
|
||||
character.CharacterImportance,
|
||||
character.DefaultDescription
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
@ -467,6 +467,7 @@ public sealed class Character
|
||||
public int? AgeReferenceSceneID { get; set; }
|
||||
public string? Height { get; set; }
|
||||
public string? EyeColour { get; set; }
|
||||
public int? CharacterImportance { get; set; }
|
||||
public string? DefaultDescription { get; set; }
|
||||
public DateTime CreatedDate { get; set; }
|
||||
public DateTime UpdatedDate { get; set; }
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace PlotLine.Models;
|
||||
@ -52,6 +53,7 @@ public sealed class ImportCharacterDto
|
||||
public string? DisplayName { get; set; }
|
||||
public string? ShortName { get; set; }
|
||||
public string? Role { get; set; }
|
||||
[JsonConverter(typeof(ImportImportanceJsonConverter))]
|
||||
public int? Importance { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
@ -299,3 +301,65 @@ public sealed record ImportResult
|
||||
public string? TechnicalDetail { get; init; }
|
||||
public string Message { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class ImportImportanceJsonConverter : JsonConverter<int?>
|
||||
{
|
||||
public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (reader.TokenType == JsonTokenType.Number && reader.TryGetInt32(out var numeric))
|
||||
{
|
||||
return Math.Clamp(numeric, 1, 10);
|
||||
}
|
||||
|
||||
if (reader.TokenType != JsonTokenType.String)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var text = reader.GetString()?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (int.TryParse(text, out var parsed))
|
||||
{
|
||||
return Math.Clamp(parsed, 1, 10);
|
||||
}
|
||||
|
||||
var normalized = text.ToLowerInvariant();
|
||||
if (normalized.Contains("major") || normalized.Contains("main") || normalized.Contains("protagonist"))
|
||||
{
|
||||
return 10;
|
||||
}
|
||||
|
||||
if (normalized.Contains("support") || normalized.Contains("secondary"))
|
||||
{
|
||||
return 6;
|
||||
}
|
||||
|
||||
if (normalized.Contains("minor") || normalized.Contains("background") || normalized.Contains("cameo"))
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value.HasValue)
|
||||
{
|
||||
writer.WriteNumberValue(value.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1349,9 +1349,8 @@ public sealed class TimelineService(
|
||||
characterGroup => characterGroup.GroupBy(x => x.SceneID).ToDictionary(sceneGroup => sceneGroup.Key, sceneGroup => sceneGroup.ToList()));
|
||||
|
||||
return characters
|
||||
// Character has no global category/importance column. Timeline grouping uses lane order:
|
||||
// the first six lanes render as "Major characters", so imported classification metadata
|
||||
// and strong scene roles should sort likely main characters ahead of supporting cast.
|
||||
// Timeline grouping prefers CharacterImportance when present; imported classification
|
||||
// metadata and strong scene roles remain as fallbacks for older projects.
|
||||
.OrderByDescending(character => CharacterLanePriority(character, appearancesByCharacterAndScene.TryGetValue(character.CharacterID, out var sceneAppearances) ? sceneAppearances.Values.SelectMany(x => x) : []))
|
||||
.ThenBy(x => x.CharacterName)
|
||||
.Select(character =>
|
||||
@ -1374,9 +1373,22 @@ public sealed class TimelineService(
|
||||
private static int CharacterLanePriority(Character character, IEnumerable<SceneCharacter> appearances)
|
||||
{
|
||||
var description = character.DefaultDescription ?? string.Empty;
|
||||
var appearanceList = appearances as IReadOnlyCollection<SceneCharacter> ?? appearances.ToList();
|
||||
var priority = 0;
|
||||
|
||||
if (description.Contains("Imported character classification: Major", StringComparison.OrdinalIgnoreCase))
|
||||
if (character.CharacterImportance >= 8)
|
||||
{
|
||||
priority += 1_200;
|
||||
}
|
||||
else if (character.CharacterImportance >= 4)
|
||||
{
|
||||
priority += 500;
|
||||
}
|
||||
else if (character.CharacterImportance.HasValue)
|
||||
{
|
||||
priority -= 150;
|
||||
}
|
||||
else if (description.Contains("Imported character classification: Major", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
priority += 1_000;
|
||||
}
|
||||
@ -1389,7 +1401,9 @@ public sealed class TimelineService(
|
||||
priority -= 100;
|
||||
}
|
||||
|
||||
foreach (var appearance in appearances)
|
||||
priority += appearanceList.Count * 3;
|
||||
|
||||
foreach (var appearance in appearanceList)
|
||||
{
|
||||
if (string.Equals(appearance.RoleInSceneTypeName, "POV Character", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@ -2904,6 +2918,7 @@ public sealed class CharacterService(IProjectRepository projects, ICharacterRepo
|
||||
AgeAtSeriesStart = character.AgeAtSeriesStart,
|
||||
Height = character.Height,
|
||||
EyeColour = character.EyeColour,
|
||||
CharacterImportance = character.CharacterImportance,
|
||||
DefaultDescription = character.DefaultDescription,
|
||||
Project = await projects.GetAsync(character.ProjectID)
|
||||
};
|
||||
@ -2947,6 +2962,7 @@ public sealed class CharacterService(IProjectRepository projects, ICharacterRepo
|
||||
AgeAtSeriesStart = model.AgeAtSeriesStart,
|
||||
Height = model.Height,
|
||||
EyeColour = model.EyeColour,
|
||||
CharacterImportance = model.CharacterImportance,
|
||||
DefaultDescription = model.DefaultDescription
|
||||
});
|
||||
|
||||
|
||||
@ -771,7 +771,7 @@ public sealed class ImportService(IImportRepository imports, ILogger<ImportServi
|
||||
{
|
||||
if (package.Characters.Any(character => character.Importance.HasValue || !string.IsNullOrWhiteSpace(character.Role)))
|
||||
{
|
||||
result.Warnings.Add("Character importance is not stored in a dedicated character field; the importer uses role/importance for timeline grouping where supported and preserves a clean classification note.");
|
||||
result.Warnings.Add("Character role/importance will be mapped to Story importance for timeline grouping.");
|
||||
}
|
||||
|
||||
foreach (var group in package.Characters
|
||||
|
||||
@ -43,6 +43,7 @@ BEGIN
|
||||
AgeReferenceSceneID int NULL,
|
||||
Height nvarchar(50) NULL,
|
||||
EyeColour nvarchar(50) NULL,
|
||||
CharacterImportance int NULL,
|
||||
DefaultDescription nvarchar(max) NULL,
|
||||
CreatedDate datetime2 NOT NULL CONSTRAINT DF_Characters_CreatedDate DEFAULT SYSUTCDATETIME(),
|
||||
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_Characters_UpdatedDate DEFAULT SYSUTCDATETIME(),
|
||||
@ -319,7 +320,7 @@ AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID,
|
||||
Height, EyeColour, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
|
||||
Height, EyeColour, CharacterImportance, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
|
||||
FROM dbo.Characters
|
||||
WHERE ProjectID = @ProjectID AND IsArchived = 0
|
||||
ORDER BY CharacterName;
|
||||
@ -332,7 +333,7 @@ AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID,
|
||||
Height, EyeColour, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
|
||||
Height, EyeColour, CharacterImportance, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
|
||||
FROM dbo.Characters
|
||||
WHERE CharacterID = @CharacterID AND IsArchived = 0;
|
||||
END;
|
||||
@ -349,14 +350,15 @@ CREATE OR ALTER PROCEDURE dbo.Character_Save
|
||||
@AgeReferenceSceneID int = NULL,
|
||||
@Height nvarchar(50) = NULL,
|
||||
@EyeColour nvarchar(50) = NULL,
|
||||
@CharacterImportance int = NULL,
|
||||
@DefaultDescription nvarchar(max) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
IF @CharacterID IS NULL OR @CharacterID = 0
|
||||
BEGIN
|
||||
INSERT dbo.Characters (ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID, Height, EyeColour, DefaultDescription)
|
||||
VALUES (@ProjectID, @CharacterName, @ShortName, @Sex, @BirthDate, @AgeAtSeriesStart, @AgeReferenceSceneID, @Height, @EyeColour, @DefaultDescription);
|
||||
INSERT dbo.Characters (ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID, Height, EyeColour, CharacterImportance, DefaultDescription)
|
||||
VALUES (@ProjectID, @CharacterName, @ShortName, @Sex, @BirthDate, @AgeAtSeriesStart, @AgeReferenceSceneID, @Height, @EyeColour, @CharacterImportance, @DefaultDescription);
|
||||
SET @CharacterID = CAST(SCOPE_IDENTITY() AS int);
|
||||
END
|
||||
ELSE
|
||||
@ -364,7 +366,7 @@ BEGIN
|
||||
UPDATE dbo.Characters
|
||||
SET CharacterName = @CharacterName, ShortName = @ShortName, Sex = @Sex, BirthDate = @BirthDate,
|
||||
AgeAtSeriesStart = @AgeAtSeriesStart, AgeReferenceSceneID = @AgeReferenceSceneID,
|
||||
Height = @Height, EyeColour = @EyeColour, DefaultDescription = @DefaultDescription,
|
||||
Height = @Height, EyeColour = @EyeColour, CharacterImportance = @CharacterImportance, DefaultDescription = @DefaultDescription,
|
||||
UpdatedDate = SYSUTCDATETIME()
|
||||
WHERE CharacterID = @CharacterID;
|
||||
END
|
||||
@ -710,11 +712,11 @@ CREATE OR ALTER PROCEDURE dbo.CharacterTimeline_GetByProject
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
SELECT TOP (12) CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID,
|
||||
Height, EyeColour, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
|
||||
SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID,
|
||||
Height, EyeColour, CharacterImportance, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
|
||||
FROM dbo.Characters
|
||||
WHERE ProjectID = @ProjectID AND IsArchived = 0
|
||||
ORDER BY CharacterName;
|
||||
ORDER BY ISNULL(CharacterImportance, 0) DESC, CharacterName;
|
||||
|
||||
SELECT sc.SceneCharacterID, sc.SceneID, sc.CharacterID, c.ProjectID, c.CharacterName, c.ShortName, c.BirthDate, c.AgeAtSeriesStart,
|
||||
sc.RoleInSceneTypeID, role.TypeName AS RoleInSceneTypeName, sc.PresenceTypeID, presence.TypeName AS PresenceTypeName,
|
||||
|
||||
@ -10,11 +10,11 @@ AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT TOP (12) CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID,
|
||||
Height, EyeColour, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
|
||||
SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID,
|
||||
Height, EyeColour, CharacterImportance, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
|
||||
FROM dbo.Characters
|
||||
WHERE ProjectID = @ProjectID AND IsArchived = 0
|
||||
ORDER BY CharacterName;
|
||||
ORDER BY ISNULL(CharacterImportance, 0) DESC, CharacterName;
|
||||
|
||||
SELECT sc.SceneCharacterID, sc.SceneID, sc.CharacterID, c.ProjectID, c.CharacterName, c.ShortName, c.BirthDate, c.AgeAtSeriesStart,
|
||||
sc.RoleInSceneTypeID, role.TypeName AS RoleInSceneTypeName, sc.PresenceTypeID, presence.TypeName AS PresenceTypeName,
|
||||
|
||||
117
PlotLine/Sql/022_CharacterImportance.sql
Normal file
117
PlotLine/Sql/022_CharacterImportance.sql
Normal file
@ -0,0 +1,117 @@
|
||||
SET ANSI_NULLS ON;
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.Characters', N'CharacterImportance') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE dbo.Characters ADD CharacterImportance int NULL;
|
||||
END;
|
||||
GO
|
||||
|
||||
UPDATE dbo.Characters
|
||||
SET CharacterImportance =
|
||||
CASE
|
||||
WHEN DefaultDescription LIKE N'%Imported character classification: Major%' THEN 10
|
||||
WHEN DefaultDescription LIKE N'%Imported character classification: Supporting%' THEN 6
|
||||
WHEN DefaultDescription LIKE N'%Imported character classification: Minor%' THEN 2
|
||||
ELSE CharacterImportance
|
||||
END
|
||||
WHERE CharacterImportance IS NULL
|
||||
AND DefaultDescription LIKE N'%Imported character classification:%';
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.Character_ListByProject
|
||||
@ProjectID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID,
|
||||
Height, EyeColour, CharacterImportance, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
|
||||
FROM dbo.Characters
|
||||
WHERE ProjectID = @ProjectID AND IsArchived = 0
|
||||
ORDER BY CharacterName;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.Character_Get
|
||||
@CharacterID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID,
|
||||
Height, EyeColour, CharacterImportance, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
|
||||
FROM dbo.Characters
|
||||
WHERE CharacterID = @CharacterID AND IsArchived = 0;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.Character_Save
|
||||
@CharacterID int = NULL,
|
||||
@ProjectID int,
|
||||
@CharacterName nvarchar(200),
|
||||
@ShortName nvarchar(100) = NULL,
|
||||
@Sex nvarchar(50) = NULL,
|
||||
@BirthDate date = NULL,
|
||||
@AgeAtSeriesStart int = NULL,
|
||||
@AgeReferenceSceneID int = NULL,
|
||||
@Height nvarchar(50) = NULL,
|
||||
@EyeColour nvarchar(50) = NULL,
|
||||
@CharacterImportance int = NULL,
|
||||
@DefaultDescription nvarchar(max) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
IF @CharacterID IS NULL OR @CharacterID = 0
|
||||
BEGIN
|
||||
INSERT dbo.Characters (ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID, Height, EyeColour, CharacterImportance, DefaultDescription)
|
||||
VALUES (@ProjectID, @CharacterName, @ShortName, @Sex, @BirthDate, @AgeAtSeriesStart, @AgeReferenceSceneID, @Height, @EyeColour, @CharacterImportance, @DefaultDescription);
|
||||
SET @CharacterID = CAST(SCOPE_IDENTITY() AS int);
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
UPDATE dbo.Characters
|
||||
SET CharacterName = @CharacterName, ShortName = @ShortName, Sex = @Sex, BirthDate = @BirthDate,
|
||||
AgeAtSeriesStart = @AgeAtSeriesStart, AgeReferenceSceneID = @AgeReferenceSceneID,
|
||||
Height = @Height, EyeColour = @EyeColour, CharacterImportance = @CharacterImportance, DefaultDescription = @DefaultDescription,
|
||||
UpdatedDate = SYSUTCDATETIME()
|
||||
WHERE CharacterID = @CharacterID;
|
||||
END
|
||||
SELECT @CharacterID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.CharacterTimeline_GetByProject
|
||||
@ProjectID int,
|
||||
@BookID int = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID,
|
||||
Height, EyeColour, CharacterImportance, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
|
||||
FROM dbo.Characters
|
||||
WHERE ProjectID = @ProjectID AND IsArchived = 0
|
||||
ORDER BY ISNULL(CharacterImportance, 0) DESC, CharacterName;
|
||||
|
||||
SELECT sc.SceneCharacterID, sc.SceneID, sc.CharacterID, c.ProjectID, c.CharacterName, c.ShortName, c.BirthDate, c.AgeAtSeriesStart,
|
||||
sc.RoleInSceneTypeID, role.TypeName AS RoleInSceneTypeName, sc.PresenceTypeID, presence.TypeName AS PresenceTypeName,
|
||||
sc.LocationID, location.LocationName, location.LocationPath, sc.EntryLocationID, entry.LocationName AS EntryLocationName,
|
||||
sc.ExitLocationID, exitLocation.LocationName AS ExitLocationName, sc.AppearanceNotes, sc.OutfitDescription,
|
||||
sc.PhysicalCondition, sc.EmotionalState, sc.KnowledgeNotes, s.SceneNumber, s.SceneTitle, ch.ChapterNumber, b.BookTitle,
|
||||
sc.CreatedDate, sc.UpdatedDate
|
||||
FROM dbo.SceneCharacters sc
|
||||
INNER JOIN dbo.Characters c ON c.CharacterID = sc.CharacterID
|
||||
INNER JOIN dbo.Scenes s ON s.SceneID = sc.SceneID
|
||||
INNER JOIN dbo.Chapters ch ON ch.ChapterID = s.ChapterID
|
||||
INNER JOIN dbo.Books b ON b.BookID = ch.BookID
|
||||
LEFT JOIN dbo.CharacterRoleInSceneTypes role ON role.CharacterRoleInSceneTypeID = sc.RoleInSceneTypeID
|
||||
LEFT JOIN dbo.PresenceTypes presence ON presence.PresenceTypeID = sc.PresenceTypeID
|
||||
LEFT JOIN dbo.LocationPaths location ON location.LocationID = sc.LocationID
|
||||
LEFT JOIN dbo.LocationPaths entry ON entry.LocationID = sc.EntryLocationID
|
||||
LEFT JOIN dbo.LocationPaths exitLocation ON exitLocation.LocationID = sc.ExitLocationID
|
||||
WHERE c.ProjectID = @ProjectID AND c.IsArchived = 0 AND s.IsArchived = 0 AND ch.IsArchived = 0 AND b.IsArchived = 0
|
||||
AND (@BookID IS NULL OR b.BookID = @BookID)
|
||||
ORDER BY b.SortOrder, ch.SortOrder, s.SortOrder, c.CharacterName;
|
||||
END;
|
||||
GO
|
||||
@ -838,9 +838,20 @@ public sealed class CharacterEditViewModel
|
||||
[Display(Name = "Eye colour")]
|
||||
public string? EyeColour { get; set; }
|
||||
|
||||
[Display(Name = "Story importance")]
|
||||
public int? CharacterImportance { get; set; }
|
||||
|
||||
[Display(Name = "Default description")]
|
||||
public string? DefaultDescription { get; set; }
|
||||
|
||||
public IReadOnlyList<SelectListItem> ImportanceOptions { get; set; } =
|
||||
[
|
||||
new("Use timeline fallback", string.Empty),
|
||||
new("Major", "10"),
|
||||
new("Secondary / Supporting", "6"),
|
||||
new("Minor", "2")
|
||||
];
|
||||
|
||||
public Project? Project { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,13 @@
|
||||
@{
|
||||
ViewData["Title"] = Model.Character.CharacterName;
|
||||
var firstAppearance = Model.Appearances.FirstOrDefault();
|
||||
var storyImportance = Model.Character.CharacterImportance switch
|
||||
{
|
||||
>= 8 => "Major",
|
||||
>= 4 => "Secondary / Supporting",
|
||||
>= 1 => "Minor",
|
||||
_ => "Timeline fallback"
|
||||
};
|
||||
}
|
||||
|
||||
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
|
||||
@ -54,6 +61,7 @@
|
||||
<p><strong>Age:</strong> @Model.DisplayAge</p>
|
||||
<p><strong>Height:</strong> @Model.Character.Height</p>
|
||||
<p><strong>Eye colour:</strong> @Model.Character.EyeColour</p>
|
||||
<p><strong>Story importance:</strong> @storyImportance</p>
|
||||
</article>
|
||||
<article class="plain-card">
|
||||
<h2>Relationships</h2>
|
||||
|
||||
@ -44,6 +44,11 @@
|
||||
<label asp-for="EyeColour" class="form-label"></label>
|
||||
<input asp-for="EyeColour" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="CharacterImportance" class="form-label"></label>
|
||||
<select asp-for="CharacterImportance" asp-items="Model.ImportanceOptions" class="form-select"></select>
|
||||
<div class="form-text">Controls timeline grouping for major, secondary/supporting, and minor characters.</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label asp-for="DefaultDescription" class="form-label"></label>
|
||||
<textarea asp-for="DefaultDescription" class="form-control" rows="5"></textarea>
|
||||
|
||||
@ -51,6 +51,61 @@
|
||||
}
|
||||
int ChapterSceneColumns(ChapterTimelineViewModel chapter) => Math.Max(chapter.Scenes.Count, 1);
|
||||
int BookSceneColumns(BookTimelineViewModel book) => Math.Max(book.Chapters.Any() ? book.Chapters.Sum(ChapterSceneColumns) : 1, 1);
|
||||
int CharacterAppearanceCount(CharacterLaneViewModel lane) => lane.SceneSlots.Sum(slot => slot.Appearances.Count);
|
||||
bool HasLeadSceneRole(CharacterLaneViewModel lane) => lane.SceneSlots
|
||||
.SelectMany(slot => slot.Appearances)
|
||||
.Any(appearance =>
|
||||
string.Equals(appearance.RoleInSceneTypeName, "POV Character", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(appearance.RoleInSceneTypeName, "Main Participant", StringComparison.OrdinalIgnoreCase));
|
||||
string CharacterGroupName(CharacterLaneViewModel lane)
|
||||
{
|
||||
var description = lane.Character.DefaultDescription ?? string.Empty;
|
||||
|
||||
// Timeline grouping prefers the editable CharacterImportance field; imported
|
||||
// classification metadata remains a fallback for older imported projects.
|
||||
if (lane.Character.CharacterImportance >= 8)
|
||||
{
|
||||
return "Major characters";
|
||||
}
|
||||
|
||||
if (lane.Character.CharacterImportance >= 4)
|
||||
{
|
||||
return "Secondary / supporting characters";
|
||||
}
|
||||
|
||||
if (lane.Character.CharacterImportance.HasValue)
|
||||
{
|
||||
return "Minor / mentioned characters";
|
||||
}
|
||||
|
||||
if (description.Contains("Imported character classification: Major", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "Major characters";
|
||||
}
|
||||
|
||||
if (description.Contains("Imported character classification: Supporting", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "Secondary / supporting characters";
|
||||
}
|
||||
|
||||
if (description.Contains("Imported character classification: Minor", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "Minor / mentioned characters";
|
||||
}
|
||||
|
||||
if (HasLeadSceneRole(lane) || CharacterAppearanceCount(lane) >= 6)
|
||||
{
|
||||
return "Major characters";
|
||||
}
|
||||
|
||||
return CharacterAppearanceCount(lane) <= 1 ? "Minor / mentioned characters" : "Secondary / supporting characters";
|
||||
}
|
||||
int CharacterGroupOrder(string groupName) => groupName switch
|
||||
{
|
||||
"Major characters" => 0,
|
||||
"Secondary / supporting characters" => 1,
|
||||
_ => 2
|
||||
};
|
||||
}
|
||||
|
||||
<section class="context-header">
|
||||
@ -106,7 +161,7 @@
|
||||
</div>
|
||||
|
||||
<section class="timeline-control-panel" data-timeline-controls>
|
||||
<details class="timeline-filter-drawer" @(hasActiveFilter ? "open" : "")>
|
||||
<details class="timeline-filter-drawer">
|
||||
<summary>Filters and focus</summary>
|
||||
<form method="get" class="timeline-filter-grid">
|
||||
<input type="hidden" name="ProjectID" value="@Model.Project.ProjectID" />
|
||||
@ -390,7 +445,10 @@
|
||||
@foreach (var chapter in book.Chapters)
|
||||
{
|
||||
var chapterWarnings = chapter.Scenes.Sum(x => x.WarningCount);
|
||||
var isActiveChapter = chapter.Scenes.Any(scene => scene.SceneID == Model.SelectedSceneID);
|
||||
<button type="button"
|
||||
class="@(isActiveChapter ? "is-active" : "")"
|
||||
data-active-chapter="@(isActiveChapter ? "true" : "false")"
|
||||
data-jump-scene="@chapter.Scenes.FirstOrDefault()?.SceneID"
|
||||
style="--chapter-scenes:@Math.Max(chapter.Scenes.Count, 1); --chapter-warnings:@Math.Min(chapterWarnings, 8)"
|
||||
title="Chapter @chapter.Chapter.ChapterNumber: @chapter.Chapter.ChapterTitle (@chapter.Scenes.Count scenes, @chapterWarnings warnings)">
|
||||
@ -784,14 +842,19 @@ else
|
||||
<div class="character-lane-board" data-timeline-section="characters" style="--scene-count:@timelineColumns.Count">
|
||||
<div class="lane-board-heading character-lane-board-heading">
|
||||
<button type="button" data-collapse-toggle="characters">Character appearances</button>
|
||||
<small>First twelve project characters</small>
|
||||
<small>All project characters, grouped by role and activity</small>
|
||||
</div>
|
||||
<div data-collapse-body="characters">
|
||||
@foreach (var group in Model.CharacterLanes.Select((lane, index) => new { lane, group = index < 6 ? "Major characters" : "Secondary characters" }).GroupBy(x => x.group))
|
||||
@foreach (var group in Model.CharacterLanes
|
||||
.Select((lane, index) => new { lane, index, group = CharacterGroupName(lane) })
|
||||
.GroupBy(x => x.group)
|
||||
.OrderBy(group => CharacterGroupOrder(group.Key)))
|
||||
{
|
||||
var groupId = $"character-group-{CharacterGroupOrder(group.Key)}";
|
||||
var defaultCollapsed = CharacterGroupOrder(group.Key) > 0 ? "collapsed" : null;
|
||||
<div class="lane-group">
|
||||
<button type="button" class="lane-group-heading" data-collapse-toggle="character-group-@group.Key.GetHashCode()">@group.Key</button>
|
||||
<div data-collapse-body="character-group-@group.Key.GetHashCode()">
|
||||
<button type="button" class="lane-group-heading" data-collapse-toggle="@groupId" data-collapse-default="@defaultCollapsed">@group.Key <span>@group.Count()</span></button>
|
||||
<div class="character-lane-group-body" data-collapse-body="@groupId">
|
||||
@foreach (var laneItem in group)
|
||||
{
|
||||
var lane = laneItem.lane;
|
||||
@ -874,7 +937,7 @@ else
|
||||
<div class="metric-graph-stack mt-2">
|
||||
@foreach (var graph in Model.MetricGraphs)
|
||||
{
|
||||
<div class="metric-graph-row">
|
||||
<div class="metric-graph-row" style="--scene-count:@Math.Max(timelineColumns.Count, 1)">
|
||||
<div class="metric-graph-label">@graph.MetricName</div>
|
||||
<div class="metric-graph-points" style="--scene-count:@Math.Max(timelineColumns.Count, 1)">
|
||||
@foreach (var column in timelineColumns)
|
||||
|
||||
@ -393,6 +393,7 @@ td.text-end {
|
||||
|
||||
.timeline-workspace {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.pacing-panel {
|
||||
@ -421,6 +422,11 @@ td.text-end {
|
||||
.metric-graph-stack {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
overscroll-behavior-x: contain;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.metric-graph-row {
|
||||
@ -428,6 +434,7 @@ td.text-end {
|
||||
grid-template-columns: 150px minmax(260px, 1fr);
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
min-width: max(100%, calc(150px + (var(--scene-count, 1) * 34px)));
|
||||
}
|
||||
|
||||
.metric-graph-label {
|
||||
@ -445,6 +452,7 @@ td.text-end {
|
||||
border-radius: 8px;
|
||||
padding: 6px;
|
||||
background: linear-gradient(180deg, #f6faf8 0%, #edf4f1 100%);
|
||||
min-width: calc(var(--scene-count) * 34px);
|
||||
}
|
||||
|
||||
.metric-point {
|
||||
@ -1953,6 +1961,12 @@ td.text-end {
|
||||
gap: 4px;
|
||||
margin: 0 0 12px;
|
||||
padding: 8px;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
overscroll-behavior-x: contain;
|
||||
scrollbar-gutter: stable;
|
||||
border: 1px solid rgba(47, 111, 99, 0.18);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
@ -1960,7 +1974,7 @@ td.text-end {
|
||||
|
||||
.timeline-minimap button {
|
||||
position: relative;
|
||||
flex: var(--chapter-scenes) 1 42px;
|
||||
flex: 0 0 max(42px, calc(var(--chapter-scenes) * 18px));
|
||||
min-width: 42px;
|
||||
min-height: 34px;
|
||||
border: 1px solid rgba(47, 111, 99, 0.18);
|
||||
@ -1971,6 +1985,13 @@ td.text-end {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.timeline-minimap button.is-active {
|
||||
border-color: var(--plotline-accent);
|
||||
color: #fff;
|
||||
background: linear-gradient(180deg, var(--plotline-accent) 0%, var(--plotline-accent-dark) 100%);
|
||||
box-shadow: 0 0 0 2px rgba(47, 111, 99, 0.14);
|
||||
}
|
||||
|
||||
.timeline-minimap button::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@ -2157,6 +2178,11 @@ td.text-end {
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.character-lane-group-body {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.lane-group-heading {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
|
||||
@ -255,19 +255,30 @@
|
||||
ctx.lineWidth = 1;
|
||||
ctx.font = "700 10px system-ui, -apple-system, Segoe UI, sans-serif";
|
||||
|
||||
let previousChapter = null;
|
||||
const chapterStarts = [];
|
||||
scenes.forEach((scene, index) => {
|
||||
if (scene.chapterLabel === previousChapter) {
|
||||
return;
|
||||
if (!index || scene.chapterLabel !== scenes[index - 1].chapterLabel) {
|
||||
chapterStarts.push({ scene, index });
|
||||
}
|
||||
});
|
||||
const labelInterval = chapterStarts.length > 40 ? 5 : chapterStarts.length > 24 ? 4 : chapterStarts.length > 14 ? 2 : 1;
|
||||
|
||||
chapterStarts.forEach((entry, chapterIndex) => {
|
||||
const { scene, index } = entry;
|
||||
const x = scales.x.getPixelForValue(index);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, chartArea.top);
|
||||
ctx.lineTo(x, chartArea.bottom);
|
||||
ctx.stroke();
|
||||
ctx.fillText(scene.chapterLabel, Math.min(x + 4, chartArea.right - 70), chartArea.bottom + 22);
|
||||
previousChapter = scene.chapterLabel;
|
||||
|
||||
const chapterNumber = Number.parseInt(String(scene.chapterLabel).replace(/[^0-9]/g, ""), 10);
|
||||
const shouldLabel = chapterIndex === 0 ||
|
||||
chapterIndex === chapterStarts.length - 1 ||
|
||||
labelInterval === 1 ||
|
||||
(Number.isFinite(chapterNumber) && chapterNumber % labelInterval === 0);
|
||||
if (shouldLabel) {
|
||||
ctx.fillText(scene.chapterLabel, Math.min(x + 4, chartArea.right - 70), chartArea.bottom + 22);
|
||||
}
|
||||
});
|
||||
|
||||
ctx.restore();
|
||||
@ -478,6 +489,7 @@
|
||||
const densitySelect = document.querySelector("[data-timeline-density]");
|
||||
const zoomSelect = document.querySelector("[data-timeline-zoom]");
|
||||
const searchInput = document.querySelector("[data-timeline-search]");
|
||||
const minimap = root.querySelector("[data-timeline-minimap]");
|
||||
const jumpSelects = document.querySelectorAll("[data-timeline-jump], [data-timeline-warning-jump], [data-timeline-entity-jump]");
|
||||
const sceneCards = [...root.querySelectorAll(".timeline-scene-card[data-scene-id]")];
|
||||
const collapsedKey = "plotline.timeline.collapsed";
|
||||
@ -494,6 +506,7 @@
|
||||
}
|
||||
};
|
||||
|
||||
const hasCollapsedPreference = localStorage.getItem(collapsedKey) !== null;
|
||||
const collapsed = readSet(collapsedKey);
|
||||
|
||||
const applyMode = (kind, value, values) => {
|
||||
@ -535,6 +548,21 @@
|
||||
window.setTimeout(() => target.classList.remove("keyboard-focus"), 1800);
|
||||
};
|
||||
|
||||
const scrollActiveChapterIntoView = (behavior = "auto") => {
|
||||
const activeChapter = minimap?.querySelector("[data-active-chapter='true']");
|
||||
if (!minimap || !activeChapter) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hostRect = minimap.getBoundingClientRect();
|
||||
const activeRect = activeChapter.getBoundingClientRect();
|
||||
const activeCenter = activeRect.left + activeRect.width / 2;
|
||||
const hostCenter = hostRect.left + hostRect.width / 2;
|
||||
if (activeRect.left < hostRect.left || activeRect.right > hostRect.right) {
|
||||
minimap.scrollBy({ left: activeCenter - hostCenter, behavior });
|
||||
}
|
||||
};
|
||||
|
||||
const saveCollapsed = () => localStorage.setItem(collapsedKey, JSON.stringify([...collapsed]));
|
||||
|
||||
const setCollapsed = (id, shouldCollapse) => {
|
||||
@ -549,6 +577,10 @@
|
||||
|
||||
document.querySelectorAll("[data-collapse-toggle]").forEach((button) => {
|
||||
const id = button.dataset.collapseToggle;
|
||||
if (!hasCollapsedPreference && button.dataset.collapseDefault === "collapsed") {
|
||||
collapsed.add(id);
|
||||
}
|
||||
|
||||
const isCollapsed = collapsed.has(id);
|
||||
setCollapsed(id, isCollapsed);
|
||||
button.addEventListener("click", () => {
|
||||
@ -567,6 +599,7 @@
|
||||
zoomSelect?.addEventListener("change", () => applyZoom(zoomSelect.value));
|
||||
applyDensity(localStorage.getItem(densityKey) || "comfortable");
|
||||
applyZoom(localStorage.getItem(zoomKey) || "chapter");
|
||||
window.requestAnimationFrame(() => scrollActiveChapterIntoView("auto"));
|
||||
|
||||
const runSearch = () => {
|
||||
const query = searchInput?.value.trim().toLowerCase() || "";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user