diff --git a/.appdata/.dotnet/TelemetryStorageService/20260603133605_8806deb4425a4aa9a6b34287e127a8c4.trn b/.appdata/.dotnet/TelemetryStorageService/20260603133605_8806deb4425a4aa9a6b34287e127a8c4.trn deleted file mode 100644 index 7de8693..0000000 --- a/.appdata/.dotnet/TelemetryStorageService/20260603133605_8806deb4425a4aa9a6b34287e127a8c4.trn +++ /dev/null @@ -1,5 +0,0 @@ -https://dc.services.visualstudio.com/v2/track -Content-Type:application/x-json-stream -Content-Encoding:gzip - -H4sIAAAAAAAACoxUS2/bOBD+KwLPpkKKFCUK2IPhtFgjTWMk7u5ibxQ5jInIokFRToMi/31BWa6dooe9CaN5fY/hD9SrPaAGLQ+HT0fo44AWKLopVJBCYCIwYVvKGiYaUuY1KwVl5F+0QO4O3lCDKq411RIwAwGYG1LillmNjQFJCTWkKHnqqZ4H1PxAyuUGjk5D7oe/IAzO96hBf7ve+Nc0W7lcd340efAdrPshql6nZb6uV3f4y3KzfdicsgYYUnHuDGqQopSAtgozIBZzpi2uiRS44i2zoEsra3kqc32E0KsuH8zLZb7xsYeomyIvWE5wIdH7AhkVVVq5VQNs3w5pi4mi2xRfTOHps/mBjhBQUyzOZJ7a3ejO3eyHdnSduYkqPEO0Qe3h1YcXOKoOLdAh+AOE6GDi5uEpu6xESU7yQhSEoAW6V3rnesjWt9lDNwHWNeFgGJZgDeaSKiw5Y9iwUlWFsFVVWLRAdxB66K663jsd/OBtzGbKsw9zVr6Prh/9OGTrPsJzUPFU91l1A6AF2oxt54bdIyjztvWPY/rXj13CshpDgD5mGxV32Z9q2KEGSVVrqqSSSgmtLZQELDFECVIZEEaA4YWpDJfcCC6JFly1lBtV0tamCFqghzEexnjm//vVEtvg9nswlw22E8ebTkXrw35toI/OuiTNOWMyVNdNqLK556pz0McPJKMGEQqFYTVgW1CBeW0UViWTWAgmLFAtq4KeCLPuefzJE7HWcGbLuhatbCmBwlY1J5VtidGWaWnKFqqiIlUNtdSEtCWndSs1MUBLbhK64M2o469WYJNAj2OfrjNbJ9ivrsffBb8wsgk+eu27JJjr4OltiLD/nWorvz/4wcWE387Sfhvg3vfp73z/M2dz8dLHSwzSHWSn0wNmVNUybEpTYW5si6WWFhvZKq0Zg7JM3W+9foGQJYMp10+anD318JQtg965CDqOIU3+ZwJ1kvPz+WQufORfP21XPsDycFjM0T+OiaRTs7MBPrwr34aUvvNDvII8Q3uCzp4Xu7KTgePpqXIpKIypwQiKC05LzKWkuJZlhduqbmUraij1xa7ZIxgXQMep3zaMadrM7O98uQzRWaXjkI7ni9eTnWZ/zim/Lnkl28/q0/Ar6WYrzZ0oSZZ9Oj2ck4P+17v5/v7+/h8AAAD//wMA7SBOIigGAAA= \ No newline at end of file diff --git a/.appdata/.dotnet/TelemetryStorageService/20260603133614_0d3e186dd02241feb8d99916734f01b4.trn b/.appdata/.dotnet/TelemetryStorageService/20260603133614_0d3e186dd02241feb8d99916734f01b4.trn deleted file mode 100644 index 4a151a6..0000000 --- a/.appdata/.dotnet/TelemetryStorageService/20260603133614_0d3e186dd02241feb8d99916734f01b4.trn +++ /dev/null @@ -1,5 +0,0 @@ -https://dc.services.visualstudio.com/v2/track -Content-Type:application/x-json-stream -Content-Encoding:gzip - -H4sIAAAAAAAACpSUTW/cNhCG/4owZ1HmlyhRN2OdoAs39SJZpEFuFDmKCcvUgqScGIb/e0HtFo5PRS8EMeIM33nmpV4gmEeEAa5Ppw9PGHKCGrLfQpxyRagiVByZGIQamGwo5VqJ7jvU4G/xGQbopLXMaiQCFRLpaEtGMVniHGpGmaO8laWm+ZFgeAHjG4dP3mKzpK8Yk18CDPC3D275We42vrHzsromLjPuQ8om2CLmr/3ulvx5fTjeHc6nEqaS3HgHAxjGKNrJEIF0IlLYifRUK9LJUUxo20n3+pzmQ8YYzNwk9/B2v1tywGwH3nDRUMI1vNbgTDZF8mgSHp9PRcWG6KbE6y28bYcXeMIIA6//hXkud2Vnf/WYxtXP7uq8ZpMe0rZOxuYlPkMNp7icMGaPG5/rlPBxnJ+PJj18PJ8p2/ThF9o1o9sta8gwABM91LABmmeT/RKqi8bd7DHk7WOOPiRv/6uWgBruvlSH2eRpiY/v5vHljHn/Pyh/MvbeB6z2NzAAZcid6JFMnCkie2eIaYUmSgk1IbO64wxq2C0h+7Aua6r2IeOPuLUEA3w0c8Kzvuto731Gm9dY+vymiq8OcXGrzdXbLBltaCMoLUlrPq25+ozOR7QZSxPHuF7qfX2fwhXfknZrjBhydTD5vvrDpHsYQJveMqONNkZZO2FLcaKOGkU7h8opdJK7zkktnZKaWiXNyKQzLRunEoEasFin2tyqZTcq7pDQQk9ORpGxx46MTLuWW2w1ltbeOFZ388bf9lSiE0Tj5IjUzBAthSBOtKbjauo6Pv2G5OIHRgtgh0/nd7cpUM716BQjXLKWSK0Z6XXbkbHrRz2qHltbNN9iDDj/BuqTt3FJy5Sri0Oqd+g+r6H8O6rNLT99IL+2Gd0s9gFjVWZsfChP5TLW19fX138AAAD//wMAZhfta4IEAAA= \ No newline at end of file diff --git a/.appdata/.dotnet/TelemetryStorageService/20260603133614_17dca1dc82fb45e6b7ceaa58d485f99b.trn b/.appdata/.dotnet/TelemetryStorageService/20260603133614_17dca1dc82fb45e6b7ceaa58d485f99b.trn deleted file mode 100644 index 8b0132c..0000000 --- a/.appdata/.dotnet/TelemetryStorageService/20260603133614_17dca1dc82fb45e6b7ceaa58d485f99b.trn +++ /dev/null @@ -1,5 +0,0 @@ -https://dc.services.visualstudio.com/v2/track -Content-Type:application/x-json-stream -Content-Encoding:gzip - -H4sIAAAAAAAACpRTy27UMBT9lcjrcepXnDi7agpiVKAjGAFi5/heU6upM3KcAqrm35Ezg6Ds2ETRlY/vefmZRPuIpCfXx+OrJ4x5JhuSwzoSTGjKNGXywGUvdc9VzVjDtGm+kg0Jt/iT9KRVznFnkErUSBWwhg7SOwqAhjMOTDSq3Gm/zaR/JjbUgE/BYT3NnzDNYYqkJ59DhOl72W1D7cZpgTpNI+7inG10hcz73faWvr3eH+7251MzzgVcByA9sZwzdN5SicxTJZ2nHTOatmqQHl3jTWfOsBAzpmjHeoaHP/thyhGz60UtZM2oMOS0IWCzLZQHO+Ph57GwWC26KfPNOl5/+2fyhIn0YvPbzPN1V24MV4/zsIQRrs7fbOeHovKYpiOmHHD1ZJU5jjaHKVaXTdsxYMxkQ+4+VvvRZj+lxxdGfTzr3/2H/EPZ/uoHuiUjbKclZtITrjjZkHfW3YeI1e6G9IRxFCA7pF5wTVUHltpGGqq11B65M60omO0Uc4jLtMzVLmb8llYBpCev7Tjjmfp1cvcho8tLKqq+6NKFfZpgcbn64z9nNaslYwW05OOSqw8IIaHLWPQd0nK57x+I0GIFbZeUMOZqb/N99cbO96QnxnaOW2ONtdo5jw1Dz4BZzVpADRpBCWhBGQVaGea0sgNXYBs++DIhG4Il7mptmDIIojS84aKjqu2AdsJx2gnh7eChsQgvfKzuxjUa1zGFIKlBD1QZbqlRUlKQjW2F9m0r/F+WXNLnrBgM+HR+KysDDdAhaE6F4g1VxnDamaalQ9sNZtAdNq5wvsUUcfzLqHfBpWmefK4u5aleWPdhieW9V2uRvodIf6wZ3UzuAVNVMrYhlnpfYj2dTqdfAAAA//8DALKjFbI2BAAA \ No newline at end of file diff --git a/.appdata/.dotnet/TelemetryStorageService/20260603133614_c43f8d6b685641a58b398f99855ecebb.trn b/.appdata/.dotnet/TelemetryStorageService/20260603133614_c43f8d6b685641a58b398f99855ecebb.trn deleted file mode 100644 index b49d88e..0000000 --- a/.appdata/.dotnet/TelemetryStorageService/20260603133614_c43f8d6b685641a58b398f99855ecebb.trn +++ /dev/null @@ -1,5 +0,0 @@ -https://dc.services.visualstudio.com/v2/track -Content-Type:application/x-json-stream -Content-Encoding:gzip - -H4sIAAAAAAAACpSTTW/cOAyG/4qhs+Xoy7LlWzDZRQdpN4M2aBd7k0W6I9SRBrKctgjmvy/kmaJNb70JlEi+fF7qhQT7hGQgt6fTX88Y8kJqkv0WEkxoyjRl8pHLQeqBq4Zxxg3v/iM18ff4nQykU85xZ5BK1EgVsJaOcnIUAA1nHJhoValpPy9keCHWN4DP3mETl4+YFh8DGcgnHyB+Lb2tb9wcV2hSnHEflmyDK2L+2e/u6dvbw+PD4fJqwaUkNx7IQCznDN1kqUQ2USXdRHtmNO3UKCd07WR6c0nzIWMKdm4W+PKzP8QcMLtBNEI2jApDzjUBm22RPNoFH7+fiooN0V2J11t4Ow4v5BkTGUT9A+al3I2b/Y2LT082wM3kg1+OpCanFE+YsscNxzbhPNvsY6iuTXazx5BJTR4+VIfZ5immp1eMPlxG3//B5O+sO/qA1f6ODIRxFCB7pJPgmqoeLLWtNFRrqSfkznSCk5rsYsg+rHFdqn3I+DltKslA/rbzghd9t8kdfUaX11Sk/6uL1/jN512EEmCkJocUYXW5+ombs4Y1kpXLhzWf1ly9R/AJXcYy02Nar+V/SxFabEm7NSUMuTrYfKze2OVIBmJs77g11lirnZuwZTgxYFazDlCDRlACOlBGgVaGOa3syBXYlo9TiRTdxd1qWyjHOg0cBFUaJFUSWzpaFFSD7KwU3di67hXW6mHe7HA9UwiSGpyAKsMtNUpKCrK1ndBT14npFyRXxzkrvAGfL19jU6ABegTNqVC8pcoYTnvTdnTs+tGMusfWFc33mALOv4B6512KS5xydV2Y6hW692so37valuerD/TbZtlddF8wVcVy60PZ5qvL5/P5/D8AAAD//wMAYTXFVSUEAAA= \ No newline at end of file diff --git a/.appdata/.dotnet/TelemetryStorageService/20260603133614_e70c32c088de4a64b9cc9ce08899b9db.trn b/.appdata/.dotnet/TelemetryStorageService/20260603133614_e70c32c088de4a64b9cc9ce08899b9db.trn deleted file mode 100644 index 6d031ad..0000000 --- a/.appdata/.dotnet/TelemetryStorageService/20260603133614_e70c32c088de4a64b9cc9ce08899b9db.trn +++ /dev/null @@ -1,5 +0,0 @@ -https://dc.services.visualstudio.com/v2/track -Content-Type:application/x-json-stream -Content-Encoding:gzip - -H4sIAAAAAAAACoxU227cNhD9FYGvFRXeRJF6c20HXaSOjeyiLfo2Ioc2a5laUJSTwPC/F9LauaJAXwRhSM45c+bMPJEED0h6cnY8Xj5iKjOpSYlbSDChKdOUyQOXvdQ9Vw1jXdty/TepSXyHn0lPOuUcdxapRI1UedbSQQZHvUfLGfdMtGrNCbcz6Z8IxMbjY3TYTPMfmOc4JdKTP2Py08cVG2LjxmnxTZ5G3KW5QHIrmfe783f097Obw/XN6daM8/q4iZ70BDhn6AJQiSxQJV2ghllNOzXIgK4N1tjTs5gK5gRjM/v7r/h+KgmL60UjZMOosOS5Jh4KrJQHmPHw+biy2CS6WOP1Ft5++yfyiJn0on4V85TujRvjm4d5WOLo32xfUpNjno6YS8RNjet99ZUEZw1rhBaMkZpcgbuLCavdRXU9biU6wxR6SS0GT5XlQK2SknrZQid06DoRSE3eYU44fpP1Kro8zVMo1YvI1Xc4+7PzywTDiCvGWxhnJDXZzfsCycM4Jbz8hG4pp1yHvKzH51MqMS3TMle7VPA2w8v56/vzJWdMpbqBclf9BvMd6YkF4zhYsADauYAtw8A8A806j9pr9Er4ziurvFaWOa1g4MpDy4ewRkhNfl013C/O4Tx/ZbOZZBw3DtVLn87HiKl8JyPpCeMovDRIg+CaKuOBQist1VrqgNzZTvBXmAPkWyykJ8EMw8CcG4Sy3IGwIH1g0utgheg6UEG2nRJat6wD57tOcOVlhw5kkMwIUpObPPnFlR9bLbcG3OTpH3RllYr0RLO2NQ6GQWjFDViDimtltMXOqgBMSTcAbw0MA2fQMmeNcMC1BTX4NnxR6TLdxoQXcT6O8PkbXNPoRv7iAAwPoH64/jbDA36c8v37k42b95cHUpMPS1o3QrVbLfIxJvpJrxN9tYwlHu4ygkd/NXn82Ue4jku1TWhgQYNTjkouBFVBKzqA5NRytNYa2XHRkZpcTO4ec7U6DGJap+pLsut9dZbdXSzoypJXfn9tPK731c0IJUz54btN8k1hPwrQCMnZ2hmPj6dttHHU3hv0mlOheEuVtZwa23Z06MxgB22wdati10s5LqX6gD5mdGWr+MWLG+b5Hbr7n8V4dcGLRTlbvbY/bbFN2v+1xJ5r8oAwLxkftm3dP5FdSpg35IvlNIq7dBXHMc7opuRn0pvW8EYbLV4Y/uc9LXWjhTXPz8/P/wIAAP//AwDpyqsjGwYAAA== \ No newline at end of file diff --git a/.appdata/.dotnet/TelemetryStorageService/20260603145258_d62464bacee14183959c11a8be8b7c91.trn b/.appdata/.dotnet/TelemetryStorageService/20260603145258_d62464bacee14183959c11a8be8b7c91.trn new file mode 100644 index 0000000..24001f9 --- /dev/null +++ b/.appdata/.dotnet/TelemetryStorageService/20260603145258_d62464bacee14183959c11a8be8b7c91.trn @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACoxUS2/bOBD+KwLPpkJRFCUK2IPhtFgjTWMk7u5ibxQ5jInIpEFRToMi/31BWa6dooe9CaN5fY/hD+TkHlCLlofDpyO4OKAFinYKUUI5JhyTcluwtqJt1eSiFpyU7F+0QPYO3lCLaqZUoQTgEjhgpkmFu9IorDWIghSa0IqlnvJ5QO0PJG2u4WgV5H74C8JgvUMt+ts67V/TbGlz1ftR58H3sHZDlE6lZb6uV3f4y3KzfdicsgYYUnFuNWqRrlmheSOwMlRgxiTBHaEGC6U1F6STpjOnMusiBCf7fNAvl/naRwdRtTSnZU4wFeh9gbSMMq3cyQG2b4e0xUTRbYovpvD02f5ARwiopYszmad2N6q3N/uhG22vb6IMzxBNkHt49eEFjrJHC3QI/gAhWpi4eXjKLisVJCc55ZQQtED3Uu2sg2x9mz30CbBUDWGgSyzAaMxEIbFgZYl1WcmaclPXNAG+g+Cgv+p6b1XwgzcxmynPPsxZeRetG/04ZGsX4TnIeKr7LPsB0AJtxq63w+4RpH7b+scx/XNjn7CsxhDAxWwj4y77Uw471CIhG1VIIYWUXCkDFQFDNJGc1Bq45qAZ1bVmgmnOBFGcya5gWlZFZ1IELdDDGA9jPPP//WqJbbD7PejLBtuJ400vo/Fhv9bgojU2SXPOmAzV9xOqbO656i24+IFk1CJSANVlA9jQgmPWaIllVQrMeckNFErUtDgRZuzz+JMnYoxmpamahneiKwhQUzeM1KYjWplSCV11UNOa1A00QhHSVaxoOqGIhqJiOqELXo8q/mqFchLocXTpOrN1gv1qHf7O2YWRTfDRK98nwWwPT29DhP3vVFv5/cEPNib8Zpb22wD33qW/8/3PnM3FSx8vMUh3kE2nR6uOUMI4po0ymOma4g4kwaKsGK8bIAAKLdCtVy8QsmQwad2kydlTD0/ZMqidjaDiGNLkfyZQJzk/n0/mwkf+9dN25QMsD4fFHP3jmEg6NTsb4MO78m1I6Ts/xCvIM7Qn6M15sSs7aTieniqbglzrBjQvMGVFhZkQBW5EVeOubjrR8QYqdbFr9gjaBlBx6rcNY5o2M/s7Xy5DtEaqOKTj+eLVZKfZn3PKr0teyfaz+jT8SrrZSnOngiTLPp0ezslB/+vdfH9/f/8PAAD//wMABJt80ygGAAA= \ No newline at end of file diff --git a/.appdata/.dotnet/TelemetryStorageService/20260603145308_5500dec2151042f69b2acc6603bae493.trn b/.appdata/.dotnet/TelemetryStorageService/20260603145308_5500dec2151042f69b2acc6603bae493.trn new file mode 100644 index 0000000..8af06a0 --- /dev/null +++ b/.appdata/.dotnet/TelemetryStorageService/20260603145308_5500dec2151042f69b2acc6603bae493.trn @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACpSUwW7cNhCGX0WYsyiTEkWJuhnrBF24qRfJIi16ozijmLBMLUjKiWHsuxfUbpD6VPRCECPO8J9vfuoNvHkmGOD2dPrwQj5FKCG5LVTzWjGuGG+OQg5tM/C+kk0tRdv/DSW4e3qFATpprbCaWEOKmETesrGZLEMkLbhAXrcy1zTfIgxvYFyF9OIsVUv8SiG6xcMAfzqPy/d8t3GVnZcVq7DMtPcxGW+zmD/2u3v2++3h+HC4nIoUc3LlEAbATgpUvWZ2qjWT0nA28npi2iIqzUczjdMlzflEwZu5ivj0635ckqdkh7qqm4qzWsO5BDTJZMmjiXR8PWUVG6K7HC+38LYd3uCFAgx1+RPmpdyNnd3NcxxXN+PNZU0mPsVtnYxNS3iFEk5hOVFIjjY+tzHS8zi/Hk18+ng5k7fxww+yayLcLatPMIBoOihhAzTPJrnFF1eNu9mRT9vHFJyPzv5XrQZKePhSHGaTpiU8v5vHlwvm/f+g/MnYR+ep2N/BAFxQjU1PbKqFYrJHw0zbaKZUoyYSVne1gBJ2i0/Or8sai71P9C1sLcEAH80c6aLvNthHl8imNeQ+/1LZV4ew4GpT8WuWgle8ajjPSWs6ran4TOgC2US5iWNYr/W+vk+pVb0l7dYQyKfiYNJj8ZuJjzCANr0VRhttjLJ2opbTxJEbxTskhYpQ1tih1BKV1NwqaUYh0bRinHIESqBsnWJz69hMulNSsNaaiUkpiBmFxPpOj42Smqwc33EsHuacZ2zPJWHDNE3IpBaGadk0DJvWdLWauq7O/H8iufpB8AwY6eXy7jYFCrEnVILl18yk1oL1uu3Y2PWjHlVPrc2a7yl4mv8F6pOzYYnLlIqrQ4p36D6vPv87is0t351nP7YZ3S32iUKRZ2ycz0/lOtbz+Xz+BwAA//8DAK3VVNOCBAAA \ No newline at end of file diff --git a/.appdata/.dotnet/TelemetryStorageService/20260603145308_8ac143cd27d54693ab5c4e3150472e22.trn b/.appdata/.dotnet/TelemetryStorageService/20260603145308_8ac143cd27d54693ab5c4e3150472e22.trn new file mode 100644 index 0000000..a8262d4 --- /dev/null +++ b/.appdata/.dotnet/TelemetryStorageService/20260603145308_8ac143cd27d54693ab5c4e3150472e22.trn @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACpSTQW/cNhCF/4owZ1EmKYoSdTPWKbpwEy+SRVr0RnFGMWGZWlCUk8DY/x5Qu4GT3noRhAEf571vhq8Q7DNBD7en07sXCmmBEpLfSpJLzbhmvD4K1Td1z7tK1Uoa3v4LJfh7+g49tMo54QyxmjQxhbxhQz06hkhGcIFcNirfab8s0L+C9RXSi3dUzctnioufA/Twtw84f829ra/cNK9YxXmifViSDS6b+bDf3bO/bg/Hh8Pl1EJLFlceoQdslUDdGeZGaZhSlrOBy5EZh6gNH+w4jBeZD4lisFO14NNbf5xToOR6Wcm64kwaOJeANtlsebALHb+fsosN0V2ul1t5++1f4YUi9LL8CfNy3Y2b/M3zMqx+wpvLN9nlKac8xflEMXnamGwxp8kmP4fi2mk3eQoJSnj4VBwmm8Y5Pv8G6tMl//5/xD/m7u++kVsT4W5eQ4IehOJQwnvrHn2gYn8HPXBBEuuO2CiFZqpDy2xTG6Z1rUcSzrRSQAm7OSQf1nldin1I9CVuAaCHP+y00MX6bXSPPpFLa8yp/tF5Fw5xxtWl4o2/4BWvap6dPKzptKbiI6GP5BLlfMe4Xu/7j0RquYl2a4wUUnGw6bH40y6P0IOxnRPWWGOtdm6khtPIkVvNWySNmlBJbFEZhVoZ7rSyg1BoGzGMuQIlUB53sW2YwIGjVophbTRTQ9My46xkrtV80AYFbYjfOBYPU9ZZ13FFWDNDIzJlhGVG1TXDurGt1GPbyqz7ieQ6fcEzYKSXy1vZHGjEjlALJpVomDJGsM40LRvabjCD7qhx2fM9xUDTL6DeexfnZR5TcV2e4jd0H9eQ33uxLdJXH9i3bUZ3s3uiWOQZWx/yel/Hej6fzz8AAAD//wMAtGYnyDYEAAA= \ No newline at end of file diff --git a/.appdata/.dotnet/TelemetryStorageService/20260603145308_b9debcf3f5ee4536bd636f1e091a712f.trn b/.appdata/.dotnet/TelemetryStorageService/20260603145308_b9debcf3f5ee4536bd636f1e091a712f.trn new file mode 100644 index 0000000..40b4084 --- /dev/null +++ b/.appdata/.dotnet/TelemetryStorageService/20260603145308_b9debcf3f5ee4536bd636f1e091a712f.trn @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACpRTwY7cKBD9FYuz8QDG2Pg26tlVWpPstJJRstobpoo0igdaGE8SjfrfV7g7Sia33NCDV/XqveKFBPOEZCS3p9NfzxjyQmqS/QYJJhRlirL2kcuxa0c2NLLVnZTyP1ITf4/fyUh6aS23GmmLCqkE1tGpdZYCoOaMAxOdLDXN54WML8T4BvDZW2zi8hHT4mMgI/nkA8SvpbfxjZ3jCk2KM+7Dkk2wRcw/+909fXt7eHw4XF4tuBRy44GMBHrJQQ2aWic0ldIwOjHhqLYASrPJuMldaD5kTMHMzQJffvaHmANmO4pGtA2jQpNzTcBkUyRPZsHH76eiYrPoruD1Bm/H8YU8YyKjqH+YeSl3Y2d/Y+PTkwlw43zwy5HU5JTiCVP2uNmxTTjPJvsYqmuT3ewxZFKThw/VYTbZxfT0yqMPl9H3fzD5O2OPPmC1vyMjYRwFtANSJ7iicgBDTddqqlSrHHKre8FJTXYxZB/WuC7VPmT8nDaVZCR/m3nBi77bZI8+o81rKtL/VSVr/ObzLkIBGKnJIUVYba5+2s1Zw5qWlcuHNZ/WXL1H8AltxjLTY1qv5X+jCCU20m5NCUOuDiYfqzdmOZKRaDNYbrTRxihrHXYMHQNmFOsBFSgEKaAHqSUoqZlV0kxcgun45ApSdJd0q22hBqE4KieoGGRLpRWc6nZCOvUd4tA5KUT3ytbqYS48YwcmEVqq0QGVmhuqZdtSaDvTC+X6XpQ4flhyTZyz4jfg8+VrbAoUwICgOBWSd1Rqzemgu55O/TDpSQ3Y2aL5HlPA+Rej3nmb4hJdrq4LU72y7v0ayveutuX56gP9tkV2F+0XTFWJ3PhQtvma8vl8Pv8PAAD//wMAZYD86yUEAAA= \ No newline at end of file diff --git a/.appdata/.dotnet/TelemetryStorageService/20260603145308_dfd35309f1174302ac99694cd6e6aa4b.trn b/.appdata/.dotnet/TelemetryStorageService/20260603145308_dfd35309f1174302ac99694cd6e6aa4b.trn new file mode 100644 index 0000000..dd69150 --- /dev/null +++ b/.appdata/.dotnet/TelemetryStorageService/20260603145308_dfd35309f1174302ac99694cd6e6aa4b.trn @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACoxUXW/sJhD9KxavXRzAGBu/pZtcdZUmWd1dtVXfBhgnNA5eYZzcqyj/vcKb3HxcVeqLZQ0w58yZM/NEAtwj6cjp4XD+gCFNZEWSX0KCCUWZoqzac9nVVcfaUlZKK9X8TVbEX+B30pFGWsutRlqhQiodq6mpekudQ80Zd0zUMueEm4l0TwR86fDBWyzH6Q+Mkx8D6cifPrjxMWODL+0wzq6M44CbMCUINpO52qwv6O+n2/319nhrwik/Lr0jHXGN5E61mtpeaColMGqY6Km2zinNDPSmPz7zIWEMMJSTu3vDd2MKmGwnSlGVjApNnlfEQYJM2cCE+++HzGKR6CzHV0t4+e2eyANG0onVq5jHdCd28Cf3k5n94E6WL1mRQxwPGJPHRY3rXfFGgrOSlUIJxsiKXIK99QGLzVlxPeQSwbZMoquoxt5RqTlQLauKuqqGRqi+aUQu8QJjwOFd1ktv4ziNfSpeRC4+4OxO1+cBzIAZ4wsME5IV2Uy7BMHBMAY8/4Z2Tsdc+zjn4/UYkg/zOE/FJiS8ifBy/vp+PceIIRVbSLfFbzDdko5oaC0HDRpAWdtjzbBnjoFijUPlFDopXOOklk5JzaySYLh0UHPT5whZkV+zhrvZWpymNzaLSYZh4VC89Gk9eAzpg4ykI4yjcFWLtBdcUdk6oFBXmipVqR651Y3grzB7iDeYSEf61hjDrDVCam5BaKhczyqnei1E04Dsq7qRQqmaNWBd0wguXdWghaqvWCvIimzj6GabPre6WhqwjeM/aFOWinREsbpuLRgjlOQt6BYlV7JVGhste2CysgZ43YIxnEHNrG6FBa40SOPq/odK5+HGBzzz02GA7+9w21KV1S8WoOU9yE/Xv0S4x8cx3l0dbVxene/JinydQ94IxSZb5NEH+k3lib6ch+T3txHBobscHf7sI8zjUiwTKpVrNUpOeyF6KoWoKZjWUGF45ZwxTOvM5my0dxiL7DDwIU/Vj2TXu+I02luf0KY5Zn5/LTyud8V2gNSP8f7DJnlX2GcBSlFxljvj8OG4jRaOyrkWneJUSF5TqTWnra4baprWaKNarG3meD2nw5yKr+h8RJuWil+8uGCub9He/SzGqwteLMpZ9truuMUWaf/XEntekXuEaY54v2zr7olsQsC4IJ/Nx1HchEs/DH5COwY3kY4zplmpdatfKP73Ra5ZqVQjn5+fn/8FAAD//wMAY5ugbB0GAAA= \ No newline at end of file diff --git a/.appdata/.dotnet/TelemetryStorageService/8869bc85123840e49fff7a7b4ffc0cf2 b/.appdata/.dotnet/TelemetryStorageService/8869bc85123840e49fff7a7b4ffc0cf2 new file mode 100644 index 0000000..4bcb820 --- /dev/null +++ b/.appdata/.dotnet/TelemetryStorageService/8869bc85123840e49fff7a7b4ffc0cf2 @@ -0,0 +1,5 @@ +https://dc.services.visualstudio.com/v2/track +Content-Type:application/x-json-stream +Content-Encoding:gzip + +H4sIAAAAAAAACoxUTW/cNhD9KwKvXcr8EkXq5toOunAdG9lFW/Q2JEc2a5laSJSTwPB/L6i1EztBgV4EYciZ9/jmzTyRBA9IOnJ6OFw8Ysoz2ZAc15BgQlOmKZN7rjpmu0bXlgthW/432ZB4iV9JR1rlPfcWqUSNVAXWUCd7T0NAyxkPTDSq1ITbmXRPBGId8DF6rMf5D5zmOCbSkT9jCuPngg2x9sO4hHoaB9ymOUPyhczH7dkl/f30Zn99c7w141yS6xhIR4J0lmHwVBjtqQLmqHGM0RCM74VAaFpxTIsp45RgqOdw/x0/jDlh9p2ohawZFZY8b0iADIWygxn3Xw+FxSrReYlv1vD62z2RR5xIJzavYh7LnfghnjzMbolDOFm/ZEMO03jAKUdc1bjeVd9JcFazWmjBGNmQK/B3MWG1Pa+uh/JE8IYpDJJa7ANVlgO1SkoaZAOt0H3bip5syCVOCYc3Va+in8Z57HP1InL1Dmd3enaRwA1YMD7AMCPZkO28y5ACDGPCiy/ol3ystZ+Wcnw2phzTMi5ztU0Zbyd4OX/NP1umCVOubiDfVb/BfEc6YsF4DhYsgPa+x4ZhzwIDzdqAOmgMSoQ2KKuCVpZ5rcBxFaDhri8RsiG/Fg13i/c4z9/ZrCYZhpVD9dKnsyFiyu9kJB1hHEWQBmkvuKbKBKDQSEu1lrpH7m0r+CvMHqZbzKQjvXHOMe+dUJZ7EBZk6JkMurdCtC2oXjatElo3rAUf2lZwFWSLHmQvmSm2u5nGsPj8Y6vl2oCbafwHfS5SkY5o1jTGg3NCK27AGlRcK6Mttlb1wJT0DnhjwDnOoGHeGuGBawvKhab/ptJFuo0Jz+N8GODrG1xT61r+4gEM70H9cP3DBA/4eZzuPx5tXH+82JMN+bSkshGqbbHI55joF10m+moZctzfTQgBw9UY8GcfYRmXap3Q1lomnHFUCtNTJVpNnVGBOmwbqaSRvS1SnY/+HqeqOAxiKlP1rdj1rjqd/F3M6PMyFX5/rTyud9XNALkfp4d3m+TNw34UoBaSswIX8PG4jVaOOgSDQXMqFG+ospZTY5uWutY467TBxhfFrpd8WHL1CUOc0Of1xS9eXDHP7tDf/yzGqwteLMpZ8druuMVWaf/XEnvekAeEeZnwYd3W3RPZpoTTiny+HEdxm67iMMQZ/ZjCTDrOhapN06oXhv99z7DaWCmen5+f/wUAAP//AwAsxW2tGwYAAA== \ No newline at end of file diff --git a/.appdata/buildverify/PlotLine.dll b/.appdata/buildverify/PlotLine.dll index 27e1a8e..663c582 100644 Binary files a/.appdata/buildverify/PlotLine.dll and b/.appdata/buildverify/PlotLine.dll differ diff --git a/PlotLine/Controllers/ProjectMetricsController.cs b/PlotLine/Controllers/ProjectMetricsController.cs new file mode 100644 index 0000000..f62f66e --- /dev/null +++ b/PlotLine/Controllers/ProjectMetricsController.cs @@ -0,0 +1,89 @@ +using Microsoft.AspNetCore.Mvc; +using PlotLine.Services; +using PlotLine.ViewModels; + +namespace PlotLine.Controllers; + +public sealed class ProjectMetricsController(ISceneMetricTypeService metrics) : Controller +{ + public async Task Index(int projectId) + { + var model = await metrics.GetAsync(projectId); + return model is null ? NotFound() : View(model); + } + + public async Task Edit(int projectId, int id) + { + var model = await metrics.GetEditAsync(projectId, id); + return model is null ? NotFound() : View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Save(SceneMetricTypeEditViewModel model) + { + if (model.MinValue >= model.MaxValue) + { + ModelState.AddModelError(nameof(model.MaxValue), "Maximum value must be greater than minimum value."); + } + + if (model.DefaultValue < model.MinValue || model.DefaultValue > model.MaxValue) + { + ModelState.AddModelError(nameof(model.DefaultValue), "Default value must sit inside the metric range."); + } + + if (!ModelState.IsValid) + { + return model.MetricTypeID == 0 + ? RedirectToAction(nameof(Index), new { projectId = model.ProjectID }) + : View("Edit", model); + } + + await metrics.SaveAsync(model); + TempData["ProjectMetricMessage"] = model.MetricTypeID == 0 ? "Scene metric added." : "Scene metric updated."; + return RedirectToAction(nameof(Index), new { projectId = model.ProjectID }); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task AddMissingDefaults(int projectId) + { + await metrics.AddMissingDefaultsAsync(projectId); + TempData["ProjectMetricMessage"] = "Missing default scene metrics added."; + return RedirectToAction(nameof(Index), new { projectId }); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task SetActive(int projectId, int id, bool isActive) + { + var updated = await metrics.SetActiveAsync(projectId, id, isActive); + TempData["ProjectMetricMessage"] = updated + ? (isActive ? "Metric reactivated." : "Metric deactivated.") + : (isActive ? "Metric could not be reactivated." : "Metric could not be deactivated."); + return RedirectToAction(nameof(Index), new { projectId }); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Move(int projectId, int id, string direction) + { + direction = string.Equals(direction, "Up", StringComparison.OrdinalIgnoreCase) ? "Up" : "Down"; + var moved = await metrics.MoveAsync(projectId, id, direction); + TempData["ProjectMetricMessage"] = moved + ? $"Metric moved {direction.ToLowerInvariant()}." + : $"Metric could not be moved {direction.ToLowerInvariant()} because it is already at the edge of its group."; + return RedirectToAction(nameof(Index), new { projectId }); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Remove(int projectId, int id) + { + var removed = await metrics.RemoveUnusedAsync(projectId, id); + TempData["ProjectMetricMessage"] = removed + ? "Unused metric removed." + : "Metric cannot be removed because scene values exist or the metric is still active."; + return RedirectToAction(nameof(Index), new { projectId }); + } +} diff --git a/PlotLine/Data/Repositories.cs b/PlotLine/Data/Repositories.cs index 6efc0d7..31c7cd3 100644 --- a/PlotLine/Data/Repositories.cs +++ b/PlotLine/Data/Repositories.cs @@ -72,6 +72,18 @@ public interface ITimelineSettingsRepository Task SaveAsync(ProjectTimelineSettings settings, IEnumerable metricTypeIds); } +public interface ISceneMetricTypeRepository +{ + Task> ListForManagementAsync(int projectId); + Task GetAsync(int metricTypeId, int projectId); + Task SaveAsync(SceneMetricType metric); + Task SetActiveAsync(int metricTypeId, int projectId, bool isActive); + Task MoveAsync(int metricTypeId, int projectId, string direction); + Task AddMissingDefaultsAsync(int projectId); + Task RemoveUnusedAsync(int metricTypeId, int projectId); + Task HasProjectMetricsAsync(int projectId); +} + public interface IPlotRepository { Task GetLookupsAsync(); @@ -1465,6 +1477,87 @@ public sealed class TimelineSettingsRepository(ISqlConnectionFactory connectionF } } +public sealed class SceneMetricTypeRepository(ISqlConnectionFactory connectionFactory) : ISceneMetricTypeRepository +{ + public async Task> ListForManagementAsync(int projectId) + { + using var connection = connectionFactory.CreateConnection(); + var rows = await connection.QueryAsync( + "dbo.SceneMetricType_ListForProjectManagement", + new { ProjectID = projectId }, + commandType: CommandType.StoredProcedure); + return rows.ToList(); + } + + public async Task GetAsync(int metricTypeId, int projectId) + { + using var connection = connectionFactory.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync( + "dbo.SceneMetricType_GetForProject", + new { MetricTypeID = metricTypeId, ProjectID = projectId }, + commandType: CommandType.StoredProcedure); + } + + public async Task SaveAsync(SceneMetricType metric) + { + using var connection = connectionFactory.CreateConnection(); + return await connection.QuerySingleAsync( + "dbo.SceneMetricType_SaveForProject", + new + { + metric.MetricTypeID, + metric.ProjectID, + metric.MetricName, + metric.Description, + metric.MinValue, + metric.MaxValue, + metric.DefaultValue, + metric.SortOrder, + metric.IsActive + }, + commandType: CommandType.StoredProcedure); + } + + public async Task SetActiveAsync(int metricTypeId, int projectId, bool isActive) + { + using var connection = connectionFactory.CreateConnection(); + return await connection.QuerySingleAsync( + "dbo.SceneMetricType_SetActiveForProject", + new { MetricTypeID = metricTypeId, ProjectID = projectId, IsActive = isActive }, + commandType: CommandType.StoredProcedure); + } + + public async Task MoveAsync(int metricTypeId, int projectId, string direction) + { + using var connection = connectionFactory.CreateConnection(); + return await connection.QuerySingleAsync( + "dbo.SceneMetricType_MoveForProject", + new { MetricTypeID = metricTypeId, ProjectID = projectId, Direction = direction }, + commandType: CommandType.StoredProcedure); + } + + public async Task AddMissingDefaultsAsync(int projectId) + { + using var connection = connectionFactory.CreateConnection(); + await connection.ExecuteAsync("dbo.SceneMetricType_AddMissingDefaultsForProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); + } + + public async Task RemoveUnusedAsync(int metricTypeId, int projectId) + { + using var connection = connectionFactory.CreateConnection(); + return await connection.QuerySingleAsync( + "dbo.SceneMetricType_RemoveUnusedForProject", + new { MetricTypeID = metricTypeId, ProjectID = projectId }, + commandType: CommandType.StoredProcedure); + } + + public async Task HasProjectMetricsAsync(int projectId) + { + using var connection = connectionFactory.CreateConnection(); + return await connection.QuerySingleAsync("dbo.SceneMetricType_ProjectHasMetrics", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); + } +} + public sealed class TimelinePresetRepository(ISqlConnectionFactory connectionFactory) : ITimelinePresetRepository { public async Task> ListAsync(int projectId, int? bookId) diff --git a/PlotLine/Models/CoreModels.cs b/PlotLine/Models/CoreModels.cs index 607245d..67da506 100644 --- a/PlotLine/Models/CoreModels.cs +++ b/PlotLine/Models/CoreModels.cs @@ -152,6 +152,8 @@ public sealed class SceneMetricType public int DefaultValue { get; set; } public int SortOrder { get; set; } public bool IsActive { get; set; } + public bool IsEnabledForProject { get; set; } + public int ValueCount { get; set; } } public sealed class SceneMetricValue diff --git a/PlotLine/Program.cs b/PlotLine/Program.cs index 7a7de88..1433993 100644 --- a/PlotLine/Program.cs +++ b/PlotLine/Program.cs @@ -26,6 +26,7 @@ public class Program builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -46,6 +47,7 @@ public class Program builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/PlotLine/Services/CoreServices.cs b/PlotLine/Services/CoreServices.cs index 2faf378..5dd226b 100644 --- a/PlotLine/Services/CoreServices.cs +++ b/PlotLine/Services/CoreServices.cs @@ -150,6 +150,17 @@ public interface ITimelineService Task AddLocationRelationshipByDropAsync(TimelineLocationRelationshipDropViewModel model); } +public interface ISceneMetricTypeService +{ + Task GetAsync(int projectId); + Task GetEditAsync(int projectId, int metricTypeId); + Task SaveAsync(SceneMetricTypeEditViewModel model); + Task SetActiveAsync(int projectId, int metricTypeId, bool isActive); + Task MoveAsync(int projectId, int metricTypeId, string direction); + Task AddMissingDefaultsAsync(int projectId); + Task RemoveUnusedAsync(int projectId, int metricTypeId); +} + public interface IAnalyticsService { Task GetStoryHealthAsync(int projectId, int? bookId, decimal? startChapterNumber, decimal? endChapterNumber, decimal? startSceneNumber, decimal? endSceneNumber); @@ -1954,6 +1965,63 @@ public sealed class ContinuityValidationService( public Task RestoreAsync(int warningId) => warnings.RestoreAsync(warningId); } +public sealed class SceneMetricTypeService(IProjectRepository projects, ISceneMetricTypeRepository metricTypes) : ISceneMetricTypeService +{ + public async Task GetAsync(int projectId) + { + var project = await projects.GetAsync(projectId); + if (project is null) + { + return null; + } + + return new ProjectSceneMetricsViewModel + { + Project = project, + Metrics = await metricTypes.ListForManagementAsync(projectId), + NewMetric = new SceneMetricTypeEditViewModel { ProjectID = projectId } + }; + } + + public async Task GetEditAsync(int projectId, int metricTypeId) + { + var metric = await metricTypes.GetAsync(metricTypeId, projectId); + return metric is null ? null : new SceneMetricTypeEditViewModel + { + MetricTypeID = metric.MetricTypeID, + ProjectID = projectId, + MetricName = metric.MetricName, + Description = metric.Description, + MinValue = metric.MinValue, + MaxValue = metric.MaxValue, + DefaultValue = metric.DefaultValue, + SortOrder = metric.SortOrder, + IsActive = metric.IsActive + }; + } + + public Task SaveAsync(SceneMetricTypeEditViewModel model) => metricTypes.SaveAsync(new SceneMetricType + { + MetricTypeID = model.MetricTypeID, + ProjectID = model.ProjectID, + MetricName = model.MetricName, + Description = model.Description, + MinValue = model.MinValue, + MaxValue = model.MaxValue, + DefaultValue = model.DefaultValue, + SortOrder = model.SortOrder, + IsActive = model.IsActive + }); + + public Task SetActiveAsync(int projectId, int metricTypeId, bool isActive) => metricTypes.SetActiveAsync(metricTypeId, projectId, isActive); + + public Task MoveAsync(int projectId, int metricTypeId, string direction) => metricTypes.MoveAsync(metricTypeId, projectId, direction); + + public Task AddMissingDefaultsAsync(int projectId) => metricTypes.AddMissingDefaultsAsync(projectId); + + public Task RemoveUnusedAsync(int projectId, int metricTypeId) => metricTypes.RemoveUnusedAsync(metricTypeId, projectId); +} + public sealed class AnalyticsService(IProjectRepository projects, IBookRepository books, IAnalyticsRepository analytics) : IAnalyticsService { public async Task GetStoryHealthAsync(int projectId, int? bookId, decimal? startChapterNumber, decimal? endChapterNumber, decimal? startSceneNumber, decimal? endSceneNumber) diff --git a/PlotLine/Sql/029_ProjectSceneMetricsManagement.sql b/PlotLine/Sql/029_ProjectSceneMetricsManagement.sql new file mode 100644 index 0000000..b59fcff --- /dev/null +++ b/PlotLine/Sql/029_ProjectSceneMetricsManagement.sql @@ -0,0 +1,430 @@ +SET ANSI_NULLS ON; +GO +SET QUOTED_IDENTIFIER ON; +GO + +CREATE OR ALTER PROCEDURE dbo.SceneMetricType_ProjectHasMetrics + @ProjectID int +AS +BEGIN + SET NOCOUNT ON; + + SELECT CAST(CASE WHEN EXISTS + ( + SELECT 1 + FROM dbo.ProjectTimelineMetricSettings ptms + INNER JOIN dbo.SceneMetricTypes mt ON mt.MetricTypeID = ptms.MetricTypeID + WHERE ptms.ProjectID = @ProjectID AND mt.IsActive = 1 + ) THEN 1 ELSE 0 END AS bit); +END; +GO + +CREATE OR ALTER PROCEDURE dbo.SceneMetricType_AddMissingDefaultsForProject + @ProjectID int +AS +BEGIN + SET NOCOUNT ON; + + IF NOT EXISTS (SELECT 1 FROM dbo.ProjectTimelineSettings WHERE ProjectID = @ProjectID) + BEGIN + INSERT dbo.ProjectTimelineSettings + (ProjectID, ShowSceneCards, ShowMetricShape, ShowPlotLines, ShowStoryAssets, ShowCharacterAppearances, ShowWarnings) + VALUES + (@ProjectID, 1, 1, 1, 1, 1, 1); + END; + + INSERT dbo.ProjectTimelineMetricSettings (ProjectID, MetricTypeID, SortOrder) + SELECT @ProjectID, mt.MetricTypeID, mt.SortOrder + FROM dbo.SceneMetricTypes mt + WHERE mt.ProjectID IS NULL + AND mt.IsActive = 1 + AND mt.MetricName IN + ( + N'Overall Intensity', N'Tension', N'Emotional Weight', N'Action', N'Darkness', + N'Romance', N'Sexual Charge', N'Violence', N'Hope / Lightness', N'Comedy', N'Mystery' + ) + AND NOT EXISTS + ( + SELECT 1 + FROM dbo.ProjectTimelineMetricSettings ptms + INNER JOIN dbo.SceneMetricTypes existing ON existing.MetricTypeID = ptms.MetricTypeID + WHERE ptms.ProjectID = @ProjectID + AND LOWER(existing.MetricName) = LOWER(mt.MetricName) + ); +END; +GO + +CREATE OR ALTER PROCEDURE dbo.SceneMetricType_ListForProjectManagement + @ProjectID int +AS +BEGIN + SET NOCOUNT ON; + + ;WITH CandidateMetrics AS + ( + SELECT mt.* + FROM dbo.SceneMetricTypes mt + WHERE mt.ProjectID = @ProjectID + UNION + SELECT mt.* + FROM dbo.SceneMetricTypes mt + WHERE mt.ProjectID IS NULL + AND mt.MetricName IN + ( + N'Overall Intensity', N'Tension', N'Emotional Weight', N'Action', N'Darkness', + N'Romance', N'Sexual Charge', N'Violence', N'Hope / Lightness', N'Comedy', N'Mystery' + ) + UNION + SELECT mt.* + FROM dbo.SceneMetricTypes mt + INNER JOIN dbo.ProjectTimelineMetricSettings ptms ON ptms.MetricTypeID = mt.MetricTypeID + WHERE ptms.ProjectID = @ProjectID + ) + SELECT cm.MetricTypeID, cm.ProjectID, cm.MetricName, cm.Description, cm.MinValue, cm.MaxValue, cm.DefaultValue, + COALESCE(ptms.SortOrder, cm.SortOrder) AS SortOrder, + CAST(CASE WHEN cm.IsActive = 1 AND ptms.MetricTypeID IS NOT NULL THEN 1 ELSE 0 END AS bit) AS IsActive, + CAST(CASE WHEN cm.IsActive = 1 AND ptms.MetricTypeID IS NOT NULL THEN 1 ELSE 0 END AS bit) AS IsEnabledForProject, + COUNT(smv.SceneID) AS ValueCount + FROM CandidateMetrics cm + LEFT JOIN dbo.ProjectTimelineMetricSettings ptms ON ptms.ProjectID = @ProjectID AND ptms.MetricTypeID = cm.MetricTypeID + LEFT JOIN dbo.SceneMetricValues smv ON smv.MetricTypeID = cm.MetricTypeID + GROUP BY cm.MetricTypeID, cm.ProjectID, cm.MetricName, cm.Description, cm.MinValue, cm.MaxValue, cm.DefaultValue, + cm.SortOrder, cm.IsActive, ptms.MetricTypeID, ptms.SortOrder + ORDER BY + CASE WHEN cm.IsActive = 1 AND ptms.MetricTypeID IS NOT NULL THEN 0 ELSE 1 END, + COALESCE(ptms.SortOrder, cm.SortOrder), + cm.MetricName; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.SceneMetricType_GetForProject + @MetricTypeID int, + @ProjectID int +AS +BEGIN + SET NOCOUNT ON; + + SELECT mt.MetricTypeID, mt.ProjectID, mt.MetricName, mt.Description, mt.MinValue, mt.MaxValue, mt.DefaultValue, + COALESCE(ptms.SortOrder, mt.SortOrder) AS SortOrder, + CAST(CASE WHEN mt.IsActive = 1 AND ptms.MetricTypeID IS NOT NULL THEN 1 ELSE 0 END AS bit) AS IsActive, + CAST(CASE WHEN mt.IsActive = 1 AND ptms.MetricTypeID IS NOT NULL THEN 1 ELSE 0 END AS bit) AS IsEnabledForProject, + (SELECT COUNT(*) FROM dbo.SceneMetricValues smv WHERE smv.MetricTypeID = mt.MetricTypeID) AS ValueCount + FROM dbo.SceneMetricTypes mt + LEFT JOIN dbo.ProjectTimelineMetricSettings ptms ON ptms.ProjectID = @ProjectID AND ptms.MetricTypeID = mt.MetricTypeID + WHERE mt.MetricTypeID = @MetricTypeID + AND (mt.ProjectID IS NULL OR mt.ProjectID = @ProjectID); +END; +GO + +CREATE OR ALTER PROCEDURE dbo.SceneMetricType_SaveForProject + @MetricTypeID int = NULL, + @ProjectID int, + @MetricName nvarchar(100), + @Description nvarchar(500) = NULL, + @MinValue int, + @MaxValue int, + @DefaultValue int, + @SortOrder int, + @IsActive bit = 1 +AS +BEGIN + SET NOCOUNT ON; + + IF @MinValue >= @MaxValue + THROW 50010, 'Metric minimum must be less than maximum.', 1; + + IF @DefaultValue < @MinValue OR @DefaultValue > @MaxValue + THROW 50011, 'Metric default value must be inside the metric range.', 1; + + IF @MetricTypeID IS NULL OR @MetricTypeID = 0 + BEGIN + SELECT @MetricTypeID = MetricTypeID + FROM dbo.SceneMetricTypes + WHERE ProjectID = @ProjectID AND LOWER(MetricName) = LOWER(@MetricName); + + IF @MetricTypeID IS NULL OR @MetricTypeID = 0 + BEGIN + INSERT dbo.SceneMetricTypes (ProjectID, MetricName, Description, MinValue, MaxValue, DefaultValue, SortOrder, IsActive) + VALUES (@ProjectID, @MetricName, @Description, @MinValue, @MaxValue, @DefaultValue, @SortOrder, 1); + SET @MetricTypeID = CAST(SCOPE_IDENTITY() AS int); + END + END + + UPDATE dbo.SceneMetricTypes + SET MetricName = @MetricName, + Description = @Description, + MinValue = @MinValue, + MaxValue = @MaxValue, + DefaultValue = @DefaultValue, + SortOrder = @SortOrder, + IsActive = 1 + WHERE MetricTypeID = @MetricTypeID + AND (ProjectID = @ProjectID OR ProjectID IS NULL); + + IF @IsActive = 1 + BEGIN + MERGE dbo.ProjectTimelineMetricSettings AS target + USING (SELECT @ProjectID AS ProjectID, @MetricTypeID AS MetricTypeID, @SortOrder AS SortOrder) AS source + ON target.ProjectID = source.ProjectID AND target.MetricTypeID = source.MetricTypeID + WHEN MATCHED THEN UPDATE SET SortOrder = source.SortOrder + WHEN NOT MATCHED THEN INSERT (ProjectID, MetricTypeID, SortOrder) VALUES (source.ProjectID, source.MetricTypeID, source.SortOrder); + END + ELSE + BEGIN + DELETE FROM dbo.ProjectTimelineMetricSettings WHERE ProjectID = @ProjectID AND MetricTypeID = @MetricTypeID; + END; + + SELECT @MetricTypeID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.SceneMetricType_SetActiveForProject + @MetricTypeID int, + @ProjectID int, + @IsActive bit +AS +BEGIN + SET NOCOUNT ON; + + IF @IsActive = 1 + BEGIN + UPDATE dbo.SceneMetricTypes + SET IsActive = 1 + WHERE MetricTypeID = @MetricTypeID + AND ProjectID = @ProjectID; + + INSERT dbo.ProjectTimelineMetricSettings (ProjectID, MetricTypeID, SortOrder) + SELECT @ProjectID, mt.MetricTypeID, mt.SortOrder + FROM dbo.SceneMetricTypes mt + WHERE mt.MetricTypeID = @MetricTypeID + AND mt.IsActive = 1 + AND (mt.ProjectID IS NULL OR mt.ProjectID = @ProjectID) + AND NOT EXISTS + ( + SELECT 1 FROM dbo.ProjectTimelineMetricSettings ptms + WHERE ptms.ProjectID = @ProjectID AND ptms.MetricTypeID = @MetricTypeID + ); + + SELECT CAST(CASE WHEN EXISTS + ( + SELECT 1 + FROM dbo.ProjectTimelineMetricSettings ptms + INNER JOIN dbo.SceneMetricTypes mt ON mt.MetricTypeID = ptms.MetricTypeID + WHERE ptms.ProjectID = @ProjectID + AND ptms.MetricTypeID = @MetricTypeID + AND mt.IsActive = 1 + AND (mt.ProjectID IS NULL OR mt.ProjectID = @ProjectID) + ) THEN 1 ELSE 0 END AS bit); + RETURN; + END + ELSE + BEGIN + DELETE FROM dbo.ProjectTimelineMetricSettings + WHERE ProjectID = @ProjectID AND MetricTypeID = @MetricTypeID; + + UPDATE dbo.SceneMetricTypes + SET IsActive = 0 + WHERE MetricTypeID = @MetricTypeID AND ProjectID = @ProjectID; + + SELECT CAST(CASE WHEN NOT EXISTS + ( + SELECT 1 + FROM dbo.ProjectTimelineMetricSettings + WHERE ProjectID = @ProjectID AND MetricTypeID = @MetricTypeID + ) THEN 1 ELSE 0 END AS bit); + RETURN; + END; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.SceneMetricType_MoveForProject + @MetricTypeID int, + @ProjectID int, + @Direction nvarchar(10) +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @NormalisedDirection nvarchar(10) = CASE WHEN UPPER(@Direction) = N'UP' THEN N'Up' ELSE N'Down' END; + + ;WITH CandidateMetrics AS + ( + SELECT mt.MetricTypeID, mt.ProjectID, mt.MetricName, + COALESCE(ptms.SortOrder, mt.SortOrder) AS SortOrder, + CAST(CASE WHEN mt.IsActive = 1 AND ptms.MetricTypeID IS NOT NULL THEN 1 ELSE 0 END AS bit) AS IsEnabledForProject, + CAST(CASE WHEN mt.IsActive = 1 AND ptms.MetricTypeID IS NOT NULL THEN 0 ELSE 1 END AS int) AS GroupRank + FROM dbo.SceneMetricTypes mt + LEFT JOIN dbo.ProjectTimelineMetricSettings ptms ON ptms.ProjectID = @ProjectID AND ptms.MetricTypeID = mt.MetricTypeID + WHERE + mt.ProjectID = @ProjectID + OR ptms.ProjectID = @ProjectID + OR + ( + mt.ProjectID IS NULL + AND mt.MetricName IN + ( + N'Overall Intensity', N'Tension', N'Emotional Weight', N'Action', N'Darkness', + N'Romance', N'Sexual Charge', N'Violence', N'Hope / Lightness', N'Comedy', N'Mystery' + ) + ) + ) + SELECT *, + ROW_NUMBER() OVER (PARTITION BY GroupRank ORDER BY SortOrder, MetricName, MetricTypeID) AS RowNumber, + ROW_NUMBER() OVER (PARTITION BY GroupRank ORDER BY SortOrder, MetricName, MetricTypeID) * 10 AS NormalisedSortOrder + INTO #OrderedMetrics + FROM CandidateMetrics; + + UPDATE ptms + SET SortOrder = ordered.NormalisedSortOrder + FROM dbo.ProjectTimelineMetricSettings ptms + INNER JOIN #OrderedMetrics ordered ON ordered.MetricTypeID = ptms.MetricTypeID + WHERE ptms.ProjectID = @ProjectID + AND ordered.IsEnabledForProject = 1; + + UPDATE mt + SET SortOrder = ordered.NormalisedSortOrder + FROM dbo.SceneMetricTypes mt + INNER JOIN #OrderedMetrics ordered ON ordered.MetricTypeID = mt.MetricTypeID + WHERE mt.ProjectID = @ProjectID + AND ordered.IsEnabledForProject = 0; + + ;WITH OrderedMetrics AS + ( + SELECT MetricTypeID, ProjectID, MetricName, NormalisedSortOrder AS SortOrder, IsEnabledForProject, GroupRank, RowNumber + FROM #OrderedMetrics + ), + MovePair AS + ( + SELECT currentMetric.MetricTypeID AS CurrentMetricTypeID, + currentMetric.ProjectID AS CurrentProjectID, + currentMetric.IsEnabledForProject AS CurrentIsEnabledForProject, + currentMetric.SortOrder AS CurrentSortOrder, + adjacentMetric.MetricTypeID AS AdjacentMetricTypeID, + adjacentMetric.ProjectID AS AdjacentProjectID, + adjacentMetric.IsEnabledForProject AS AdjacentIsEnabledForProject, + adjacentMetric.SortOrder AS AdjacentSortOrder + FROM OrderedMetrics currentMetric + INNER JOIN OrderedMetrics adjacentMetric ON adjacentMetric.GroupRank = currentMetric.GroupRank + AND adjacentMetric.RowNumber = CASE WHEN @NormalisedDirection = N'Up' THEN currentMetric.RowNumber - 1 ELSE currentMetric.RowNumber + 1 END + WHERE currentMetric.MetricTypeID = @MetricTypeID + ) + SELECT * + INTO #MovePair + FROM MovePair; + + IF NOT EXISTS (SELECT 1 FROM #MovePair) + BEGIN + SELECT CAST(0 AS bit); + RETURN; + END; + + IF EXISTS (SELECT 1 FROM #MovePair WHERE CurrentIsEnabledForProject = 0 AND CurrentProjectID IS NULL) + BEGIN + SELECT CAST(0 AS bit); + RETURN; + END; + + IF EXISTS (SELECT 1 FROM #MovePair WHERE AdjacentIsEnabledForProject = 0 AND AdjacentProjectID IS NULL) + BEGIN + SELECT CAST(0 AS bit); + RETURN; + END; + + UPDATE ptms + SET SortOrder = mp.AdjacentSortOrder + FROM dbo.ProjectTimelineMetricSettings ptms + INNER JOIN #MovePair mp ON mp.CurrentMetricTypeID = ptms.MetricTypeID + WHERE ptms.ProjectID = @ProjectID + AND mp.CurrentIsEnabledForProject = 1; + + UPDATE mt + SET SortOrder = mp.AdjacentSortOrder + FROM dbo.SceneMetricTypes mt + INNER JOIN #MovePair mp ON mp.CurrentMetricTypeID = mt.MetricTypeID + WHERE mt.ProjectID = @ProjectID + AND mp.CurrentIsEnabledForProject = 0; + + UPDATE ptms + SET SortOrder = mp.CurrentSortOrder + FROM dbo.ProjectTimelineMetricSettings ptms + INNER JOIN #MovePair mp ON mp.AdjacentMetricTypeID = ptms.MetricTypeID + WHERE ptms.ProjectID = @ProjectID + AND mp.AdjacentIsEnabledForProject = 1; + + UPDATE mt + SET SortOrder = mp.CurrentSortOrder + FROM dbo.SceneMetricTypes mt + INNER JOIN #MovePair mp ON mp.AdjacentMetricTypeID = mt.MetricTypeID + WHERE mt.ProjectID = @ProjectID + AND mp.AdjacentIsEnabledForProject = 0; + + SELECT CAST(1 AS bit); +END; +GO + +CREATE OR ALTER PROCEDURE dbo.SceneMetricType_RemoveUnusedForProject + @MetricTypeID int, + @ProjectID int +AS +BEGIN + SET NOCOUNT ON; + + IF EXISTS + ( + SELECT 1 + FROM dbo.SceneMetricTypes mt + WHERE mt.MetricTypeID = @MetricTypeID + AND mt.ProjectID = @ProjectID + AND mt.IsActive = 0 + AND NOT EXISTS (SELECT 1 FROM dbo.SceneMetricValues smv WHERE smv.MetricTypeID = mt.MetricTypeID) + ) + BEGIN + DELETE FROM dbo.ProjectTimelineMetricSettings + WHERE ProjectID = @ProjectID AND MetricTypeID = @MetricTypeID; + + DELETE FROM dbo.SceneMetricTypes + WHERE MetricTypeID = @MetricTypeID AND ProjectID = @ProjectID; + + SELECT CAST(1 AS bit); + RETURN; + END; + + SELECT CAST(0 AS bit); +END; +GO + +CREATE OR ALTER PROCEDURE dbo.ProjectTimelineSettings_EnsureDefaults + @ProjectID int +AS +BEGIN + SET NOCOUNT ON; + + IF NOT EXISTS (SELECT 1 FROM dbo.Projects WHERE ProjectID = @ProjectID AND IsArchived = 0) + RETURN; + + IF NOT EXISTS (SELECT 1 FROM dbo.ProjectTimelineSettings WHERE ProjectID = @ProjectID) + BEGIN + INSERT dbo.ProjectTimelineSettings + (ProjectID, ShowSceneCards, ShowMetricShape, ShowPlotLines, ShowStoryAssets, ShowCharacterAppearances, ShowWarnings) + VALUES + (@ProjectID, 1, 1, 1, 1, 1, 1); + END; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.ProjectTimelineSettings_MetricTypes + @ProjectID int +AS +BEGIN + SET NOCOUNT ON; + + SELECT mt.MetricTypeID, mt.ProjectID, mt.MetricName, mt.Description, mt.MinValue, mt.MaxValue, mt.DefaultValue, + ptms.SortOrder, mt.IsActive + FROM dbo.ProjectTimelineMetricSettings ptms + INNER JOIN dbo.SceneMetricTypes mt ON mt.MetricTypeID = ptms.MetricTypeID + WHERE ptms.ProjectID = @ProjectID + AND mt.IsActive = 1 + AND (mt.ProjectID IS NULL OR mt.ProjectID = @ProjectID) + ORDER BY ptms.SortOrder, mt.SortOrder, mt.MetricName; +END; +GO diff --git a/PlotLine/ViewModels/CoreViewModels.cs b/PlotLine/ViewModels/CoreViewModels.cs index 5744873..8698009 100644 --- a/PlotLine/ViewModels/CoreViewModels.cs +++ b/PlotLine/ViewModels/CoreViewModels.cs @@ -447,6 +447,41 @@ public sealed class TimelineMetricSettingOptionViewModel public bool IsSelected { get; set; } } +public sealed class ProjectSceneMetricsViewModel +{ + public Project Project { get; set; } = new(); + public IReadOnlyList Metrics { get; set; } = []; + public SceneMetricTypeEditViewModel NewMetric { get; set; } = new(); + public bool HasEnabledMetrics => Metrics.Any(x => x.IsEnabledForProject && x.IsActive); +} + +public sealed class SceneMetricTypeEditViewModel +{ + public int MetricTypeID { get; set; } + public int ProjectID { get; set; } + + [Required, StringLength(100)] + [Display(Name = "Metric name")] + public string MetricName { get; set; } = string.Empty; + + public string? Description { get; set; } + + [Display(Name = "Minimum value")] + public int MinValue { get; set; } = 1; + + [Display(Name = "Maximum value")] + public int MaxValue { get; set; } = 10; + + [Display(Name = "Default value")] + public int DefaultValue { get; set; } = 5; + + [Display(Name = "Sort order")] + public int SortOrder { get; set; } = 100; + + [Display(Name = "Active")] + public bool IsActive { get; set; } = true; +} + public sealed class StoryBibleViewModel { public Project Project { get; set; } = new(); diff --git a/PlotLine/Views/ProjectMetrics/Edit.cshtml b/PlotLine/Views/ProjectMetrics/Edit.cshtml new file mode 100644 index 0000000..2b4f3a9 --- /dev/null +++ b/PlotLine/Views/ProjectMetrics/Edit.cshtml @@ -0,0 +1,61 @@ +@model SceneMetricTypeEditViewModel +@{ + ViewData["Title"] = "Edit Scene Metric"; +} + +
+
+

