Feature/stats finishoff (#1720)

* Added ability to click on genres, tags, and people to view all items in a modal.

* Made it so we can click and open a filtered search from generic list

* Fixed broken epub pagination area due to a typo in a query selector

* Added day breakdown, wrapping up stats
This commit is contained in:
Joe Milazzo 2023-01-03 19:41:10 -06:00 committed by GitHub
parent dfbc8da427
commit 02daa5ed56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 378 additions and 66 deletions

View File

@ -121,6 +121,14 @@ public class StatsController : BaseApiController
return Ok(await _statService.ReadCountByDay(userId, days)); return Ok(await _statService.ReadCountByDay(userId, days));
} }
[HttpGet("day-breakdown")]
[Authorize("RequireAdminRole")]
[ResponseCache(CacheProfileName = "Statistics")]
public ActionResult<IEnumerable<StatCount<DayOfWeek>>> GetDayBreakdown()
{
return Ok(_statService.GetDayBreakdown());
}
[HttpGet("user/reading-history")] [HttpGet("user/reading-history")]
[ResponseCache(CacheProfileName = "Statistics")] [ResponseCache(CacheProfileName = "Statistics")]

View File

@ -27,6 +27,7 @@ public interface IStatisticService
Task<IEnumerable<TopReadDto>> GetTopUsers(int days); Task<IEnumerable<TopReadDto>> GetTopUsers(int days);
Task<IEnumerable<ReadHistoryEvent>> GetReadingHistory(int userId); Task<IEnumerable<ReadHistoryEvent>> GetReadingHistory(int userId);
Task<IEnumerable<PagesReadOnADayCount<DateTime>>> ReadCountByDay(int userId = 0, int days = 0); Task<IEnumerable<PagesReadOnADayCount<DateTime>>> ReadCountByDay(int userId = 0, int days = 0);
IEnumerable<StatCount<DayOfWeek>> GetDayBreakdown();
} }
/// <summary> /// <summary>
@ -385,6 +386,17 @@ public class StatisticService : IStatisticService
return results; return results;
} }
public IEnumerable<StatCount<DayOfWeek>> GetDayBreakdown()
{
return _context.AppUserProgresses
.AsSplitQuery()
.AsNoTracking()
.GroupBy(p => p.LastModified.DayOfWeek)
.OrderBy(g => g.Key)
.Select(g => new StatCount<DayOfWeek>{ Value = g.Key, Count = g.Count() })
.AsEnumerable();
}
public async Task<IEnumerable<TopReadDto>> GetTopUsers(int days) public async Task<IEnumerable<TopReadDto>> GetTopUsers(int days)
{ {
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList(); var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList();

View File

@ -7433,7 +7433,7 @@
"batch": { "batch": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", "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": { "bcrypt-pbkdf": {
"version": "1.0.2", "version": "1.0.2",
@ -7534,7 +7534,7 @@
"boolbase": { "boolbase": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "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": { "bootstrap": {
"version": "5.2.0", "version": "5.2.0",
@ -7656,7 +7656,7 @@
"bytes": { "bytes": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw=="
}, },
"cacache": { "cacache": {
"version": "15.3.0", "version": "15.3.0",
@ -7829,7 +7829,7 @@
"clone": { "clone": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
"integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="
}, },
"clone-deep": { "clone-deep": {
"version": "4.0.1", "version": "4.0.1",
@ -7957,7 +7957,7 @@
"commondir": { "commondir": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="
}, },
"compressible": { "compressible": {
"version": "2.0.18", "version": "2.0.18",
@ -7992,7 +7992,7 @@
"ms": { "ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
} }
} }
}, },
@ -8043,7 +8043,7 @@
"cookie-signature": { "cookie-signature": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
}, },
"copy-anything": { "copy-anything": {
"version": "2.0.6", "version": "2.0.6",
@ -8718,7 +8718,7 @@
"dns-equal": { "dns-equal": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", "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": { "dns-packet": {
"version": "1.3.4", "version": "1.3.4",
@ -8813,7 +8813,7 @@
"ee-first": { "ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "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": { "electron-to-chromium": {
"version": "1.4.68", "version": "1.4.68",
@ -8839,7 +8839,7 @@
"encodeurl": { "encodeurl": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
}, },
"encoding": { "encoding": {
"version": "0.1.13", "version": "0.1.13",
@ -9080,7 +9080,7 @@
"escape-html": { "escape-html": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "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": { "escape-string-regexp": {
"version": "1.0.5", "version": "1.0.5",
@ -9157,7 +9157,7 @@
"etag": { "etag": {
"version": "1.8.1", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
}, },
"event-target-shim": { "event-target-shim": {
"version": "5.0.1", "version": "5.0.1",
@ -9590,7 +9590,7 @@
"fresh": { "fresh": {
"version": "0.5.2", "version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="
}, },
"fs-minipass": { "fs-minipass": {
"version": "2.1.0", "version": "2.1.0",
@ -9860,7 +9860,7 @@
"hpack.js": { "hpack.js": {
"version": "2.1.6", "version": "2.1.6",
"resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
"integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==",
"requires": { "requires": {
"inherits": "^2.0.1", "inherits": "^2.0.1",
"obuf": "^1.0.0", "obuf": "^1.0.0",
@ -9897,7 +9897,7 @@
"http-deceiver": { "http-deceiver": {
"version": "1.2.7", "version": "1.2.7",
"resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
"integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=" "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw=="
}, },
"http-parser-js": { "http-parser-js": {
"version": "0.5.5", "version": "0.5.5",
@ -10049,7 +10049,7 @@
"image-size": { "image-size": {
"version": "0.5.5", "version": "0.5.5",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", "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 "optional": true
}, },
"immediate": { "immediate": {
@ -10251,7 +10251,7 @@
"is-extglob": { "is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="
}, },
"is-fullwidth-code-point": { "is-fullwidth-code-point": {
"version": "3.0.0", "version": "3.0.0",
@ -10368,7 +10368,7 @@
"isobject": { "isobject": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="
}, },
"isstream": { "isstream": {
"version": "0.1.2", "version": "0.1.2",
@ -12429,7 +12429,7 @@
"lodash.debounce": { "lodash.debounce": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "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": { "lodash.deburr": {
"version": "4.1.0", "version": "4.1.0",
@ -12731,7 +12731,7 @@
"media-typer": { "media-typer": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
}, },
"memfs": { "memfs": {
"version": "3.4.1", "version": "3.4.1",
@ -12744,7 +12744,7 @@
"merge-descriptors": { "merge-descriptors": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
}, },
"merge-stream": { "merge-stream": {
"version": "2.0.0", "version": "2.0.0",
@ -12759,7 +12759,7 @@
"methods": { "methods": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="
}, },
"micromatch": { "micromatch": {
"version": "4.0.4", "version": "4.0.4",
@ -13173,7 +13173,7 @@
"normalize-range": { "normalize-range": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", "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": { "npm-bundled": {
"version": "1.1.2", "version": "1.1.2",
@ -13829,7 +13829,7 @@
"path-to-regexp": { "path-to-regexp": {
"version": "0.1.7", "version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "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": { "path-type": {
"version": "4.0.0", "version": "4.0.0",
@ -14300,7 +14300,7 @@
"promise-inflight": { "promise-inflight": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
"integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="
}, },
"promise-retry": { "promise-retry": {
"version": "2.0.1", "version": "2.0.1",
@ -14692,7 +14692,7 @@
"prr": { "prr": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
"integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==",
"optional": true "optional": true
}, },
"psl": { "psl": {
@ -15144,7 +15144,7 @@
"select-hose": { "select-hose": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
"integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=" "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg=="
}, },
"selenium-webdriver": { "selenium-webdriver": {
"version": "3.6.0", "version": "3.6.0",
@ -15211,7 +15211,7 @@
"serve-index": { "serve-index": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz",
"integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==",
"requires": { "requires": {
"accepts": "~1.3.4", "accepts": "~1.3.4",
"batch": "0.6.1", "batch": "0.6.1",
@ -15233,7 +15233,7 @@
"http-errors": { "http-errors": {
"version": "1.6.3", "version": "1.6.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
"integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==",
"requires": { "requires": {
"depd": "~1.1.2", "depd": "~1.1.2",
"inherits": "2.0.3", "inherits": "2.0.3",
@ -15244,12 +15244,12 @@
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="
}, },
"ms": { "ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
}, },
"setprototypeof": { "setprototypeof": {
"version": "1.1.0", "version": "1.1.0",
@ -15814,7 +15814,7 @@
"text-table": { "text-table": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="
}, },
"throat": { "throat": {
"version": "6.0.1", "version": "6.0.1",
@ -15881,7 +15881,7 @@
"tr46": { "tr46": {
"version": "0.0.3", "version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
}, },
"tree-kill": { "tree-kill": {
"version": "1.2.2", "version": "1.2.2",
@ -16112,7 +16112,7 @@
"unpipe": { "unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
}, },
"update-browserslist-db": { "update-browserslist-db": {
"version": "1.0.5", "version": "1.0.5",
@ -16148,7 +16148,7 @@
"utils-merge": { "utils-merge": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="
}, },
"uuid": { "uuid": {
"version": "3.4.0", "version": "3.4.0",
@ -16203,7 +16203,7 @@
"vary": { "vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
}, },
"verror": { "verror": {
"version": "1.10.0", "version": "1.10.0",
@ -16263,7 +16263,7 @@
"wcwidth": { "wcwidth": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
"integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
"requires": { "requires": {
"defaults": "^1.0.3" "defaults": "^1.0.3"
} }
@ -16281,7 +16281,7 @@
"webidl-conversions": { "webidl-conversions": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
}, },
"webpack": { "webpack": {
"version": "5.73.0", "version": "5.73.0",
@ -16608,7 +16608,7 @@
"whatwg-url": { "whatwg-url": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"requires": { "requires": {
"tr46": "~0.0.3", "tr46": "~0.0.3",
"webidl-conversions": "^3.0.0" "webidl-conversions": "^3.0.0"

View File

@ -1,4 +1,4 @@
import { HttpClient, HttpParams } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { environment } from 'src/environments/environment'; import { environment } from 'src/environments/environment';
import { UserReadStatistics } from '../statistics/_models/user-read-statistics'; 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 { PublicationStatus } from '../_models/metadata/publication-status';
import { MangaFormat } from '../_models/manga-format'; 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 publicationStatusPipe = new PublicationStatusPipe();
const mangaFormatPipe = new MangaFormatPipe(); const mangaFormatPipe = new MangaFormatPipe();
@ -85,4 +95,8 @@ export class StatisticsService {
getReadCountByDay(userId: number = 0, days: number = 0) { getReadCountByDay(userId: number = 0, days: number = 0) {
return this.httpClient.get<Array<any>>(this.baseUrl + 'stats/reading-count-by-day?userId=' + userId + '&days=' + days); return this.httpClient.get<Array<any>>(this.baseUrl + 'stats/reading-count-by-day?userId=' + userId + '&days=' + days);
} }
getDayBreakdown() {
return this.httpClient.get<Array<StatCount<DayOfWeek>>>(this.baseUrl + 'stats/day-breakdown');
}
} }

View File

@ -1,5 +1,4 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { Library } from 'src/app/_models/library'; import { Library } from 'src/app/_models/library';
import { Member } from 'src/app/_models/auth/member'; import { Member } from 'src/app/_models/auth/member';
@ -24,7 +23,7 @@ export class LibraryAccessModalComponent implements OnInit {
return this.selections != null && this.selections.hasSomeSelected(); 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 { ngOnInit(): void {
this.libraryService.getLibraries().subscribe(libs => { this.libraryService.getLibraries().subscribe(libs => {

View File

@ -256,7 +256,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
/** /**
* book-content class * book-content class
*/ */
@ViewChild('bookContentElemRef', {static: false}) bookContentElemRef!: ElementRef<HTMLDivElement>; @ViewChild('readingHtml', {static: false}) bookContentElemRef!: ElementRef<HTMLDivElement>;
@ViewChild('readingSection', {static: false}) readingSectionElemRef!: ElementRef<HTMLDivElement>; @ViewChild('readingSection', {static: false}) readingSectionElemRef!: ElementRef<HTMLDivElement>;
@ViewChild('stickyTop', {static: false}) stickyTopElemRef!: ElementRef<HTMLDivElement>; @ViewChild('stickyTop', {static: false}) stickyTopElemRef!: ElementRef<HTMLDivElement>;
@ViewChild('reader', {static: true}) reader!: ElementRef; @ViewChild('reader', {static: true}) reader!: ElementRef;
@ -382,7 +382,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
get PageHeightForPagination() { get PageHeightForPagination() {
if (this.layoutMode === BookPageLayoutMode.Default) { 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 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) { if (this.bookContainerElemRef?.nativeElement?.clientHeight > this.bookContentElemRef?.nativeElement?.clientHeight) {
return (this.bookContainerElemRef?.nativeElement?.clientHeight || 0) + 'px'; return (this.bookContainerElemRef?.nativeElement?.clientHeight || 0) + 'px';

View File

@ -1,3 +1,4 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { DashboardRoutingModule } from './dashboard-routing.module'; import { DashboardRoutingModule } from './dashboard-routing.module';

View File

@ -60,6 +60,10 @@ export class AddToListModalComponent implements OnInit, AfterViewInit {
@ViewChild('title') inputElem!: ElementRef<HTMLInputElement>; @ViewChild('title') inputElem!: ElementRef<HTMLInputElement>;
filterList = (listItem: ReadingList) => {
return listItem.title.toLowerCase().indexOf((this.listForm.value.filterQuery || '').toLowerCase()) >= 0;
}
constructor(private modal: NgbActiveModal, private readingListService: ReadingListService, private toastr: ToastrService) { } 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;
}
} }

View File

@ -0,0 +1,28 @@
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
</div>
<div class="modal-body">
<form style="width: 100%" [formGroup]="listForm">
<div class="mb-3" *ngIf="items.length >= 5">
<label for="filter" class="form-label">Filter</label>
<div class="input-group">
<input id="filter" autocomplete="off" class="form-control" formControlName="filterQuery" type="text" aria-describedby="reset-input">
<button class="btn btn-outline-secondary" type="button" id="reset-input" (click)="listForm.get('filterQuery')?.setValue('');">Clear</button>
</div>
</div>
<div class="list-group">
<li class="list-group-item d-flex justify-content-between align-items-center" *ngFor="let item of items | filter: filterList; let i = index">
{{item}}
<button class="btn btn-primary" [disabled]="clicked === undefined" (click)="handleClick(item)">
<i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i>
<span class="visually-hidden">Open a filtered search for {{item}}</span>
</button>
</li>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" (click)="close()">Close</button>
</div>

View File

@ -0,0 +1,7 @@
.list-group-item.no-click {
cursor: not-allowed;
}
.list-group-item {
cursor: pointer;
}

View File

@ -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<string> = [];
@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);
}
}
}

View File

@ -0,0 +1,16 @@
<div class="row g-0 mb-2">
<h4>Day Breakdown</h4>
</div>
<ngx-charts-bar-vertical
class="dark"
[view]="view"
[results]="dayBreakdown$ | async"
[xAxis]="true"
[yAxis]="true"
[legend]="showLegend"
[showXAxisLabel]="true"
[showYAxisLabel]="true"
xAxisLabel="Day of Week"
yAxisLabel="Reading Events">
</ngx-charts-bar-vertical>

View File

@ -0,0 +1,3 @@
::ng-deep .dark .ngx-charts text {
fill: #a0aabe;
}

View File

@ -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<void>();
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<Array<PieDataItem>>;
constructor(private statService: StatisticsService) {
const dayOfWeekPipe = new DayOfWeekPipe();
this.dayBreakdown$ = this.statService.getDayBreakdown().pipe(
map((data: Array<StatCount<DayOfWeek>>) => {
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();
}
}

View File

@ -59,7 +59,7 @@ export class ReadByDayAndComponent implements OnInit, OnDestroy {
shareReplay(), shareReplay(),
); );
this.data$.subscribe(_ => console.log('hi')); this.data$.subscribe();
} }
ngOnInit(): void { ngOnInit(): void {

View File

@ -2,7 +2,7 @@
<div class="row g-0 mt-4 mb-3 d-flex justify-content-around" *ngIf="stats$ | async as stats"> <div class="row g-0 mt-4 mb-3 d-flex justify-content-around" *ngIf="stats$ | async as stats">
<ng-container> <ng-container>
<div class="col-auto mb-2"> <div class="col-auto mb-2">
<app-icon-and-title label="Total Series" [clickable]="false" fontClasses="fa-regular fa-calendar" title="Total Series"> <app-icon-and-title label="Total Series" [clickable]="false" fontClasses="fa-solid fa-book-open" title="Total Series">
{{stats.seriesCount | compactNumber}} Series {{stats.seriesCount | compactNumber}} Series
</app-icon-and-title> </app-icon-and-title>
</div> </div>
@ -11,7 +11,7 @@
<ng-container > <ng-container >
<div class="col-auto mb-2"> <div class="col-auto mb-2">
<app-icon-and-title label="Total Volumes" [clickable]="false" fontClasses="fas fa-eye" title="Total Volumes"> <app-icon-and-title label="Total Volumes" [clickable]="false" fontClasses="fas fa-book" title="Total Volumes">
{{stats.volumeCount | compactNumber}} Volumes {{stats.volumeCount | compactNumber}} Volumes
</app-icon-and-title> </app-icon-and-title>
</div> </div>
@ -38,7 +38,7 @@
<ng-container> <ng-container>
<div class="col-auto mb-2"> <div class="col-auto mb-2">
<app-icon-and-title label="Total Size" [clickable]="false" fontClasses="fa-solid fa-weight-scale" title="Total Size"> <app-icon-and-title label="Total Size" [clickable]="false" fontClasses="fa-solid fa-scale-unbalanced" title="Total Size">
{{stats.totalSize | bytes}} {{stats.totalSize | bytes}}
</app-icon-and-title> </app-icon-and-title>
</div> </div>
@ -47,7 +47,7 @@
<ng-container> <ng-container>
<div class="col-auto mb-2"> <div class="col-auto mb-2">
<app-icon-and-title label="Total Genres" [clickable]="false" fontClasses="fa-solid fa-tags" title="Total Genres"> <app-icon-and-title label="Total Genres" [clickable]="true" fontClasses="fa-solid fa-tags" title="Total Genres" (click)="openGenreList();$event.stopPropagation();">
{{stats.totalGenres | compactNumber}} Genres {{stats.totalGenres | compactNumber}} Genres
</app-icon-and-title> </app-icon-and-title>
</div> </div>
@ -56,7 +56,7 @@
<ng-container> <ng-container>
<div class="col-auto mb-2"> <div class="col-auto mb-2">
<app-icon-and-title label="Total Tags" [clickable]="false" fontClasses="fa-solid fa-tags" title="Total Tags"> <app-icon-and-title label="Total Tags" [clickable]="true" fontClasses="fa-solid fa-tags" title="Total Tags" (click)="openTagList();$event.stopPropagation();">
{{stats.totalTags | compactNumber}} Tags {{stats.totalTags | compactNumber}} Tags
</app-icon-and-title> </app-icon-and-title>
</div> </div>
@ -65,7 +65,7 @@
<ng-container> <ng-container>
<div class="col-auto mb-2"> <div class="col-auto mb-2">
<app-icon-and-title label="Total People" [clickable]="false" fontClasses="fa-solid fa-user-tag" title="Total People"> <app-icon-and-title label="Total People" [clickable]="true" fontClasses="fa-solid fa-user-tag" title="Total People" (click)="openPeopleList();$event.stopPropagation();">
{{stats.totalPeople | compactNumber}} People {{stats.totalPeople | compactNumber}} People
</app-icon-and-title> </app-icon-and-title>
</div> </div>
@ -74,7 +74,7 @@
<div class="grid row g-0 pt-2 pb-2 d-flex justify-content-around"> <div class="grid row g-0 pt-2 pb-2 d-flex justify-content-around">
<div class="col-auto"> <div class="col-auto">
<app-stat-list [data$]="releaseYears$" title="Release Years" lable="series"></app-stat-list> <app-stat-list [data$]="releaseYears$" title="Release Years" label="series"></app-stat-list>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<app-stat-list [data$]="mostActiveUsers$" title="Most Active Users" label="reads"></app-stat-list> <app-stat-list [data$]="mostActiveUsers$" title="Most Active Users" label="reads"></app-stat-list>
@ -109,4 +109,10 @@
</div> </div>
</div> </div>
<div class="row g-0 pt-4 pb-2 " style="height: 242px">
<div class="col-md-12 col-sm-12 mt-4 pt-2">
<app-day-breakdown></app-day-breakdown>
</div>
</div>
</div> </div>

View File

@ -1,13 +1,15 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { map, Observable, shareReplay, Subject, takeUntil, tap } from 'rxjs'; 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 { Series } from 'src/app/_models/series';
import { User } from 'src/app/_models/user';
import { ImageService } from 'src/app/_services/image.service'; 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 { StatisticsService } from 'src/app/_services/statistics.service';
import { PieDataItem } from '../../_models/pie-data-item'; import { PieDataItem } from '../../_models/pie-data-item';
import { ServerStatistics } from '../../_models/server-statistics'; import { ServerStatistics } from '../../_models/server-statistics';
import { GenericListModalComponent } from '../_modals/generic-list-modal/generic-list-modal.component';
@Component({ @Component({
selector: 'app-server-stats', selector: 'app-server-stats',
@ -25,8 +27,13 @@ export class ServerStatsComponent implements OnInit, OnDestroy {
stats$!: Observable<ServerStatistics>; stats$!: Observable<ServerStatistics>;
seriesImage: (data: PieDataItem) => string; seriesImage: (data: PieDataItem) => string;
private readonly onDestroy = new Subject<void>(); private readonly onDestroy = new Subject<void>();
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) => { this.seriesImage = (data: PieDataItem) => {
if (data.extra) return this.imageService.getSeriesCoverImage(data.extra.id); if (data.extra) return this.imageService.getSeriesCoverImage(data.extra.id);
return ''; return '';
@ -75,9 +82,40 @@ export class ServerStatsComponent implements OnInit, OnDestroy {
this.onDestroy.complete(); this.onDestroy.complete();
} }
openSeries = (data: PieDataItem) => { openGenreList() {
const series = data.extra as Series; this.metadataService.getAllGenres().subscribe(genres => {
this.router.navigate(['library', series.libraryId, 'series', series.id]); 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';
});
} }

View File

@ -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';
}
}
}

View File

@ -7,7 +7,7 @@ import { SharedModule } from '../shared/shared.module';
import { ServerStatsComponent } from './_components/server-stats/server-stats.component'; import { ServerStatsComponent } from './_components/server-stats/server-stats.component';
import { NgxChartsModule } from '@swimlane/ngx-charts'; import { NgxChartsModule } from '@swimlane/ngx-charts';
import { StatListComponent } from './_components/stat-list/stat-list.component'; 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 { PublicationStatusStatsComponent } from './_components/publication-status-stats/publication-status-stats.component';
import { ReactiveFormsModule } from '@angular/forms'; import { ReactiveFormsModule } from '@angular/forms';
import { MangaFormatStatsComponent } from './_components/manga-format-stats/manga-format-stats.component'; 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 { PipeModule } from '../pipe/pipe.module';
import { TopReadersComponent } from './_components/top-readers/top-readers.component'; import { TopReadersComponent } from './_components/top-readers/top-readers.component';
import { ReadByDayAndComponent } from './_components/read-by-day-and/read-by-day-and.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, MangaFormatStatsComponent,
FileBreakdownStatsComponent, FileBreakdownStatsComponent,
TopReadersComponent, TopReadersComponent,
ReadByDayAndComponent ReadByDayAndComponent,
GenericListModalComponent,
DayBreakdownComponent,
DayOfWeekPipe
], ],
imports: [ imports: [
CommonModule, CommonModule,
TableModule, TableModule,
SharedModule, SharedModule,
NgbTooltipModule, NgbTooltipModule,
NgbModalModule,
ReactiveFormsModule, ReactiveFormsModule,
PipeModule, PipeModule,

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0", "name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
}, },
"version": "0.6.1.18" "version": "0.6.1.21"
}, },
"servers": [ "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": { "/api/Stats/user/reading-history": {
"get": { "get": {
"tags": [ "tags": [
@ -10293,6 +10331,32 @@
}, },
"additionalProperties": false "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": { "DeleteSeriesDto": {
"type": "object", "type": "object",
"properties": { "properties": {