diff --git a/API/Controllers/StatsController.cs b/API/Controllers/StatsController.cs index a53c306f8..d16293d0e 100644 --- a/API/Controllers/StatsController.cs +++ b/API/Controllers/StatsController.cs @@ -121,6 +121,14 @@ public class StatsController : BaseApiController return Ok(await _statService.ReadCountByDay(userId, days)); } + [HttpGet("day-breakdown")] + [Authorize("RequireAdminRole")] + [ResponseCache(CacheProfileName = "Statistics")] + public ActionResult>> GetDayBreakdown() + { + return Ok(_statService.GetDayBreakdown()); + } + [HttpGet("user/reading-history")] [ResponseCache(CacheProfileName = "Statistics")] diff --git a/API/Services/StatisticService.cs b/API/Services/StatisticService.cs index 580271706..f301718e3 100644 --- a/API/Services/StatisticService.cs +++ b/API/Services/StatisticService.cs @@ -27,6 +27,7 @@ public interface IStatisticService Task> GetTopUsers(int days); Task> GetReadingHistory(int userId); Task>> ReadCountByDay(int userId = 0, int days = 0); + IEnumerable> GetDayBreakdown(); } /// @@ -385,6 +386,17 @@ public class StatisticService : IStatisticService return results; } + public IEnumerable> GetDayBreakdown() + { + return _context.AppUserProgresses + .AsSplitQuery() + .AsNoTracking() + .GroupBy(p => p.LastModified.DayOfWeek) + .OrderBy(g => g.Key) + .Select(g => new StatCount{ Value = g.Key, Count = g.Count() }) + .AsEnumerable(); + } + public async Task> GetTopUsers(int days) { var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList(); diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index 2dab79886..17498c1e1 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -7433,7 +7433,7 @@ "batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=" + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==" }, "bcrypt-pbkdf": { "version": "1.0.2", @@ -7534,7 +7534,7 @@ "boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" }, "bootstrap": { "version": "5.2.0", @@ -7656,7 +7656,7 @@ "bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==" }, "cacache": { "version": "15.3.0", @@ -7829,7 +7829,7 @@ "clone": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==" }, "clone-deep": { "version": "4.0.1", @@ -7957,7 +7957,7 @@ "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" }, "compressible": { "version": "2.0.18", @@ -7992,7 +7992,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" } } }, @@ -8043,7 +8043,7 @@ "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, "copy-anything": { "version": "2.0.6", @@ -8718,7 +8718,7 @@ "dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=" + "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==" }, "dns-packet": { "version": "1.3.4", @@ -8813,7 +8813,7 @@ "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "electron-to-chromium": { "version": "1.4.68", @@ -8839,7 +8839,7 @@ "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, "encoding": { "version": "0.1.13", @@ -9080,7 +9080,7 @@ "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, "escape-string-regexp": { "version": "1.0.5", @@ -9157,7 +9157,7 @@ "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, "event-target-shim": { "version": "5.0.1", @@ -9590,7 +9590,7 @@ "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" }, "fs-minipass": { "version": "2.1.0", @@ -9860,7 +9860,7 @@ "hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", "requires": { "inherits": "^2.0.1", "obuf": "^1.0.0", @@ -9897,7 +9897,7 @@ "http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=" + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==" }, "http-parser-js": { "version": "0.5.5", @@ -10049,7 +10049,7 @@ "image-size": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", - "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", "optional": true }, "immediate": { @@ -10251,7 +10251,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" }, "is-fullwidth-code-point": { "version": "3.0.0", @@ -10368,7 +10368,7 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==" }, "isstream": { "version": "0.1.2", @@ -12429,7 +12429,7 @@ "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, "lodash.deburr": { "version": "4.1.0", @@ -12731,7 +12731,7 @@ "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" }, "memfs": { "version": "3.4.1", @@ -12744,7 +12744,7 @@ "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, "merge-stream": { "version": "2.0.0", @@ -12759,7 +12759,7 @@ "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" }, "micromatch": { "version": "4.0.4", @@ -13173,7 +13173,7 @@ "normalize-range": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=" + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==" }, "npm-bundled": { "version": "1.1.2", @@ -13829,7 +13829,7 @@ "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, "path-type": { "version": "4.0.0", @@ -14300,7 +14300,7 @@ "promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==" }, "promise-retry": { "version": "2.0.1", @@ -14692,7 +14692,7 @@ "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", "optional": true }, "psl": { @@ -15144,7 +15144,7 @@ "select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=" + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==" }, "selenium-webdriver": { "version": "3.6.0", @@ -15211,7 +15211,7 @@ "serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", "requires": { "accepts": "~1.3.4", "batch": "0.6.1", @@ -15233,7 +15233,7 @@ "http-errors": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", "requires": { "depd": "~1.1.2", "inherits": "2.0.3", @@ -15244,12 +15244,12 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "setprototypeof": { "version": "1.1.0", @@ -15814,7 +15814,7 @@ "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" }, "throat": { "version": "6.0.1", @@ -15881,7 +15881,7 @@ "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "tree-kill": { "version": "1.2.2", @@ -16112,7 +16112,7 @@ "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, "update-browserslist-db": { "version": "1.0.5", @@ -16148,7 +16148,7 @@ "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" }, "uuid": { "version": "3.4.0", @@ -16203,7 +16203,7 @@ "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, "verror": { "version": "1.10.0", @@ -16263,7 +16263,7 @@ "wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", "requires": { "defaults": "^1.0.3" } @@ -16281,7 +16281,7 @@ "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "webpack": { "version": "5.73.0", @@ -16608,7 +16608,7 @@ "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "requires": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" diff --git a/UI/Web/src/app/_services/statistics.service.ts b/UI/Web/src/app/_services/statistics.service.ts index 4543384c7..9b14c9d30 100644 --- a/UI/Web/src/app/_services/statistics.service.ts +++ b/UI/Web/src/app/_services/statistics.service.ts @@ -1,4 +1,4 @@ -import { HttpClient, HttpParams } from '@angular/common/http'; +import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { environment } from 'src/environments/environment'; import { UserReadStatistics } from '../statistics/_models/user-read-statistics'; @@ -13,6 +13,16 @@ import { StatCount } from '../statistics/_models/stat-count'; import { PublicationStatus } from '../_models/metadata/publication-status'; import { MangaFormat } from '../_models/manga-format'; +export enum DayOfWeek +{ + Sunday = 0, + Monday = 1, + Tuesday = 2, + Wednesday = 3, + Thursday = 4, + Friday = 5, + Saturday = 6, +} const publicationStatusPipe = new PublicationStatusPipe(); const mangaFormatPipe = new MangaFormatPipe(); @@ -85,4 +95,8 @@ export class StatisticsService { getReadCountByDay(userId: number = 0, days: number = 0) { return this.httpClient.get>(this.baseUrl + 'stats/reading-count-by-day?userId=' + userId + '&days=' + days); } + + getDayBreakdown() { + return this.httpClient.get>>(this.baseUrl + 'stats/day-breakdown'); + } } diff --git a/UI/Web/src/app/admin/_modals/library-access-modal/library-access-modal.component.ts b/UI/Web/src/app/admin/_modals/library-access-modal/library-access-modal.component.ts index cc0167393..33899b279 100644 --- a/UI/Web/src/app/admin/_modals/library-access-modal/library-access-modal.component.ts +++ b/UI/Web/src/app/admin/_modals/library-access-modal/library-access-modal.component.ts @@ -1,5 +1,4 @@ import { Component, Input, OnInit } from '@angular/core'; -import { FormBuilder } from '@angular/forms'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { Library } from 'src/app/_models/library'; import { Member } from 'src/app/_models/auth/member'; @@ -24,7 +23,7 @@ export class LibraryAccessModalComponent implements OnInit { return this.selections != null && this.selections.hasSomeSelected(); } - constructor(public modal: NgbActiveModal, private libraryService: LibraryService, private fb: FormBuilder) { } + constructor(public modal: NgbActiveModal, private libraryService: LibraryService) { } ngOnInit(): void { this.libraryService.getLibraries().subscribe(libs => { diff --git a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts index fbba061dd..c4d98c824 100644 --- a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts +++ b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts @@ -256,7 +256,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { /** * book-content class */ - @ViewChild('bookContentElemRef', {static: false}) bookContentElemRef!: ElementRef; + @ViewChild('readingHtml', {static: false}) bookContentElemRef!: ElementRef; @ViewChild('readingSection', {static: false}) readingSectionElemRef!: ElementRef; @ViewChild('stickyTop', {static: false}) stickyTopElemRef!: ElementRef; @ViewChild('reader', {static: true}) reader!: ElementRef; @@ -382,7 +382,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { get PageHeightForPagination() { if (this.layoutMode === BookPageLayoutMode.Default) { - // if the book content is less than the height of the container, override and return height of container for pagination area if (this.bookContainerElemRef?.nativeElement?.clientHeight > this.bookContentElemRef?.nativeElement?.clientHeight) { return (this.bookContainerElemRef?.nativeElement?.clientHeight || 0) + 'px'; diff --git a/UI/Web/src/app/dashboard/dashboard.module.ts b/UI/Web/src/app/dashboard/dashboard.module.ts index def517670..c316e4203 100644 --- a/UI/Web/src/app/dashboard/dashboard.module.ts +++ b/UI/Web/src/app/dashboard/dashboard.module.ts @@ -1,3 +1,4 @@ + import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DashboardRoutingModule } from './dashboard-routing.module'; diff --git a/UI/Web/src/app/reading-list/_modals/add-to-list-modal/add-to-list-modal.component.ts b/UI/Web/src/app/reading-list/_modals/add-to-list-modal/add-to-list-modal.component.ts index 3d88fcd1b..fc8212d3e 100644 --- a/UI/Web/src/app/reading-list/_modals/add-to-list-modal/add-to-list-modal.component.ts +++ b/UI/Web/src/app/reading-list/_modals/add-to-list-modal/add-to-list-modal.component.ts @@ -60,6 +60,10 @@ export class AddToListModalComponent implements OnInit, AfterViewInit { @ViewChild('title') inputElem!: ElementRef; + filterList = (listItem: ReadingList) => { + return listItem.title.toLowerCase().indexOf((this.listForm.value.filterQuery || '').toLowerCase()) >= 0; + } + constructor(private modal: NgbActiveModal, private readingListService: ReadingListService, private toastr: ToastrService) { } @@ -128,9 +132,4 @@ export class AddToListModalComponent implements OnInit, AfterViewInit { } } - - filterList = (listItem: ReadingList) => { - return listItem.title.toLowerCase().indexOf((this.listForm.value.filterQuery || '').toLowerCase()) >= 0; - } - } diff --git a/UI/Web/src/app/statistics/_components/_modals/generic-list-modal/generic-list-modal.component.html b/UI/Web/src/app/statistics/_components/_modals/generic-list-modal/generic-list-modal.component.html new file mode 100644 index 000000000..ddbc9eda2 --- /dev/null +++ b/UI/Web/src/app/statistics/_components/_modals/generic-list-modal/generic-list-modal.component.html @@ -0,0 +1,28 @@ + + + + diff --git a/UI/Web/src/app/statistics/_components/_modals/generic-list-modal/generic-list-modal.component.scss b/UI/Web/src/app/statistics/_components/_modals/generic-list-modal/generic-list-modal.component.scss new file mode 100644 index 000000000..57df5fe23 --- /dev/null +++ b/UI/Web/src/app/statistics/_components/_modals/generic-list-modal/generic-list-modal.component.scss @@ -0,0 +1,7 @@ +.list-group-item.no-click { + cursor: not-allowed; +} + +.list-group-item { + cursor: pointer; +} \ No newline at end of file diff --git a/UI/Web/src/app/statistics/_components/_modals/generic-list-modal/generic-list-modal.component.ts b/UI/Web/src/app/statistics/_components/_modals/generic-list-modal/generic-list-modal.component.ts new file mode 100644 index 000000000..069f24b0f --- /dev/null +++ b/UI/Web/src/app/statistics/_components/_modals/generic-list-modal/generic-list-modal.component.ts @@ -0,0 +1,34 @@ +import { Component, EventEmitter, Input } from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'app-generic-list-modal', + templateUrl: './generic-list-modal.component.html', + styleUrls: ['./generic-list-modal.component.scss'] +}) +export class GenericListModalComponent { + @Input() items: Array = []; + @Input() title: string = ''; + @Input() clicked: ((item: string) => void) | undefined = undefined; + + listForm: FormGroup = new FormGroup({ + 'filterQuery': new FormControl('', []) + }); + + filterList = (listItem: string) => { + return listItem.toLowerCase().indexOf((this.listForm.value.filterQuery || '').toLowerCase()) >= 0; + } + + constructor(private modal: NgbActiveModal) {} + + close() { + this.modal.close(); + } + + handleClick(item: string) { + if (this.clicked) { + this.clicked(item); + } + } +} diff --git a/UI/Web/src/app/statistics/_components/day-breakdown/day-breakdown.component.html b/UI/Web/src/app/statistics/_components/day-breakdown/day-breakdown.component.html new file mode 100644 index 000000000..ad6b3ec16 --- /dev/null +++ b/UI/Web/src/app/statistics/_components/day-breakdown/day-breakdown.component.html @@ -0,0 +1,16 @@ +
+