Project Settings

+

Edit scene metric

+
+ Back to metrics +
+ +
+ + +
+ +
+ + + +
+
+ + +
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + +
+
+
+ + +
+
+ + Cancel +
+
+ +@section Scripts { + +} diff --git a/PlotLine/Views/ProjectMetrics/Index.cshtml b/PlotLine/Views/ProjectMetrics/Index.cshtml new file mode 100644 index 0000000..7af8465 --- /dev/null +++ b/PlotLine/Views/ProjectMetrics/Index.cshtml @@ -0,0 +1,182 @@ +@model ProjectSceneMetricsViewModel +@{ + ViewData["Title"] = "Scene Metrics"; +} + + + +
+
+

Project Settings

+

Scene Metrics

+

Create and manage the metric sliders available when editing scenes.

+
+ +
+ +@if (TempData["ProjectMetricMessage"] is string message) +{ +
@message
+} + +
+
+
+

Defaults

+

Add missing default metrics

+
+
+

Adds any missing core metrics for this project without duplicating existing metrics.

+
+ + +
+
+ +
+
+
+

New Metric

+

Add scene metric

+
+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+
+ +
+
+
+

Available Metrics

+

Project scene metrics

+
+ @Model.Metrics.Count metric@(Model.Metrics.Count == 1 ? "" : "s") +
+ + @if (!Model.Metrics.Any()) + { +
+

This project has no scene metrics yet.

+
+ + +
+
+ } + else + { +
+ + + + + + + + + + + + + + @foreach (var metric in Model.Metrics) + { + var canMove = metric.IsEnabledForProject || metric.ProjectID == Model.Project.ProjectID; + var canRemove = !metric.IsActive && metric.ProjectID == Model.Project.ProjectID && metric.ValueCount == 0; + + + + + + + + + + } + +
MetricRangeDefaultSortStatusValuesActions
+ @metric.MetricName + @if (!string.IsNullOrWhiteSpace(metric.Description)) + { +
@metric.Description
+ } +
@metric.MinValue-@metric.MaxValue@metric.DefaultValue@metric.SortOrder + @(metric.IsActive ? "Active" : "Inactive") + @metric.ValueCount +
+
+ + + + +
+
+ + + + +
+ Edit +
+ + + + +
+ @if (canRemove) + { +
+ + + +
+ } +
+
+
+ } +
diff --git a/PlotLine/Views/Projects/Details.cshtml b/PlotLine/Views/Projects/Details.cshtml index 47d4fc5..d18101a 100644 --- a/PlotLine/Views/Projects/Details.cshtml +++ b/PlotLine/Views/Projects/Details.cshtml @@ -16,6 +16,7 @@