Day Breakdown

+
+ + + \ No newline at end of file diff --git a/UI/Web/src/app/statistics/_components/day-breakdown/day-breakdown.component.scss b/UI/Web/src/app/statistics/_components/day-breakdown/day-breakdown.component.scss new file mode 100644 index 000000000..eb069bb88 --- /dev/null +++ b/UI/Web/src/app/statistics/_components/day-breakdown/day-breakdown.component.scss @@ -0,0 +1,3 @@ +::ng-deep .dark .ngx-charts text { + fill: #a0aabe; +} \ No newline at end of file diff --git a/UI/Web/src/app/statistics/_components/day-breakdown/day-breakdown.component.ts b/UI/Web/src/app/statistics/_components/day-breakdown/day-breakdown.component.ts new file mode 100644 index 000000000..9a8e0cf34 --- /dev/null +++ b/UI/Web/src/app/statistics/_components/day-breakdown/day-breakdown.component.ts @@ -0,0 +1,55 @@ +import { Component, OnInit } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { LegendPosition } from '@swimlane/ngx-charts'; +import { Subject, combineLatest, map, takeUntil, Observable } from 'rxjs'; +import { DayOfWeek, StatisticsService } from 'src/app/_services/statistics.service'; +import { compare } from 'src/app/_single-module/table/_directives/sortable-header.directive'; +import { PieDataItem } from '../../_models/pie-data-item'; +import { StatCount } from '../../_models/stat-count'; +import { DayOfWeekPipe } from '../../_pipes/day-of-week.pipe'; + +@Component({ + selector: 'app-day-breakdown', + templateUrl: './day-breakdown.component.html', + styleUrls: ['./day-breakdown.component.scss'] +}) +export class DayBreakdownComponent implements OnInit { + + private readonly onDestroy = new Subject(); + + view: [number, number] = [700, 400]; + gradient: boolean = true; + showLegend: boolean = true; + showLabels: boolean = true; + isDoughnut: boolean = false; + legendPosition: LegendPosition = LegendPosition.Right; + colorScheme = { + domain: ['#5AA454', '#A10A28', '#C7B42C', '#AAAAAA'] + }; + + formControl: FormControl = new FormControl(true, []); + dayBreakdown$!: Observable>; + + constructor(private statService: StatisticsService) { + const dayOfWeekPipe = new DayOfWeekPipe(); + this.dayBreakdown$ = this.statService.getDayBreakdown().pipe( + map((data: Array>) => { + return data.map(d => { + return {name: dayOfWeekPipe.transform(d.value), value: d.count}; + }) + }), + takeUntil(this.onDestroy) + ); + } + + ngOnInit(): void { + this.onDestroy.next(); + this.onDestroy.complete(); + } + + ngOnDestroy(): void { + this.onDestroy.next(); + this.onDestroy.complete(); + } + +} diff --git a/UI/Web/src/app/statistics/_components/read-by-day-and/read-by-day-and.component.ts b/UI/Web/src/app/statistics/_components/read-by-day-and/read-by-day-and.component.ts index b66d247a4..9bfcfc2a6 100644 --- a/UI/Web/src/app/statistics/_components/read-by-day-and/read-by-day-and.component.ts +++ b/UI/Web/src/app/statistics/_components/read-by-day-and/read-by-day-and.component.ts @@ -59,7 +59,7 @@ export class ReadByDayAndComponent implements OnInit, OnDestroy { shareReplay(), ); - this.data$.subscribe(_ => console.log('hi')); + this.data$.subscribe(); } ngOnInit(): void { diff --git a/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.html b/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.html index 47fbc0189..b69718aec 100644 --- a/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.html +++ b/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.html @@ -2,7 +2,7 @@
- + {{stats.seriesCount | compactNumber}} Series
@@ -11,7 +11,7 @@
- + {{stats.volumeCount | compactNumber}} Volumes
@@ -38,7 +38,7 @@
- + {{stats.totalSize | bytes}}
@@ -47,7 +47,7 @@
- + {{stats.totalGenres | compactNumber}} Genres
@@ -56,7 +56,7 @@
- + {{stats.totalTags | compactNumber}} Tags
@@ -65,7 +65,7 @@
- + {{stats.totalPeople | compactNumber}} People
@@ -74,7 +74,7 @@
- +
@@ -109,4 +109,10 @@
+
+
+ +
+
+
diff --git a/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.ts b/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.ts index 5316f1ecc..8b261b043 100644 --- a/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.ts +++ b/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.ts @@ -1,13 +1,15 @@ import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { Router } from '@angular/router'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { map, Observable, shareReplay, Subject, takeUntil, tap } from 'rxjs'; -import { DownloadService } from 'src/app/shared/_services/download.service'; +import { FilterQueryParam } from 'src/app/shared/_services/filter-utilities.service'; import { Series } from 'src/app/_models/series'; -import { User } from 'src/app/_models/user'; import { ImageService } from 'src/app/_services/image.service'; +import { MetadataService } from 'src/app/_services/metadata.service'; import { StatisticsService } from 'src/app/_services/statistics.service'; import { PieDataItem } from '../../_models/pie-data-item'; import { ServerStatistics } from '../../_models/server-statistics'; +import { GenericListModalComponent } from '../_modals/generic-list-modal/generic-list-modal.component'; @Component({ selector: 'app-server-stats', @@ -25,8 +27,13 @@ export class ServerStatsComponent implements OnInit, OnDestroy { stats$!: Observable; seriesImage: (data: PieDataItem) => string; private readonly onDestroy = new Subject(); + openSeries = (data: PieDataItem) => { + const series = data.extra as Series; + this.router.navigate(['library', series.libraryId, 'series', series.id]); + } - constructor(private statService: StatisticsService, private router: Router, private imageService: ImageService) { + constructor(private statService: StatisticsService, private router: Router, private imageService: ImageService, + private metadataService: MetadataService, private modalService: NgbModal) { this.seriesImage = (data: PieDataItem) => { if (data.extra) return this.imageService.getSeriesCoverImage(data.extra.id); return ''; @@ -75,9 +82,40 @@ export class ServerStatsComponent implements OnInit, OnDestroy { this.onDestroy.complete(); } - openSeries = (data: PieDataItem) => { - const series = data.extra as Series; - this.router.navigate(['library', series.libraryId, 'series', series.id]); + openGenreList() { + this.metadataService.getAllGenres().subscribe(genres => { + const ref = this.modalService.open(GenericListModalComponent, { scrollable: true }); + ref.componentInstance.items = genres.map(t => t.title); + ref.componentInstance.title = 'Genres'; + ref.componentInstance.clicked = (item: string) => { + const params: any = {}; + params[FilterQueryParam.Genres] = item; + params[FilterQueryParam.Page] = 1; + this.router.navigate(['all-series'], {queryParams: params}); + }; + }); + } + + openTagList() { + this.metadataService.getAllTags().subscribe(tags => { + const ref = this.modalService.open(GenericListModalComponent, { scrollable: true }); + ref.componentInstance.items = tags.map(t => t.title); + ref.componentInstance.title = 'Tags'; + ref.componentInstance.clicked = (item: string) => { + const params: any = {}; + params[FilterQueryParam.Tags] = item; + params[FilterQueryParam.Page] = 1; + this.router.navigate(['all-series'], {queryParams: params}); + }; + }); + } + + openPeopleList() { + this.metadataService.getAllPeople().subscribe(people => { + const ref = this.modalService.open(GenericListModalComponent, { scrollable: true }); + ref.componentInstance.items = [...new Set(people.map(person => person.name))]; + ref.componentInstance.title = 'People'; + }); } diff --git a/UI/Web/src/app/statistics/_pipes/day-of-week.pipe.ts b/UI/Web/src/app/statistics/_pipes/day-of-week.pipe.ts new file mode 100644 index 000000000..e669cbffb --- /dev/null +++ b/UI/Web/src/app/statistics/_pipes/day-of-week.pipe.ts @@ -0,0 +1,22 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { DayOfWeek } from 'src/app/_services/statistics.service'; + +@Pipe({ + name: 'dayOfWeek' +}) +export class DayOfWeekPipe implements PipeTransform { + + transform(value: DayOfWeek): string { + switch(value) { + case DayOfWeek.Monday: return 'Monday'; + case DayOfWeek.Tuesday: return 'Tuesday'; + case DayOfWeek.Wednesday: return 'Wednesday'; + case DayOfWeek.Thursday: return 'Thursday'; + case DayOfWeek.Friday: return 'Friday'; + case DayOfWeek.Saturday: return 'Saturday'; + case DayOfWeek.Sunday: return 'Sunday'; + + } + } + +} diff --git a/UI/Web/src/app/statistics/statistics.module.ts b/UI/Web/src/app/statistics/statistics.module.ts index 83d70326e..54a7ea380 100644 --- a/UI/Web/src/app/statistics/statistics.module.ts +++ b/UI/Web/src/app/statistics/statistics.module.ts @@ -7,7 +7,7 @@ import { SharedModule } from '../shared/shared.module'; import { ServerStatsComponent } from './_components/server-stats/server-stats.component'; import { NgxChartsModule } from '@swimlane/ngx-charts'; import { StatListComponent } from './_components/stat-list/stat-list.component'; -import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; +import { NgbModalModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { PublicationStatusStatsComponent } from './_components/publication-status-stats/publication-status-stats.component'; import { ReactiveFormsModule } from '@angular/forms'; import { MangaFormatStatsComponent } from './_components/manga-format-stats/manga-format-stats.component'; @@ -15,6 +15,9 @@ import { FileBreakdownStatsComponent } from './_components/file-breakdown-stats/ import { PipeModule } from '../pipe/pipe.module'; import { TopReadersComponent } from './_components/top-readers/top-readers.component'; import { ReadByDayAndComponent } from './_components/read-by-day-and/read-by-day-and.component'; +import { GenericListModalComponent } from './_components/_modals/generic-list-modal/generic-list-modal.component'; +import { DayBreakdownComponent } from './_components/day-breakdown/day-breakdown.component'; +import { DayOfWeekPipe } from './_pipes/day-of-week.pipe'; @@ -28,13 +31,17 @@ import { ReadByDayAndComponent } from './_components/read-by-day-and/read-by-day MangaFormatStatsComponent, FileBreakdownStatsComponent, TopReadersComponent, - ReadByDayAndComponent + ReadByDayAndComponent, + GenericListModalComponent, + DayBreakdownComponent, + DayOfWeekPipe ], imports: [ CommonModule, TableModule, SharedModule, NgbTooltipModule, + NgbModalModule, ReactiveFormsModule, PipeModule, diff --git a/openapi.json b/openapi.json index 816287de9..4498169f7 100644 --- a/openapi.json +++ b/openapi.json @@ -7,7 +7,7 @@ "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.6.1.18" + "version": "0.6.1.21" }, "servers": [ { @@ -8033,6 +8033,44 @@ } } }, + "/api/Stats/day-breakdown": { + "get": { + "tags": [ + "Stats" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DayOfWeekStatCount" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DayOfWeekStatCount" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DayOfWeekStatCount" + } + } + } + } + } + } + } + }, "/api/Stats/user/reading-history": { "get": { "tags": [ @@ -10293,6 +10331,32 @@ }, "additionalProperties": false }, + "DayOfWeek": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6 + ], + "type": "integer", + "format": "int32" + }, + "DayOfWeekStatCount": { + "type": "object", + "properties": { + "value": { + "$ref": "#/components/schemas/DayOfWeek" + }, + "count": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, "DeleteSeriesDto": { "type": "object", "properties": {