mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 04:44:37 +03:00
1 line
185 KiB
Plaintext
1 line
185 KiB
Plaintext
[{"/home/peroo/stash/ui/v2/src/App.tsx":"1","/home/peroo/stash/ui/v2/src/components/ErrorBoundary.tsx":"2","/home/peroo/stash/ui/v2/src/components/Galleries/Galleries.tsx":"3","/home/peroo/stash/ui/v2/src/components/Galleries/Gallery.tsx":"4","/home/peroo/stash/ui/v2/src/components/Galleries/GalleryList.tsx":"5","/home/peroo/stash/ui/v2/src/components/Galleries/GalleryViewer.tsx":"6","/home/peroo/stash/ui/v2/src/components/MainNavbar.tsx":"7","/home/peroo/stash/ui/v2/src/components/PageNotFound.tsx":"8","/home/peroo/stash/ui/v2/src/components/Settings/Settings.tsx":"9","/home/peroo/stash/ui/v2/src/components/Settings/SettingsAboutPanel.tsx":"10","/home/peroo/stash/ui/v2/src/components/Settings/SettingsConfigurationPanel.tsx":"11","/home/peroo/stash/ui/v2/src/components/Settings/SettingsInterfacePanel.tsx":"12","/home/peroo/stash/ui/v2/src/components/Settings/SettingsLogsPanel.tsx":"13","/home/peroo/stash/ui/v2/src/components/Settings/SettingsTasksPanel/GenerateButton.tsx":"14","/home/peroo/stash/ui/v2/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx":"15","/home/peroo/stash/ui/v2/src/components/Shared/DetailsEditNavbar.tsx":"16","/home/peroo/stash/ui/v2/src/components/Shared/DurationInput.tsx":"17","/home/peroo/stash/ui/v2/src/components/Shared/FolderSelect/FolderSelect.tsx":"18","/home/peroo/stash/ui/v2/src/components/Shared/TagLink.tsx":"19","/home/peroo/stash/ui/v2/src/components/Stats.tsx":"20","/home/peroo/stash/ui/v2/src/components/Studios/StudioCard.tsx":"21","/home/peroo/stash/ui/v2/src/components/Studios/StudioDetails/Studio.tsx":"22","/home/peroo/stash/ui/v2/src/components/Studios/StudioList.tsx":"23","/home/peroo/stash/ui/v2/src/components/Studios/Studios.tsx":"24","/home/peroo/stash/ui/v2/src/components/Tags/TagList.tsx":"25","/home/peroo/stash/ui/v2/src/components/Tags/Tags.tsx":"26","/home/peroo/stash/ui/v2/src/components/Wall/WallItem.tsx":"27","/home/peroo/stash/ui/v2/src/components/Wall/WallPanel.tsx":"28","/home/peroo/stash/ui/v2/src/components/list/AddFilter.tsx":"29","/home/peroo/stash/ui/v2/src/components/list/ListFilter.tsx":"30","/home/peroo/stash/ui/v2/src/components/list/Pagination.tsx":"31","/home/peroo/stash/ui/v2/src/components/performers/PerformerCard.tsx":"32","/home/peroo/stash/ui/v2/src/components/performers/PerformerDetails/Performer.tsx":"33","/home/peroo/stash/ui/v2/src/components/performers/PerformerList.tsx":"34","/home/peroo/stash/ui/v2/src/components/performers/PerformerListTable.tsx":"35","/home/peroo/stash/ui/v2/src/components/performers/performers.tsx":"36","/home/peroo/stash/ui/v2/src/components/scenes/SceneCard.tsx":"37","/home/peroo/stash/ui/v2/src/components/scenes/SceneDetails/Scene.tsx":"38","/home/peroo/stash/ui/v2/src/components/scenes/SceneDetails/SceneDetailPanel.tsx":"39","/home/peroo/stash/ui/v2/src/components/scenes/SceneDetails/SceneEditPanel.tsx":"40","/home/peroo/stash/ui/v2/src/components/scenes/SceneDetails/SceneFileInfoPanel.tsx":"41","/home/peroo/stash/ui/v2/src/components/scenes/SceneDetails/SceneMarkersPanel.tsx":"42","/home/peroo/stash/ui/v2/src/components/scenes/SceneDetails/ScenePerformerPanel.tsx":"43","/home/peroo/stash/ui/v2/src/components/scenes/SceneFilenameParser.tsx":"44","/home/peroo/stash/ui/v2/src/components/scenes/SceneList.tsx":"45","/home/peroo/stash/ui/v2/src/components/scenes/SceneListTable.tsx":"46","/home/peroo/stash/ui/v2/src/components/scenes/SceneMarkerList.tsx":"47","/home/peroo/stash/ui/v2/src/components/scenes/ScenePlayer/ScenePlayer.tsx":"48","/home/peroo/stash/ui/v2/src/components/scenes/ScenePlayer/ScenePlayerScrubber.tsx":"49","/home/peroo/stash/ui/v2/src/components/scenes/SceneSelectedOptions.tsx":"50","/home/peroo/stash/ui/v2/src/components/scenes/helpers.tsx":"51","/home/peroo/stash/ui/v2/src/components/scenes/scenes.tsx":"52","/home/peroo/stash/ui/v2/src/components/select/FilterMultiSelect.tsx":"53","/home/peroo/stash/ui/v2/src/components/select/FilterSelect.tsx":"54","/home/peroo/stash/ui/v2/src/components/select/MarkerTitleSuggest.tsx":"55","/home/peroo/stash/ui/v2/src/components/select/ScrapePerformerSuggest.tsx":"56","/home/peroo/stash/ui/v2/src/components/select/ValidGalleriesSelect.tsx":"57","/home/peroo/stash/ui/v2/src/core/StashService.ts":"58","/home/peroo/stash/ui/v2/src/core/generated-graphql.tsx":"59","/home/peroo/stash/ui/v2/src/hooks/ListHook.tsx":"60","/home/peroo/stash/ui/v2/src/hooks/LocalForage.ts":"61","/home/peroo/stash/ui/v2/src/hooks/VideoHover.ts":"62","/home/peroo/stash/ui/v2/src/index.tsx":"63","/home/peroo/stash/ui/v2/src/models/base-props.ts":"64","/home/peroo/stash/ui/v2/src/models/index.ts":"65","/home/peroo/stash/ui/v2/src/models/list-filter/criteria/criterion.ts":"66","/home/peroo/stash/ui/v2/src/models/list-filter/criteria/favorite.ts":"67","/home/peroo/stash/ui/v2/src/models/list-filter/criteria/has-markers.ts":"68","/home/peroo/stash/ui/v2/src/models/list-filter/criteria/is-missing.ts":"69","/home/peroo/stash/ui/v2/src/models/list-filter/criteria/none.ts":"70","/home/peroo/stash/ui/v2/src/models/list-filter/criteria/performers.ts":"71","/home/peroo/stash/ui/v2/src/models/list-filter/criteria/rating.ts":"72","/home/peroo/stash/ui/v2/src/models/list-filter/criteria/resolution.ts":"73","/home/peroo/stash/ui/v2/src/models/list-filter/criteria/studios.ts":"74","/home/peroo/stash/ui/v2/src/models/list-filter/criteria/tags.ts":"75","/home/peroo/stash/ui/v2/src/models/list-filter/criteria/utils.ts":"76","/home/peroo/stash/ui/v2/src/models/list-filter/filter.ts":"77","/home/peroo/stash/ui/v2/src/models/list-filter/types.ts":"78","/home/peroo/stash/ui/v2/src/models/react-images.d.ts":"79","/home/peroo/stash/ui/v2/src/models/react-jw-player.d.ts":"80","/home/peroo/stash/ui/v2/src/models/types.ts":"81","/home/peroo/stash/ui/v2/src/react-app-env.d.ts":"82","/home/peroo/stash/ui/v2/src/serviceWorker.ts":"83","/home/peroo/stash/ui/v2/src/utils/color.ts":"84","/home/peroo/stash/ui/v2/src/utils/errors.ts":"85","/home/peroo/stash/ui/v2/src/utils/navigation.ts":"86","/home/peroo/stash/ui/v2/src/utils/table.tsx":"87","/home/peroo/stash/ui/v2/src/utils/text.ts":"88","/home/peroo/stash/ui/v2/src/utils/toasts.ts":"89","/home/peroo/stash/ui/v2/src/utils/zoom.ts":"90"},{"size":1571,"mtime":1575810635928,"results":"91","hashOfConfig":"92"},{"size":769,"mtime":1575810635928,"results":"93","hashOfConfig":"92"},{"size":364,"mtime":1575810635928,"results":"94","hashOfConfig":"92"},{"size":1109,"mtime":1575819631714,"results":"95","hashOfConfig":"92"},{"size":1954,"mtime":1575810635928,"results":"96","hashOfConfig":"92"},{"size":1464,"mtime":1575810635928,"results":"97","hashOfConfig":"92"},{"size":3041,"mtime":1575810635928,"results":"98","hashOfConfig":"92"},{"size":154,"mtime":1575810635928,"results":"99","hashOfConfig":"92"},{"size":1885,"mtime":1575819631714,"results":"100","hashOfConfig":"92"},{"size":1317,"mtime":1575811824080,"results":"101","hashOfConfig":"92"},{"size":8087,"mtime":1575819631714,"results":"102","hashOfConfig":"92"},{"size":4442,"mtime":1575819631718,"results":"103","hashOfConfig":"92"},{"size":5104,"mtime":1575819631718,"results":"104","hashOfConfig":"92"},{"size":1767,"mtime":1575810635928,"results":"105","hashOfConfig":"92"},{"size":7508,"mtime":1575811824080,"results":"106","hashOfConfig":"92"},{"size":4090,"mtime":1575811824080,"results":"107","hashOfConfig":"92"},{"size":2809,"mtime":1575811824080,"results":"108","hashOfConfig":"92"},{"size":2973,"mtime":1575819631718,"results":"109","hashOfConfig":"92"},{"size":1258,"mtime":1575810635928,"results":"110","hashOfConfig":"92"},{"size":1858,"mtime":1575810635928,"results":"111","hashOfConfig":"92"},{"size":875,"mtime":1575810635928,"results":"112","hashOfConfig":"92"},{"size":5604,"mtime":1575819631718,"results":"113","hashOfConfig":"92"},{"size":1352,"mtime":1575810635928,"results":"114","hashOfConfig":"92"},{"size":364,"mtime":1575810635928,"results":"115","hashOfConfig":"92"},{"size":5680,"mtime":1575819631718,"results":"116","hashOfConfig":"92"},{"size":244,"mtime":1575810635928,"results":"117","hashOfConfig":"92"},{"size":4911,"mtime":1575811824080,"results":"118","hashOfConfig":"92"},{"size":2744,"mtime":1575810635928,"results":"119","hashOfConfig":"92"},{"size":6795,"mtime":1575811824080,"results":"120","hashOfConfig":"92"},{"size":7603,"mtime":1575811824080,"results":"121","hashOfConfig":"92"},{"size":3323,"mtime":1575810635928,"results":"122","hashOfConfig":"92"},{"size":1669,"mtime":1575810635928,"results":"123","hashOfConfig":"92"},{"size":14259,"mtime":1576598625042,"results":"124","hashOfConfig":"92"},{"size":1500,"mtime":1576598518665,"results":"125","hashOfConfig":"92"},{"size":2591,"mtime":1575810635928,"results":"126","hashOfConfig":"92"},{"size":397,"mtime":1575810635928,"results":"127","hashOfConfig":"92"},{"size":6923,"mtime":1576598490141,"results":"128","hashOfConfig":"92"},{"size":3548,"mtime":1575819631722,"results":"129","hashOfConfig":"92"},{"size":1498,"mtime":1576598390374,"results":"130","hashOfConfig":"92"},{"size":7524,"mtime":1576598365666,"results":"131","hashOfConfig":"92"},{"size":3150,"mtime":1575810635928,"results":"132","hashOfConfig":"92"},{"size":9201,"mtime":1576598346025,"results":"133","hashOfConfig":"92"},{"size":618,"mtime":1575810635928,"results":"134","hashOfConfig":"92"},{"size":32274,"mtime":1576598320443,"results":"135","hashOfConfig":"92"},{"size":2776,"mtime":1576598120984,"results":"136","hashOfConfig":"92"},{"size":3111,"mtime":1575810635928,"results":"137","hashOfConfig":"92"},{"size":1114,"mtime":1576598100401,"results":"138","hashOfConfig":"92"},{"size":7917,"mtime":1576598079008,"results":"139","hashOfConfig":"92"},{"size":10564,"mtime":1575819631746,"results":"140","hashOfConfig":"92"},{"size":8729,"mtime":1576597964447,"results":"141","hashOfConfig":"92"},{"size":1451,"mtime":1575810635928,"results":"142","hashOfConfig":"92"},{"size":484,"mtime":1575810635928,"results":"143","hashOfConfig":"92"},{"size":6521,"mtime":1576597838041,"results":"144","hashOfConfig":"92"},{"size":3823,"mtime":1576597810488,"results":"145","hashOfConfig":"92"},{"size":2275,"mtime":1575810635928,"results":"146","hashOfConfig":"92"},{"size":2608,"mtime":1576597793145,"results":"147","hashOfConfig":"92"},{"size":2695,"mtime":1576597777105,"results":"148","hashOfConfig":"92"},{"size":14701,"mtime":1575811824080,"results":"149","hashOfConfig":"92"},{"size":68793,"mtime":1575811113226,"results":"150","hashOfConfig":"92"},{"size":10880,"mtime":1576597208786,"results":"151","hashOfConfig":"92"},{"size":1659,"mtime":1575811824080,"results":"152","hashOfConfig":"92"},{"size":2144,"mtime":1575811824080,"results":"153","hashOfConfig":"92"},{"size":803,"mtime":1575810635928,"results":"154","hashOfConfig":"92"},{"size":124,"mtime":1575810635928,"results":"155","hashOfConfig":"92"},{"size":55,"mtime":1575810635928,"results":"156","hashOfConfig":"92"},{"size":6833,"mtime":1575810635928,"results":"157","hashOfConfig":"92"},{"size":659,"mtime":1575810635928,"results":"158","hashOfConfig":"92"},{"size":664,"mtime":1575810635928,"results":"159","hashOfConfig":"92"},{"size":682,"mtime":1575810635928,"results":"160","hashOfConfig":"92"},{"size":566,"mtime":1575810635928,"results":"161","hashOfConfig":"92"},{"size":966,"mtime":1575810635928,"results":"162","hashOfConfig":"92"},{"size":1013,"mtime":1575810635928,"results":"163","hashOfConfig":"92"},{"size":694,"mtime":1575810635928,"results":"164","hashOfConfig":"92"},{"size":881,"mtime":1575810635928,"results":"165","hashOfConfig":"92"},{"size":1287,"mtime":1575810635928,"results":"166","hashOfConfig":"92"},{"size":1965,"mtime":1575810635928,"results":"167","hashOfConfig":"92"},{"size":12393,"mtime":1575810635928,"results":"168","hashOfConfig":"92"},{"size":278,"mtime":1575810635928,"results":"169","hashOfConfig":"92"},{"size":187,"mtime":1575810635928,"results":"170","hashOfConfig":"92"},{"size":200,"mtime":1575810635928,"results":"171","hashOfConfig":"92"},{"size":74,"mtime":1575810635928,"results":"172","hashOfConfig":"92"},{"size":40,"mtime":1575810635928,"results":"173","hashOfConfig":"92"},{"size":5216,"mtime":1575810635928,"results":"174","hashOfConfig":"92"},{"size":308,"mtime":1575810635928,"results":"175","hashOfConfig":"92"},{"size":529,"mtime":1575810635928,"results":"176","hashOfConfig":"92"},{"size":2396,"mtime":1575810635928,"results":"177","hashOfConfig":"92"},{"size":4050,"mtime":1575810635928,"results":"178","hashOfConfig":"92"},{"size":2240,"mtime":1576597744487,"results":"179","hashOfConfig":"92"},{"size":275,"mtime":1575810635928,"results":"180","hashOfConfig":"92"},{"size":123,"mtime":1575811824080,"results":"181","hashOfConfig":"92"},{"filePath":"182","messages":"183","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"184"},"mugomj",{"filePath":"185","messages":"186","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"187","messages":"188","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"189","messages":"190","errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":1,"source":"191"},{"filePath":"192","messages":"193","errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"194"},{"filePath":"195","messages":"196","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"197"},{"filePath":"198","messages":"199","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"200","messages":"201","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"202","messages":"203","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":1,"source":"204"},{"filePath":"205","messages":"206","errorCount":0,"warningCount":5,"fixableErrorCount":0,"fixableWarningCount":0,"source":"207"},{"filePath":"208","messages":"209","errorCount":0,"warningCount":4,"fixableErrorCount":0,"fixableWarningCount":1,"source":"210"},{"filePath":"211","messages":"212","errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":1,"source":"213"},{"filePath":"214","messages":"215","errorCount":0,"warningCount":4,"fixableErrorCount":0,"fixableWarningCount":3,"source":"216"},{"filePath":"217","messages":"218","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"219","messages":"220","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"221"},{"filePath":"222","messages":"223","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"224"},{"filePath":"225","messages":"226","errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"227"},{"filePath":"228","messages":"229","errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":1,"source":"230"},{"filePath":"231","messages":"232","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"233"},{"filePath":"234","messages":"235","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"236"},{"filePath":"237","messages":"238","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"239","messages":"240","errorCount":0,"warningCount":10,"fixableErrorCount":0,"fixableWarningCount":2,"source":"241"},{"filePath":"242","messages":"243","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"244"},{"filePath":"245","messages":"246","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"247","messages":"248","errorCount":0,"warningCount":12,"fixableErrorCount":0,"fixableWarningCount":1,"source":"249"},{"filePath":"250","messages":"251","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"252","messages":"253","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"254"},{"filePath":"255","messages":"256","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"257"},{"filePath":"258","messages":"259","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"260","messages":"261","errorCount":0,"warningCount":4,"fixableErrorCount":0,"fixableWarningCount":0,"source":"262"},{"filePath":"263","messages":"264","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"265","messages":"266","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"267","messages":"268","errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":2,"source":null},{"filePath":"269","messages":"270","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"271","messages":"272","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"273","messages":"274","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"275","messages":"276","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"277","messages":"278","errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":2,"source":"279"},{"filePath":"280","messages":"281","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"282","messages":"283","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"284","messages":"285","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"286","messages":"287","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"288","messages":"289","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"290","messages":"291","errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":2,"source":"292"},{"filePath":"293","messages":"294","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"295","messages":"296","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"297","messages":"298","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"299","messages":"300","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"301","messages":"302","errorCount":0,"warningCount":6,"fixableErrorCount":0,"fixableWarningCount":2,"source":"303"},{"filePath":"304","messages":"305","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"306","messages":"307","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"308","messages":"309","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"310","messages":"311","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":1,"source":"312"},{"filePath":"313","messages":"314","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"315","messages":"316","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"317","messages":"318","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"319","messages":"320","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"321","messages":"322","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"323","messages":"324","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"325","messages":"326","errorCount":0,"warningCount":4,"fixableErrorCount":0,"fixableWarningCount":4,"source":"327"},{"filePath":"328","messages":"329","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"330","messages":"331","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"332","messages":"333","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"334","messages":"335","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"336","messages":"337","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"338","messages":"339","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"340","messages":"341","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"342","messages":"343","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"344","messages":"345","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"346","messages":"347","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"348","messages":"349","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"350","messages":"351","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"352","messages":"353","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"354","messages":"355","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"356","messages":"357","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"358","messages":"359","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"360","messages":"361","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"362","messages":"363","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"364","messages":"365","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"366","messages":"367","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"368","messages":"369","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"370","messages":"371","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"372","messages":"373","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"374","messages":"375","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"376","messages":"377","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"378","messages":"379","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"380","messages":"381","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"382","messages":"383","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"384","messages":"385","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"386","messages":"387","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/home/peroo/stash/ui/v2/src/App.tsx",["388"],"import React, { FunctionComponent, useEffect } from \"react\";\nimport { Route, Switch } from \"react-router-dom\";\nimport { ErrorBoundary } from \"./components/ErrorBoundary\";\nimport Galleries from \"./components/Galleries/Galleries\";\nimport { MainNavbar } from \"./components/MainNavbar\";\nimport { PageNotFound } from \"./components/PageNotFound\";\nimport Performers from \"./components/performers/performers\";\nimport Scenes from \"./components/scenes/scenes\";\nimport { Settings } from \"./components/Settings/Settings\";\nimport { Stats } from \"./components/Stats\";\nimport Studios from \"./components/Studios/Studios\";\nimport Tags from \"./components/Tags/Tags\";\nimport { SceneFilenameParser } from \"./components/scenes/SceneFilenameParser\";\n\ninterface IProps {}\n\nexport const App: FunctionComponent<IProps> = (props: IProps) => {\n return (\n <div className=\"bp3-dark\">\n <ErrorBoundary>\n <MainNavbar />\n <Switch>\n <Route exact={true} path=\"/\" component={Stats} />\n <Route path=\"/scenes\" component={Scenes} />\n {/* <Route path=\"/scenes/:id\" component={Scene} /> */}\n <Route path=\"/galleries\" component={Galleries} />\n <Route path=\"/performers\" component={Performers} />\n <Route path=\"/tags\" component={Tags} />\n <Route path=\"/studios\" component={Studios} />\n <Route path=\"/settings\" component={Settings} />\n <Route path=\"/sceneFilenameParser\" component={SceneFilenameParser} />\n <Route component={PageNotFound} />\n </Switch>\n </ErrorBoundary>\n </div>\n );\n};\n","/home/peroo/stash/ui/v2/src/components/ErrorBoundary.tsx",[],"/home/peroo/stash/ui/v2/src/components/Galleries/Galleries.tsx",[],"/home/peroo/stash/ui/v2/src/components/Galleries/Gallery.tsx",["389","390"],"import {\n Spinner,\n} from \"@blueprintjs/core\";\nimport _ from \"lodash\";\nimport React, { FunctionComponent, useEffect, useState } from \"react\";\nimport * as GQL from \"../../core/generated-graphql\";\nimport { StashService } from \"../../core/StashService\";\nimport { IBaseProps } from \"../../models\";\nimport { GalleryViewer } from \"./GalleryViewer\";\n\ninterface IProps extends IBaseProps {}\n\nexport const Gallery: FunctionComponent<IProps> = (props: IProps) => {\n const [gallery, setGallery] = useState<Partial<GQL.GalleryDataFragment>>({});\n const [isLoading, setIsLoading] = useState(false);\n\n const { data, error, loading } = StashService.useFindGallery(props.match.params.id);\n\n useEffect(() => {\n setIsLoading(loading);\n if (!data || !data.findGallery || !!error) { return; }\n setGallery(data.findGallery);\n }, [data]);\n\n if (!data || !data.findGallery || isLoading) { return <Spinner size={Spinner.SIZE_LARGE} />; }\n if (!!error) { return <>{error.message}</>; }\n return (\n <div style={{width: \"75vw\", margin: \"0 auto\"}}>\n <GalleryViewer gallery={gallery as any} />\n </div>\n );\n};\n","/home/peroo/stash/ui/v2/src/components/Galleries/GalleryList.tsx",["391","392"],"import { HTMLTable } from \"@blueprintjs/core\";\nimport _ from \"lodash\";\nimport React, { FunctionComponent } from \"react\";\nimport { QueryHookResult } from \"react-apollo-hooks\";\nimport { Link } from \"react-router-dom\";\nimport { FindGalleriesQuery, FindGalleriesVariables } from \"../../core/generated-graphql\";\nimport { ListHook } from \"../../hooks/ListHook\";\nimport { IBaseProps } from \"../../models/base-props\";\nimport { ListFilterModel } from \"../../models/list-filter/filter\";\nimport { DisplayMode, FilterMode } from \"../../models/list-filter/types\";\n\ninterface IProps extends IBaseProps {}\n\nexport const GalleryList: FunctionComponent<IProps> = (props: IProps) => {\n const listData = ListHook.useList({\n filterMode: FilterMode.Galleries,\n props,\n renderContent,\n });\n\n function renderContent(result: QueryHookResult<FindGalleriesQuery, FindGalleriesVariables>, filter: ListFilterModel) {\n if (!result.data || !result.data.findGalleries) { return; }\n if (filter.displayMode === DisplayMode.Grid) {\n return <h1>TODO</h1>;\n } else if (filter.displayMode === DisplayMode.List) {\n return (\n <HTMLTable style={{margin: \"0 auto\"}}>\n <thead>\n <tr>\n <th>Preview</th>\n <th>Path</th>\n </tr>\n </thead>\n <tbody>\n {result.data.findGalleries.galleries.map((gallery) => (\n <tr key={gallery.id}>\n <td>\n <Link to={`/galleries/${gallery.id}`}>\n {gallery.files.length > 0 ? <img src={`${gallery.files[0].path}?thumb=true`} /> : undefined}\n </Link>\n </td>\n <td><Link to={`/galleries/${gallery.id}`}>{gallery.path}</Link></td>\n </tr>\n ))}\n </tbody>\n </HTMLTable>\n );\n } else if (filter.displayMode === DisplayMode.Wall) {\n return <h1>TODO</h1>;\n }\n }\n\n return listData.template;\n};\n","/home/peroo/stash/ui/v2/src/components/Galleries/GalleryViewer.tsx",["393"],"import _ from \"lodash\";\nimport React, { FunctionComponent, useState } from \"react\";\nimport Lightbox from \"react-images\";\nimport Gallery from \"react-photo-gallery\";\nimport * as GQL from \"../../core/generated-graphql\";\n\ninterface IProps {\n gallery: GQL.GalleryDataFragment;\n}\n\nexport const GalleryViewer: FunctionComponent<IProps> = (props: IProps) => {\n const [currentImage, setCurrentImage] = useState<number>(0);\n const [lightboxIsOpen, setLightboxIsOpen] = useState<boolean>(false);\n\n function openLightbox(event: any, obj: any) {\n setCurrentImage(obj.index);\n setLightboxIsOpen(true);\n }\n function closeLightbox() {\n setCurrentImage(0);\n setLightboxIsOpen(false);\n }\n function gotoPrevious() {\n setCurrentImage(currentImage - 1);\n }\n function gotoNext() {\n setCurrentImage(currentImage + 1);\n }\n\n const photos = props.gallery.files.map((file) => ({src: file.path || \"\", caption: file.name}));\n const thumbs = props.gallery.files.map((file) => ({src: `${file.path}?thumb=true` || \"\", width: 1, height: 1}));\n return (\n <div>\n <Gallery photos={thumbs} columns={15} onClick={openLightbox} />\n <Lightbox\n images={photos}\n onClose={closeLightbox}\n onClickPrev={gotoPrevious}\n onClickNext={gotoNext}\n currentImage={currentImage}\n isOpen={lightboxIsOpen}\n onClickImage={() => window.open(photos[currentImage].src, \"_blank\")}\n width={9999}\n />\n </div>\n );\n};\n","/home/peroo/stash/ui/v2/src/components/MainNavbar.tsx",[],"/home/peroo/stash/ui/v2/src/components/PageNotFound.tsx",[],"/home/peroo/stash/ui/v2/src/components/Settings/Settings.tsx",["394"],"import {\n Card,\n Tab,\n Tabs,\n} from \"@blueprintjs/core\";\nimport queryString from \"query-string\";\nimport React, { FunctionComponent, useEffect, useState } from \"react\";\nimport { IBaseProps } from \"../../models\";\nimport { SettingsAboutPanel } from \"./SettingsAboutPanel\";\nimport { SettingsConfigurationPanel } from \"./SettingsConfigurationPanel\";\nimport { SettingsInterfacePanel } from \"./SettingsInterfacePanel\";\nimport { SettingsLogsPanel } from \"./SettingsLogsPanel\";\nimport { SettingsTasksPanel } from \"./SettingsTasksPanel/SettingsTasksPanel\";\n\ninterface IProps extends IBaseProps {}\n\ntype TabId = \"configuration\" | \"tasks\" | \"logs\" | \"about\";\n\nexport const Settings: FunctionComponent<IProps> = (props: IProps) => {\n const [tabId, setTabId] = useState<TabId>(getTabId());\n\n useEffect(() => {\n const location = Object.assign({}, props.history.location);\n location.search = queryString.stringify({tab: tabId}, {encode: false});\n props.history.replace(location);\n }, [tabId]);\n\n function getTabId(): TabId {\n const queryParams = queryString.parse(props.location.search);\n if (!queryParams.tab || typeof queryParams.tab !== \"string\") { return \"tasks\"; }\n return queryParams.tab as TabId;\n }\n\n return (\n <Card id=\"details-container\">\n <Tabs\n renderActiveTabPanelOnly={true}\n vertical={true}\n onChange={(newId) => setTabId(newId as TabId)}\n defaultSelectedTabId={getTabId()}\n >\n <Tab id=\"configuration\" title=\"Configuration\" panel={<SettingsConfigurationPanel />} />\n <Tab id=\"interface\" title=\"Interface Configuration\" panel={<SettingsInterfacePanel />} />\n <Tab id=\"tasks\" title=\"Tasks\" panel={<SettingsTasksPanel />} />\n <Tab id=\"logs\" title=\"Logs\" panel={<SettingsLogsPanel />} />\n <Tab id=\"about\" title=\"About\" panel={<SettingsAboutPanel />} />\n </Tabs>\n </Card>\n );\n};\n","/home/peroo/stash/ui/v2/src/components/Settings/SettingsAboutPanel.tsx",["395","396","397","398","399"],"import {\n H1,\n H4,\n H6,\n HTMLTable,\n Spinner,\n Tag,\n} from \"@blueprintjs/core\";\nimport React, { FunctionComponent } from \"react\";\nimport * as GQL from \"../../core/generated-graphql\";\nimport { TextUtils } from \"../../utils/text\";\nimport { StashService } from \"../../core/StashService\";\n\ninterface IProps {}\n\nexport const SettingsAboutPanel: FunctionComponent<IProps> = (props: IProps) => {\n const { data, error, loading } = StashService.useVersion();\n\n function maybeRenderTag() {\n if (!data || !data.version || !data.version.version) { return; }\n return (\n <tr>\n <td>Version:</td>\n <td>{data.version.version}</td>\n </tr>\n );\n }\n\n function renderVersion() {\n if (!data || !data.version) { return; }\n return (\n <>\n <HTMLTable>\n <tbody>\n {maybeRenderTag()}\n <tr>\n <td>Build hash:</td>\n <td>{data.version.hash}</td>\n </tr>\n <tr>\n <td>Build time:</td>\n <td>{data.version.build_time}</td>\n </tr>\n </tbody> \n </HTMLTable>\n </>\n );\n }\n return (\n <>\n <H4>About</H4>\n {!data || loading ? <Spinner size={Spinner.SIZE_LARGE} /> : undefined}\n {!!error ? <span>error.message</span> : undefined}\n {renderVersion()}\n </>\n );\n};\n","/home/peroo/stash/ui/v2/src/components/Settings/SettingsConfigurationPanel.tsx",["400","401","402","403"],"import {\n Button,\n Divider,\n FormGroup,\n H1,\n H4,\n H6,\n InputGroup,\n Spinner,\n Tag,\n Checkbox,\n HTMLSelect,\n} from \"@blueprintjs/core\";\nimport React, { FunctionComponent, useEffect, useState } from \"react\";\nimport * as GQL from \"../../core/generated-graphql\";\nimport { StashService } from \"../../core/StashService\";\nimport { ErrorUtils } from \"../../utils/errors\";\nimport { ToastUtils } from \"../../utils/toasts\";\nimport { FolderSelect } from \"../Shared/FolderSelect/FolderSelect\";\n\ninterface IProps {}\n\nexport const SettingsConfigurationPanel: FunctionComponent<IProps> = (props: IProps) => {\n // Editing config state\n const [stashes, setStashes] = useState<string[]>([]);\n const [databasePath, setDatabasePath] = useState<string | undefined>(undefined);\n const [generatedPath, setGeneratedPath] = useState<string | undefined>(undefined);\n const [maxTranscodeSize, setMaxTranscodeSize] = useState<GQL.StreamingResolutionEnum | undefined>(undefined);\n const [maxStreamingTranscodeSize, setMaxStreamingTranscodeSize] = useState<GQL.StreamingResolutionEnum | undefined>(undefined);\n const [username, setUsername] = useState<string | undefined>(undefined);\n const [password, setPassword] = useState<string | undefined>(undefined);\n const [logFile, setLogFile] = useState<string | undefined>();\n const [logOut, setLogOut] = useState<boolean>(true);\n const [logLevel, setLogLevel] = useState<string>(\"Info\");\n const [logAccess, setLogAccess] = useState<boolean>(true);\n\n const { data, error, loading } = StashService.useConfiguration();\n\n const updateGeneralConfig = StashService.useConfigureGeneral({\n stashes,\n databasePath,\n generatedPath,\n maxTranscodeSize,\n maxStreamingTranscodeSize,\n username,\n password,\n logFile,\n logOut,\n logLevel,\n logAccess,\n });\n\n useEffect(() => {\n if (!data || !data.configuration || !!error) { return; }\n const conf = StashService.nullToUndefined(data.configuration) as GQL.ConfigDataFragment;\n if (!!conf.general) {\n setStashes(conf.general.stashes || []);\n setDatabasePath(conf.general.databasePath);\n setGeneratedPath(conf.general.generatedPath);\n setMaxTranscodeSize(conf.general.maxTranscodeSize);\n setMaxStreamingTranscodeSize(conf.general.maxStreamingTranscodeSize);\n setUsername(conf.general.username);\n setPassword(conf.general.password);\n setLogFile(conf.general.logFile);\n setLogOut(conf.general.logOut);\n setLogLevel(conf.general.logLevel);\n setLogAccess(conf.general.logAccess);\n }\n }, [data]);\n\n function onStashesChanged(directories: string[]) {\n setStashes(directories);\n }\n\n async function onSave() {\n try {\n const result = await updateGeneralConfig();\n console.log(result);\n ToastUtils.success(\"Updated config\");\n } catch (e) {\n ErrorUtils.handle(e);\n }\n }\n\n const transcodeQualities = [\n GQL.StreamingResolutionEnum.Low,\n GQL.StreamingResolutionEnum.Standard,\n GQL.StreamingResolutionEnum.StandardHd,\n GQL.StreamingResolutionEnum.FullHd,\n GQL.StreamingResolutionEnum.FourK,\n GQL.StreamingResolutionEnum.Original\n ].map(resolutionToString);\n\n function resolutionToString(r : GQL.StreamingResolutionEnum | undefined) {\n switch (r) {\n case GQL.StreamingResolutionEnum.Low: return \"240p\";\n case GQL.StreamingResolutionEnum.Standard: return \"480p\";\n case GQL.StreamingResolutionEnum.StandardHd: return \"720p\";\n case GQL.StreamingResolutionEnum.FullHd: return \"1080p\";\n case GQL.StreamingResolutionEnum.FourK: return \"4k\";\n case GQL.StreamingResolutionEnum.Original: return \"Original\";\n }\n\n return \"Original\";\n }\n\n function translateQuality(quality : string) {\n switch (quality) {\n case \"240p\": return GQL.StreamingResolutionEnum.Low;\n case \"480p\": return GQL.StreamingResolutionEnum.Standard;\n case \"720p\": return GQL.StreamingResolutionEnum.StandardHd;\n case \"1080p\": return GQL.StreamingResolutionEnum.FullHd;\n case \"4k\": return GQL.StreamingResolutionEnum.FourK;\n case \"Original\": return GQL.StreamingResolutionEnum.Original;\n }\n\n return GQL.StreamingResolutionEnum.Original;\n }\n\n return (\n <>\n {!!error ? <h1>{error.message}</h1> : undefined}\n {(!data || !data.configuration || loading) ? <Spinner size={Spinner.SIZE_LARGE} /> : undefined}\n <H4>Library</H4>\n <FormGroup>\n <FormGroup>\n <FormGroup\n label=\"Stashes\"\n helperText=\"Directory locations to your content\"\n >\n <FolderSelect\n directories={stashes}\n onDirectoriesChanged={onStashesChanged}\n />\n </FormGroup>\n </FormGroup>\n \n <FormGroup\n label=\"Database Path\"\n helperText=\"File location for the SQLite database (requires restart)\"\n >\n <InputGroup value={databasePath} onChange={(e: any) => setDatabasePath(e.target.value)} />\n </FormGroup>\n\n <FormGroup\n label=\"Generated Path\"\n helperText=\"Directory location for the generated files (scene markers, scene previews, sprites, etc)\"\n >\n <InputGroup value={generatedPath} onChange={(e: any) => setGeneratedPath(e.target.value)} />\n </FormGroup>\n </FormGroup>\n \n <Divider />\n <FormGroup>\n <H4>Video</H4>\n <FormGroup \n label=\"Maximum transcode size\"\n helperText=\"Maximum size for generated transcodes\"\n >\n <HTMLSelect\n options={transcodeQualities}\n onChange={(event) => setMaxTranscodeSize(translateQuality(event.target.value))}\n value={resolutionToString(maxTranscodeSize)}\n />\n </FormGroup>\n <FormGroup \n label=\"Maximum streaming transcode size\"\n helperText=\"Maximum size for transcoded streams\"\n >\n <HTMLSelect\n options={transcodeQualities}\n onChange={(event) => setMaxStreamingTranscodeSize(translateQuality(event.target.value))}\n value={resolutionToString(maxStreamingTranscodeSize)}\n />\n </FormGroup>\n </FormGroup>\n <Divider />\n\n <FormGroup>\n <H4>Authentication</H4>\n <FormGroup\n label=\"Username\"\n helperText=\"Username to access Stash. Leave blank to disable user authentication\"\n >\n <InputGroup value={username} onChange={(e: any) => setUsername(e.target.value)} />\n </FormGroup>\n <FormGroup\n label=\"Password\"\n helperText=\"Password to access Stash. Leave blank to disable user authentication\"\n >\n <InputGroup type=\"password\" value={password} onChange={(e: any) => setPassword(e.target.value)} />\n </FormGroup>\n </FormGroup>\n\n <Divider />\n <H4>Logging</H4>\n <FormGroup\n label=\"Log file\"\n helperText=\"Path to the file to output logging to. Blank to disable file logging. Requires restart.\"\n >\n <InputGroup value={logFile} onChange={(e: any) => setLogFile(e.target.value)} />\n </FormGroup>\n\n <FormGroup\n helperText=\"Logs to the terminal in addition to a file. Always true if file logging is disabled. Requires restart.\"\n >\n <Checkbox\n checked={logOut}\n label=\"Log to terminal\"\n onChange={() => setLogOut(!logOut)}\n />\n </FormGroup>\n\n <FormGroup inline={true} label=\"Log Level\">\n <HTMLSelect\n options={[\"Debug\", \"Info\", \"Warning\", \"Error\"]}\n onChange={(event) => setLogLevel(event.target.value)}\n value={logLevel}\n />\n </FormGroup>\n\n <FormGroup\n helperText=\"Logs http access to the terminal. Requires restart.\"\n >\n <Checkbox\n checked={logAccess}\n label=\"Log http access\"\n onChange={() => setLogAccess(!logAccess)}\n />\n </FormGroup>\n\n <Divider />\n <Button intent=\"primary\" onClick={() => onSave()}>Save</Button>\n </>\n );\n};\n","/home/peroo/stash/ui/v2/src/components/Settings/SettingsInterfacePanel.tsx",["404","405"],"import {\n Button,\n Checkbox,\n Divider,\n FormGroup,\n H4,\n Spinner,\n TextArea,\n NumericInput\n} from \"@blueprintjs/core\";\nimport _ from \"lodash\";\nimport React, { FunctionComponent, useEffect, useState } from \"react\";\nimport { StashService } from \"../../core/StashService\";\nimport { ErrorUtils } from \"../../utils/errors\";\nimport { ToastUtils } from \"../../utils/toasts\";\n\ninterface IProps {}\n\nexport const SettingsInterfacePanel: FunctionComponent<IProps> = () => {\n const config = StashService.useConfiguration();\n const [soundOnPreview, setSoundOnPreview] = useState<boolean>();\n const [wallShowTitle, setWallShowTitle] = useState<boolean>();\n const [maximumLoopDuration, setMaximumLoopDuration] = useState<number>(0);\n const [autostartVideo, setAutostartVideo] = useState<boolean>();\n const [showStudioAsText, setShowStudioAsText] = useState<boolean>();\n const [css, setCSS] = useState<string>();\n const [cssEnabled, setCSSEnabled] = useState<boolean>();\n\n const updateInterfaceConfig = StashService.useConfigureInterface({\n soundOnPreview,\n wallShowTitle,\n maximumLoopDuration,\n autostartVideo,\n showStudioAsText,\n css,\n cssEnabled\n });\n\n useEffect(() => {\n if (!config.data || !config.data.configuration || !!config.error) { return; }\n if (!!config.data.configuration.interface) {\n let iCfg = config.data.configuration.interface;\n setSoundOnPreview(iCfg.soundOnPreview !== undefined ? iCfg.soundOnPreview : true);\n setWallShowTitle(iCfg.wallShowTitle !== undefined ? iCfg.wallShowTitle : true);\n setMaximumLoopDuration(iCfg.maximumLoopDuration || 0);\n setAutostartVideo(iCfg.autostartVideo !== undefined ? iCfg.autostartVideo : false);\n setShowStudioAsText(iCfg.showStudioAsText !== undefined ? iCfg.showStudioAsText : false);\n setCSS(config.data.configuration.interface.css || \"\");\n setCSSEnabled(config.data.configuration.interface.cssEnabled || false);\n }\n }, [config.data]);\n\n async function onSave() {\n try {\n const result = await updateInterfaceConfig();\n console.log(result);\n ToastUtils.success(\"Updated config\");\n } catch (e) {\n ErrorUtils.handle(e);\n }\n }\n\n return (\n <>\n {!!config.error ? <h1>{config.error.message}</h1> : undefined}\n {(!config.data || !config.data.configuration || config.loading) ? <Spinner size={Spinner.SIZE_LARGE} /> : undefined}\n <H4>User Interface</H4>\n <FormGroup\n label=\"Scene / Marker Wall\"\n helperText=\"Configuration for wall items\"\n >\n <Checkbox\n checked={wallShowTitle}\n label=\"Display title and tags\"\n onChange={() => setWallShowTitle(!wallShowTitle)}\n />\n <Checkbox\n checked={soundOnPreview}\n label=\"Enable sound\"\n onChange={() => setSoundOnPreview(!soundOnPreview)}\n />\n </FormGroup>\n\n <FormGroup\n label=\"Scene List\"\n >\n <Checkbox\n checked={showStudioAsText}\n label=\"Show Studios as text\"\n onChange={() => {\n setShowStudioAsText(!showStudioAsText)\n }}\n />\n </FormGroup>\n \n <FormGroup\n label=\"Scene Player\"\n >\n <Checkbox\n checked={autostartVideo}\n label=\"Auto-start video\"\n onChange={() => {\n setAutostartVideo(!autostartVideo)\n }}\n />\n\n <FormGroup\n label=\"Maximum loop duration\"\n helperText=\"Maximum scene duration - in seconds - where scene player will loop the video - 0 to disable\"\n >\n <NumericInput \n value={maximumLoopDuration} \n type=\"number\"\n onValueChange={(value: number) => setMaximumLoopDuration(value)}\n min={0}\n minorStepSize={1}\n />\n </FormGroup>\n </FormGroup>\n\n <FormGroup\n label=\"Custom CSS\"\n helperText=\"Page must be reloaded for changes to take effect.\"\n >\n <Checkbox\n checked={cssEnabled}\n label=\"Custom CSS enabled\"\n onChange={() => {\n setCSSEnabled(!cssEnabled)\n }}\n />\n\n <TextArea \n value={css} \n onChange={(e: any) => setCSS(e.target.value)}\n fill={true}\n rows={16}>\n </TextArea>\n </FormGroup>\n\n <Divider />\n <Button intent=\"primary\" onClick={() => onSave()}>Save</Button>\n </>\n );\n};\n","/home/peroo/stash/ui/v2/src/components/Settings/SettingsLogsPanel.tsx",["406","407","408","409"],"import {\n H4, FormGroup, HTMLSelect,\n} from \"@blueprintjs/core\";\nimport React, { FunctionComponent, useState, useEffect, useRef } from \"react\";\nimport * as GQL from \"../../core/generated-graphql\";\nimport { StashService } from \"../../core/StashService\";\n\ninterface IProps {}\n\nfunction convertTime(logEntry : GQL.LogEntryDataFragment) {\n function pad(val : number) {\n var ret = val.toString();\n if (val <= 9) {\n ret = \"0\" + ret;\n }\n\n return ret;\n }\n\n var date = new Date(logEntry.time);\n var month = date.getMonth() + 1;\n var day = date.getDate();\n var dateStr = date.getFullYear() + \"-\" + pad(month) + \"-\" + pad(day);\n dateStr += \" \" + pad(date.getHours()) + \":\" + pad(date.getMinutes()) + \":\" + pad(date.getSeconds());\n\n return dateStr;\n}\n\nclass LogEntry {\n public time: string;\n public level: string;\n public message: string;\n public id: string;\n\n private static nextId: number = 0;\n\n public constructor(logEntry: GQL.LogEntryDataFragment) {\n this.time = convertTime(logEntry);\n this.level = logEntry.level;\n this.message = logEntry.message;\n\n var id = LogEntry.nextId++;\n this.id = id.toString();\n }\n}\n\nexport const SettingsLogsPanel: FunctionComponent<IProps> = (props: IProps) => {\n const { data, error } = StashService.useLoggingSubscribe();\n const { data: existingData } = StashService.useLogs();\n \n const logEntries = useRef<LogEntry[]>([]);\n const [logLevel, setLogLevel] = useState<string>(\"Info\");\n const [filteredLogEntries, setFilteredLogEntries] = useState<LogEntry[]>([]);\n const lastUpdate = useRef<number>(0);\n const updateTimeout = useRef<NodeJS.Timeout>();\n\n // maximum number of log entries to display. Subsequent entries will truncate \n // the list, dropping off the oldest entries first.\n const MAX_LOG_ENTRIES = 200;\n\n function truncateLogEntries(entries : LogEntry[]) {\n entries.length = Math.min(entries.length, MAX_LOG_ENTRIES);\n }\n\n function prependLogEntries(toPrepend : LogEntry[]) {\n var newLogEntries = toPrepend.concat(logEntries.current);\n truncateLogEntries(newLogEntries);\n logEntries.current = newLogEntries;\n }\n\n function appendLogEntries(toAppend : LogEntry[]) {\n var newLogEntries = logEntries.current.concat(toAppend);\n truncateLogEntries(newLogEntries);\n logEntries.current = newLogEntries;\n }\n\n useEffect(() => {\n if (!data) { return; }\n\n // append data to the logEntries\n var convertedData = data.loggingSubscribe.map(convertLogEntry);\n\n // filter subscribed data as it comes in, otherwise we'll end up\n // truncating stuff that wasn't filtered out\n convertedData = convertedData.filter(filterByLogLevel)\n \n // put newest entries at the top\n convertedData.reverse();\n prependLogEntries(convertedData);\n\n updateFilteredEntries();\n }, [data]);\n\n useEffect(() => {\n if (!existingData || !existingData.logs) { return; }\n\n var convertedData = existingData.logs.map(convertLogEntry);\n appendLogEntries(convertedData);\n\n updateFilteredEntries();\n }, [existingData]);\n\n function updateFilteredEntries() {\n if (!updateTimeout.current) {\n console.log(\"Updating after timeout\");\n }\n updateTimeout.current = undefined;\n\n var filteredEntries = logEntries.current.filter(filterByLogLevel);\n setFilteredLogEntries(filteredEntries);\n\n lastUpdate.current = new Date().getTime();\n }\n\n useEffect(() => {\n updateFilteredEntries();\n }, [logLevel]);\n\n function convertLogEntry(logEntry : GQL.LogEntryDataFragment) {\n return new LogEntry(logEntry);\n }\n\n function levelClass(level : string) {\n return level.toLowerCase().trim();\n }\n\n interface ILogElementProps {\n logEntry : LogEntry\n }\n\n function LogElement(props : ILogElementProps) {\n // pad to maximum length of level enum\n var level = props.logEntry.level.padEnd(GQL.LogLevel.Progress.length);\n\n return (\n <>\n <span>{props.logEntry.time}</span> \n <span className={levelClass(props.logEntry.level)}>{level}</span> \n <span>{props.logEntry.message}</span>\n <br/>\n </>\n );\n }\n\n function maybeRenderError() {\n if (error) {\n return (\n <>\n <span className={\"error\"}>Error connecting to log server: {error.message}</span><br/>\n </>\n );\n }\n }\n\n const logLevels = [\"Debug\", \"Info\", \"Warning\", \"Error\"];\n\n function filterByLogLevel(logEntry : LogEntry) {\n if (logLevel == \"Debug\") {\n return true;\n }\n\n var logLevelIndex = logLevels.indexOf(logLevel);\n var levelIndex = logLevels.indexOf(logEntry.level);\n\n return levelIndex >= logLevelIndex;\n }\n\n return (\n <>\n <H4>Logs</H4>\n <div>\n <FormGroup inline={true} label=\"Log Level\">\n <HTMLSelect\n options={logLevels}\n onChange={(event) => setLogLevel(event.target.value)}\n value={logLevel}\n />\n </FormGroup>\n </div>\n <div className=\"logs\">\n {maybeRenderError()}\n {filteredLogEntries.map((logEntry) =>\n <LogElement logEntry={logEntry} key={logEntry.id}/>\n )}\n </div>\n </>\n );\n};\n","/home/peroo/stash/ui/v2/src/components/Settings/SettingsTasksPanel/GenerateButton.tsx",[],"/home/peroo/stash/ui/v2/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx",["410"],"import {\n Alert,\n Button,\n Checkbox,\n Divider,\n FormGroup,\n H4,\n AnchorButton,\n ProgressBar,\n H5,\n} from \"@blueprintjs/core\";\nimport React, { FunctionComponent, useState, useEffect } from \"react\";\nimport { StashService } from \"../../../core/StashService\";\nimport { ErrorUtils } from \"../../../utils/errors\";\nimport { ToastUtils } from \"../../../utils/toasts\";\nimport { GenerateButton } from \"./GenerateButton\";\nimport { Link } from \"react-router-dom\";\n\ninterface IProps {}\n\nexport const SettingsTasksPanel: FunctionComponent<IProps> = (props: IProps) => {\n const [isImportAlertOpen, setIsImportAlertOpen] = useState<boolean>(false);\n const [isCleanAlertOpen, setIsCleanAlertOpen] = useState<boolean>(false);\n const [nameFromMetadata, setNameFromMetadata] = useState<boolean>(true);\n const [status, setStatus] = useState<string>(\"\");\n const [progress, setProgress] = useState<number | undefined>(undefined);\n\n const [autoTagPerformers, setAutoTagPerformers] = useState<boolean>(true);\n const [autoTagStudios, setAutoTagStudios] = useState<boolean>(true);\n const [autoTagTags, setAutoTagTags] = useState<boolean>(true);\n\n const jobStatus = StashService.useJobStatus();\n const metadataUpdate = StashService.useMetadataUpdate();\n\n function statusToText(status : string) {\n switch(status) {\n case \"Idle\":\n return \"Idle\";\n case \"Scan\":\n return \"Scanning for new content\";\n case \"Generate\":\n return \"Generating supporting files\";\n case \"Clean\":\n return \"Cleaning the database\";\n case \"Export\":\n return \"Exporting to JSON\";\n case \"Import\":\n return \"Importing from JSON\";\n case \"Auto Tag\":\n return \"Auto tagging scenes\";\n }\n\n return \"Idle\";\n }\n\n useEffect(() => {\n if (!!jobStatus.data && !!jobStatus.data.jobStatus) {\n setStatus(statusToText(jobStatus.data.jobStatus.status));\n var newProgress = jobStatus.data.jobStatus.progress;\n if (newProgress < 0) {\n setProgress(undefined);\n } else {\n setProgress(newProgress);\n }\n }\n }, [jobStatus.data]);\n\n useEffect(() => {\n if (!!metadataUpdate.data && !!metadataUpdate.data.metadataUpdate) {\n setStatus(statusToText(metadataUpdate.data.metadataUpdate.status));\n var newProgress = metadataUpdate.data.metadataUpdate.progress;\n if (newProgress < 0) {\n setProgress(undefined);\n } else {\n setProgress(newProgress);\n }\n }\n }, [metadataUpdate.data]);\n\n function onImport() {\n setIsImportAlertOpen(false);\n StashService.queryMetadataImport().then(() => { jobStatus.refetch()});\n }\n\n function renderImportAlert() {\n return (\n <Alert\n cancelButtonText=\"Cancel\"\n confirmButtonText=\"Import\"\n icon=\"trash\"\n intent=\"danger\"\n isOpen={isImportAlertOpen}\n onCancel={() => setIsImportAlertOpen(false)}\n onConfirm={() => onImport()}\n >\n <p>\n Are you sure you want to import? This will delete the database and re-import from\n your exported metadata.\n </p>\n </Alert>\n );\n }\n\n function onClean() {\n setIsCleanAlertOpen(false);\n StashService.queryMetadataClean().then(() => { jobStatus.refetch()});\n }\n\n function renderCleanAlert() {\n return (\n <Alert\n cancelButtonText=\"Cancel\"\n confirmButtonText=\"Clean\"\n icon=\"trash\"\n intent=\"danger\"\n isOpen={isCleanAlertOpen}\n onCancel={() => setIsCleanAlertOpen(false)}\n onConfirm={() => onClean()}\n >\n <p>\n Are you sure you want to Clean?\n This will delete db information and generated content\n for all scenes that are no longer found in the filesystem.\n </p>\n </Alert>\n );\n }\n\n async function onScan() {\n try {\n await StashService.queryMetadataScan({nameFromMetadata});\n ToastUtils.success(\"Started scan\");\n jobStatus.refetch();\n } catch (e) {\n ErrorUtils.handle(e);\n }\n }\n\n function getAutoTagInput() {\n var wildcard = [\"*\"];\n return {\n performers: autoTagPerformers ? wildcard : [],\n studios: autoTagStudios ? wildcard : [],\n tags: autoTagTags ? wildcard : []\n }\n }\n\n async function onAutoTag() {\n try {\n await StashService.queryMetadataAutoTag(getAutoTagInput());\n ToastUtils.success(\"Started auto tagging\");\n jobStatus.refetch();\n } catch (e) {\n ErrorUtils.handle(e);\n }\n }\n\n function maybeRenderStop() {\n if (!status || status === \"Idle\") {\n return undefined;\n }\n\n return (\n <>\n <FormGroup>\n <Button id=\"stop\" text=\"Stop\" intent=\"danger\" onClick={() => StashService.queryStopJob().then(() => jobStatus.refetch())} />\n </FormGroup>\n </>\n );\n }\n\n function renderJobStatus() {\n return (\n <>\n <FormGroup>\n <H5>Status: {status}</H5>\n {!!status && status !== \"Idle\" ? <ProgressBar value={progress}/> : undefined}\n </FormGroup>\n {maybeRenderStop()}\n </>\n );\n }\n\n return (\n <>\n {renderImportAlert()}\n {renderCleanAlert()}\n\n <H4>Running Jobs</H4>\n\n {renderJobStatus()}\n\n <Divider/>\n\n <H4>Library</H4>\n <FormGroup\n helperText=\"Scan for new content and add it to the database.\"\n labelFor=\"scan\"\n inline={true}\n >\n <Checkbox\n checked={nameFromMetadata}\n label=\"Set name from metadata (if present)\"\n onChange={() => setNameFromMetadata(!nameFromMetadata)}\n />\n <Button id=\"scan\" text=\"Scan\" onClick={() => onScan()} />\n </FormGroup>\n\n <Divider />\n\n <H4>Auto Tagging</H4>\n\n <FormGroup\n helperText=\"Auto-tag content based on filenames.\"\n labelFor=\"autoTag\"\n inline={true}\n >\n <Checkbox\n checked={autoTagPerformers}\n label=\"Performers\"\n onChange={() => setAutoTagPerformers(!autoTagPerformers)}\n />\n <Checkbox\n checked={autoTagStudios}\n label=\"Studios\"\n onChange={() => setAutoTagStudios(!autoTagStudios)}\n />\n <Checkbox\n checked={autoTagTags}\n label=\"Tags\"\n onChange={() => setAutoTagTags(!autoTagTags)}\n />\n <Button id=\"autoTag\" text=\"Auto Tag\" onClick={() => onAutoTag()} />\n </FormGroup>\n\n <FormGroup>\n <Link className=\"bp3-button\" to={\"/sceneFilenameParser\"}>\n Scene Filename Parser\n </Link>\n </FormGroup>\n <Divider />\n\n <H4>Generated Content</H4>\n <GenerateButton />\n <FormGroup\n helperText=\"Check for missing files and remove them from the database. This is a destructive action.\"\n labelFor=\"clean\"\n inline={true}\n >\n <Button id=\"clean\" text=\"Clean\" intent=\"danger\" onClick={() => setIsCleanAlertOpen(true)} />\n </FormGroup>\n <Divider />\n\n <H4>Metadata</H4>\n <FormGroup\n helperText=\"Export the database content into JSON format\"\n labelFor=\"export\"\n inline={true}\n >\n <Button id=\"export\" text=\"Export\" onClick={() => StashService.queryMetadataExport().then(() => { jobStatus.refetch()})} />\n </FormGroup>\n\n <FormGroup\n helperText=\"Import from exported JSON. This is a destructive action.\"\n labelFor=\"import\"\n inline={true}\n >\n <Button id=\"import\" text=\"Import\" intent=\"danger\" onClick={() => setIsImportAlertOpen(true)} />\n </FormGroup>\n </>\n );\n};\n","/home/peroo/stash/ui/v2/src/components/Shared/DetailsEditNavbar.tsx",["411"],"import {\n Alert,\n Button,\n FileInput,\n Menu,\n MenuItem,\n Navbar,\n NavbarDivider,\n Popover,\n} from \"@blueprintjs/core\";\nimport _ from \"lodash\";\nimport React, { FunctionComponent, useState } from \"react\";\nimport { Link } from \"react-router-dom\";\nimport * as GQL from \"../../core/generated-graphql\";\nimport { NavigationUtils } from \"../../utils/navigation\";\n\ninterface IProps {\n performer?: Partial<GQL.PerformerDataFragment>;\n studio?: Partial<GQL.StudioDataFragment>;\n isNew: boolean;\n isEditing: boolean;\n onToggleEdit: () => void;\n onSave: () => void;\n onDelete: () => void;\n onAutoTag?: () => void;\n onImageChange: (event: React.FormEvent<HTMLInputElement>) => void;\n\n // TODO: only for performers. make generic\n scrapers?: GQL.ListScrapersListScrapers[];\n onDisplayScraperDialog?: (scraper: GQL.ListScrapersListScrapers) => void;\n}\n\nexport const DetailsEditNavbar: FunctionComponent<IProps> = (props: IProps) => {\n const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);\n\n function renderEditButton() {\n if (props.isNew) { return; }\n return (\n <Button\n intent=\"primary\"\n text={props.isEditing ? \"Cancel\" : \"Edit\"}\n onClick={() => props.onToggleEdit()}\n />\n );\n }\n\n function renderSaveButton() {\n if (!props.isEditing) { return; }\n return <Button intent=\"success\" text=\"Save\" onClick={() => props.onSave()} />;\n }\n\n function renderDeleteButton() {\n if (props.isNew || props.isEditing) { return; }\n return <Button intent=\"danger\" text=\"Delete\" onClick={() => setIsDeleteAlertOpen(true)} />;\n }\n\n function renderImageInput() {\n if (!props.isEditing) { return; }\n return <FileInput text=\"Choose image...\" onInputChange={props.onImageChange} inputProps={{accept: \".jpg,.jpeg\"}} />;\n }\n\n function renderScraperMenuItem(scraper : GQL.ListScrapersListScrapers) {\n return (\n <MenuItem\n text={scraper.name}\n onClick={() => { if (props.onDisplayScraperDialog) { props.onDisplayScraperDialog(scraper); }}}\n />\n );\n }\n\n function renderScraperMenu() {\n if (!props.performer) { return; }\n if (!props.isEditing) { return; }\n const scraperMenu = (\n <Menu>\n {props.scrapers ? props.scrapers.map((s) => renderScraperMenuItem(s)) : undefined}\n </Menu>\n );\n return (\n <Popover content={scraperMenu} position=\"bottom\">\n <Button text=\"Scrape with...\"/>\n </Popover>\n );\n }\n\n function renderAutoTagButton() {\n if (props.isNew || props.isEditing) { return; }\n if (!!props.onAutoTag) {\n return (<Button text=\"Auto Tag\" onClick={() => {\n if (props.onAutoTag) { props.onAutoTag() }\n }}></Button>)\n }\n }\n\n function renderScenesButton() {\n if (props.isEditing) { return; }\n let linkSrc: string = \"#\";\n if (!!props.performer) {\n linkSrc = NavigationUtils.makePerformerScenesUrl(props.performer);\n } else if (!!props.studio) {\n linkSrc = NavigationUtils.makeStudioScenesUrl(props.studio);\n }\n return (\n <Link className=\"bp3-button\" to={linkSrc}>\n Scenes\n </Link>\n );\n }\n\n function renderDeleteAlert() {\n var name;\n\n if (props.performer) {\n name = props.performer.name;\n }\n if (props.studio) {\n name = props.studio.name;\n }\n\n return (\n <Alert\n cancelButtonText=\"Cancel\"\n confirmButtonText=\"Delete\"\n icon=\"trash\"\n intent=\"danger\"\n isOpen={isDeleteAlertOpen}\n onCancel={() => setIsDeleteAlertOpen(false)}\n onConfirm={() => props.onDelete()}\n >\n <p>\n Are you sure you want to delete {name}?\n </p>\n </Alert>\n );\n }\n\n\n return (\n <>\n {renderDeleteAlert()}\n <Navbar>\n <Navbar.Group>\n {renderEditButton()}\n {props.isEditing && !props.isNew ? <NavbarDivider /> : undefined}\n {renderScraperMenu()}\n {renderImageInput()}\n {renderSaveButton()}\n\n {renderAutoTagButton()}\n {renderScenesButton()}\n {renderDeleteButton()}\n </Navbar.Group>\n </Navbar>\n </>\n );\n};\n","/home/peroo/stash/ui/v2/src/components/Shared/DurationInput.tsx",["412","413"],"import React, { FunctionComponent, useState, useEffect } from \"react\";\nimport { InputGroup, ButtonGroup, Button, IInputGroupProps, HTMLInputProps, ControlGroup } from \"@blueprintjs/core\";\nimport { TextUtils } from \"../../utils/text\";\nimport { FIXED, NUMERIC_INPUT } from \"@blueprintjs/core/lib/esm/common/classes\";\n\ninterface IProps {\n disabled?: boolean\n numericValue: number\n onValueChange(valueAsNumber: number): void\n onReset?(): void\n}\n\nexport const DurationInput: FunctionComponent<HTMLInputProps & IProps> = (props: IProps) => {\n const [value, setValue] = useState<string>(secondsToString(props.numericValue));\n\n useEffect(() => {\n setValue(secondsToString(props.numericValue));\n }, [props.numericValue]);\n\n function secondsToString(seconds : number) {\n let ret = TextUtils.secondsToTimestamp(seconds);\n\n if (ret.startsWith(\"00:\")) {\n ret = ret.substr(3);\n\n if (ret.startsWith(\"0\")) {\n ret = ret.substr(1);\n }\n }\n\n return ret;\n }\n\n function stringToSeconds(v : string) {\n if (!v) {\n return 0;\n }\n \n let splits = v.split(\":\");\n\n if (splits.length > 3) {\n return 0;\n }\n\n let seconds = 0;\n let factor = 1;\n while(splits.length > 0) {\n let thisSplit = splits.pop();\n if (thisSplit == undefined) {\n return 0;\n }\n\n let thisInt = parseInt(thisSplit, 10);\n if (isNaN(thisInt)) {\n return 0;\n }\n\n seconds += factor * thisInt;\n factor *= 60;\n }\n\n return seconds;\n }\n\n function increment() {\n let seconds = stringToSeconds(value);\n seconds += 1;\n props.onValueChange(seconds);\n }\n\n function decrement() {\n let seconds = stringToSeconds(value);\n seconds -= 1;\n props.onValueChange(seconds);\n }\n\n function renderButtons() {\n return (\n <ButtonGroup\n vertical={true}\n className={FIXED}\n >\n <Button\n icon=\"chevron-up\"\n disabled={props.disabled}\n onClick={() => increment()}\n />\n <Button\n icon=\"chevron-down\"\n disabled={props.disabled}\n onClick={() => decrement()}\n />\n </ButtonGroup>\n )\n }\n\n function onReset() {\n if (props.onReset) {\n props.onReset();\n }\n }\n\n function maybeRenderReset() {\n if (props.onReset) {\n return (\n <Button\n icon=\"time\"\n onClick={() => onReset()}\n />\n )\n }\n }\n\n return (\n <ControlGroup className={NUMERIC_INPUT}>\n <InputGroup\n disabled={props.disabled}\n value={value}\n onChange={(e : any) => setValue(e.target.value)}\n onBlur={() => props.onValueChange(stringToSeconds(value))}\n placeholder=\"hh:mm:ss\"\n rightElement={maybeRenderReset()}\n />\n {renderButtons()}\n </ControlGroup>\n )\n};","/home/peroo/stash/ui/v2/src/components/Shared/FolderSelect/FolderSelect.tsx",["414","415"],"import {\n Button,\n Classes,\n Dialog,\n InputGroup,\n Spinner,\n FormGroup,\n} from \"@blueprintjs/core\";\nimport _ from \"lodash\";\nimport React, { FunctionComponent, useEffect, useState } from \"react\";\nimport { StashService } from \"../../../core/StashService\";\n\ninterface IProps {\n directories: string[];\n onDirectoriesChanged: (directories: string[]) => void;\n}\n\nexport const FolderSelect: FunctionComponent<IProps> = (props: IProps) => {\n const [currentDirectory, setCurrentDirectory] = useState<string>(\"\");\n const [isDisplayingDialog, setIsDisplayingDialog] = useState<boolean>(false);\n const [selectableDirectories, setSelectableDirectories] = useState<string[]>([]);\n const [selectedDirectories, setSelectedDirectories] = useState<string[]>([]);\n const { data, error, loading } = StashService.useDirectories(currentDirectory);\n\n useEffect(() => {\n setSelectedDirectories(props.directories);\n }, [props.directories]);\n\n useEffect(() => {\n if (!data || !data.directories || !!error) { return; }\n setSelectableDirectories(StashService.nullToUndefined(data.directories));\n }, [data]);\n\n function onSelectDirectory() {\n selectedDirectories.push(currentDirectory);\n setSelectedDirectories(selectedDirectories);\n setCurrentDirectory(\"\");\n setIsDisplayingDialog(false);\n props.onDirectoriesChanged(selectedDirectories);\n }\n\n function onRemoveDirectory(directory: string) {\n const newSelectedDirectories = selectedDirectories.filter((dir) => dir !== directory);\n setSelectedDirectories(newSelectedDirectories);\n props.onDirectoriesChanged(newSelectedDirectories);\n }\n\n function renderDialog() {\n return (\n <Dialog\n isOpen={isDisplayingDialog}\n onClose={() => setIsDisplayingDialog(false)}\n title=\"Select Directory\"\n >\n <div className=\"dialog-content\">\n <InputGroup\n large={true}\n placeholder=\"File path\"\n onChange={(e: any) => setCurrentDirectory(e.target.value)}\n value={currentDirectory}\n rightElement={(!data || !data.directories || loading) ? <Spinner size={Spinner.SIZE_SMALL} /> : undefined}\n />\n {selectableDirectories.map((path) => {\n return <div key={path} onClick={() => setCurrentDirectory(path)}>{path}</div>;\n })}\n </div>\n <div className={Classes.DIALOG_FOOTER}>\n <div className={Classes.DIALOG_FOOTER_ACTIONS}>\n <Button onClick={() => onSelectDirectory()}>Add</Button>\n </div>\n </div>\n </Dialog>\n );\n }\n\n return (\n <>\n {!!error ? <h1>{error.message}</h1> : undefined}\n {renderDialog()}\n <FormGroup>\n {selectedDirectories.map((path) => {\n return <div key={path}>{path} <a onClick={() => onRemoveDirectory(path)}>Remove</a></div>;\n })}\n </FormGroup>\n \n <Button small={true} onClick={() => setIsDisplayingDialog(true)}>Add Directory</Button>\n </>\n );\n};\n","/home/peroo/stash/ui/v2/src/components/Shared/TagLink.tsx",["416"],"import {\n ITagProps,\n Tag,\n} from \"@blueprintjs/core\";\nimport _ from \"lodash\";\nimport React, { FunctionComponent } from \"react\";\nimport { Link } from \"react-router-dom\";\nimport { PerformerDataFragment, SceneMarkerDataFragment, TagDataFragment } from \"../../core/generated-graphql\";\nimport { NavigationUtils } from \"../../utils/navigation\";\nimport { TextUtils } from \"../../utils/text\";\n\ninterface IProps extends ITagProps {\n tag?: Partial<TagDataFragment>;\n performer?: Partial<PerformerDataFragment>;\n marker?: Partial<SceneMarkerDataFragment>;\n}\n\nexport const TagLink: FunctionComponent<IProps> = (props: IProps) => {\n let link: string = \"#\";\n let title: string = \"\";\n if (!!props.tag) {\n link = NavigationUtils.makeTagScenesUrl(props.tag);\n title = props.tag.name || \"\";\n } else if (!!props.performer) {\n link = NavigationUtils.makePerformerScenesUrl(props.performer);\n title = props.performer.name || \"\";\n } else if (!!props.marker) {\n link = NavigationUtils.makeSceneMarkerUrl(props.marker);\n title = `${props.marker.title} - ${TextUtils.secondsToTimestamp(props.marker.seconds || 0)}`;\n }\n return (\n <Tag\n className=\"tag-item\"\n interactive={true}\n >\n <Link to={link}>{title}</Link>\n </Tag>\n );\n};\n","/home/peroo/stash/ui/v2/src/components/Stats.tsx",["417"],"import { H1, Spinner } from \"@blueprintjs/core\";\nimport React, { FunctionComponent } from \"react\";\nimport { StashService } from \"../core/StashService\";\n\nexport const Stats: FunctionComponent = () => {\n const { data, error, loading } = StashService.useStats();\n\n function renderStats() {\n if (!data || !data.stats) { return; }\n return (\n <nav id=\"details-container\" className=\"level\">\n <div className=\"level-item has-text-centered\">\n <div>\n <p className=\"heading\">Scenes</p>\n <p className=\"title\">{data.stats.scene_count}</p>\n </div>\n </div>\n <div className=\"level-item has-text-centered\">\n <div>\n <p className=\"heading\">Galleries</p>\n <p className=\"title\">{data.stats.gallery_count}</p>\n </div>\n </div>\n <div className=\"level-item has-text-centered\">\n <div>\n <p className=\"heading\">Performers</p>\n <p className=\"title\">{data.stats.performer_count}</p>\n </div>\n </div>\n <div className=\"level-item has-text-centered\">\n <div>\n <p className=\"heading\">Studios</p>\n <p className=\"title\">{data.stats.studio_count}</p>\n </div>\n </div>\n <div className=\"level-item has-text-centered\">\n <div>\n <p className=\"heading\">Tags</p>\n <p className=\"title\">{data.stats.tag_count}</p>\n </div>\n </div>\n </nav>\n );\n }\n\n return (\n <div id=\"details-container\">\n {!data || loading ? <Spinner size={Spinner.SIZE_LARGE} /> : undefined}\n {!!error ? <span>error.message</span> : undefined}\n {renderStats()}\n\n <h3>Notes</h3>\n <pre>\n {`\n This is still an early version, some things are still a work in progress.\n `}\n </pre>\n </div>\n );\n};\n","/home/peroo/stash/ui/v2/src/components/Studios/StudioCard.tsx",[],"/home/peroo/stash/ui/v2/src/components/Studios/StudioDetails/Studio.tsx",["418","419","420","421","422","423","424","425","426","427"],"import {\n Button,\n Classes,\n Dialog,\n EditableText,\n HTMLSelect,\n HTMLTable,\n Spinner,\n} from \"@blueprintjs/core\";\nimport _ from \"lodash\";\nimport React, { FunctionComponent, useEffect, useState } from \"react\";\nimport * as GQL from \"../../../core/generated-graphql\";\nimport { StashService } from \"../../../core/StashService\";\nimport { IBaseProps } from \"../../../models\";\nimport { ErrorUtils } from \"../../../utils/errors\";\nimport { TableUtils } from \"../../../utils/table\";\nimport { DetailsEditNavbar } from \"../../Shared/DetailsEditNavbar\";\nimport { ToastUtils } from \"../../../utils/toasts\";\n\ninterface IProps extends IBaseProps {}\n\nexport const Studio: FunctionComponent<IProps> = (props: IProps) => {\n const isNew = props.match.params.id === \"new\";\n\n // Editing state\n const [isEditing, setIsEditing] = useState<boolean>(isNew);\n\n // Editing studio state\n const [image, setImage] = useState<string | undefined>(undefined);\n const [name, setName] = useState<string | undefined>(undefined);\n const [url, setUrl] = useState<string | undefined>(undefined);\n\n // Studio state\n const [studio, setStudio] = useState<Partial<GQL.StudioDataFragment>>({});\n const [imagePreview, setImagePreview] = useState<string | undefined>(undefined);\n\n // Network state\n const [isLoading, setIsLoading] = useState(false);\n\n const { data, error, loading } = StashService.useFindStudio(props.match.params.id);\n const updateStudio = StashService.useStudioUpdate(getStudioInput() as GQL.StudioUpdateInput);\n const createStudio = StashService.useStudioCreate(getStudioInput() as GQL.StudioCreateInput);\n const deleteStudio = StashService.useStudioDestroy(getStudioInput() as GQL.StudioDestroyInput);\n\n function updateStudioEditState(state: Partial<GQL.StudioDataFragment>) {\n setName(state.name);\n setUrl(state.url);\n }\n\n useEffect(() => {\n setIsLoading(loading);\n if (!data || !data.findStudio || !!error) { return; }\n setStudio(data.findStudio);\n }, [data]);\n\n useEffect(() => {\n setImagePreview(studio.image_path);\n setImage(undefined);\n updateStudioEditState(studio);\n if (!isNew) {\n setIsEditing(false);\n }\n }, [studio]);\n\n function pasteImage(e : any) {\n if (e.clipboardData.files.length == 0) {\n return;\n }\n \n const file: File = e.clipboardData.files[0];\n const reader: FileReader = new FileReader();\n \n reader.onloadend = (e) => {\n setImagePreview(reader.result as string);\n setImage(reader.result as string);\n };\n reader.readAsDataURL(file);\n }\n\n useEffect(() => {\n window.addEventListener(\"paste\", pasteImage);\n \n return () => window.removeEventListener(\"paste\", pasteImage);\n });\n\n if (!isNew && !isEditing) {\n if (!data || !data.findStudio || isLoading) { return <Spinner size={Spinner.SIZE_LARGE} />; }\n if (!!error) { return <>error...</>; }\n }\n\n function getStudioInput() {\n const input: Partial<GQL.StudioCreateInput | GQL.StudioUpdateInput> = {\n name,\n url,\n image,\n };\n\n if (!isNew) {\n (input as GQL.StudioUpdateInput).id = props.match.params.id;\n }\n return input;\n }\n\n async function onSave() {\n setIsLoading(true);\n try {\n if (!isNew) {\n const result = await updateStudio();\n setStudio(result.data.studioUpdate);\n } else {\n const result = await createStudio();\n setStudio(result.data.studioCreate);\n props.history.push(`/studios/${result.data.studioCreate.id}`);\n }\n } catch (e) {\n ErrorUtils.handle(e);\n }\n setIsLoading(false);\n }\n\n async function onAutoTag() {\n if (!studio || !studio.id) {\n return;\n }\n try {\n await StashService.queryMetadataAutoTag({ studios: [studio.id]});\n ToastUtils.success(\"Started auto tagging\");\n } catch (e) {\n ErrorUtils.handle(e);\n }\n }\n\n async function onDelete() {\n setIsLoading(true);\n try {\n const result = await deleteStudio();\n } catch (e) {\n ErrorUtils.handle(e);\n }\n setIsLoading(false);\n \n // redirect to studios page\n props.history.push(`/studios`);\n }\n\n function onImageChange(event: React.FormEvent<HTMLInputElement>) {\n const file: File = (event.target as any).files[0];\n const reader: FileReader = new FileReader();\n\n reader.onloadend = (e) => {\n setImagePreview(reader.result as string);\n setImage(reader.result as string);\n };\n reader.readAsDataURL(file);\n }\n\n // TODO: CSS class\n return (\n <>\n <div className=\"columns is-multiline no-spacing\">\n <div className=\"column is-half details-image-container\">\n <img className=\"studio\" src={imagePreview} />\n </div>\n <div className=\"column is-half details-detail-container\">\n <DetailsEditNavbar\n studio={studio}\n isNew={isNew}\n isEditing={isEditing}\n onToggleEdit={() => { setIsEditing(!isEditing); updateStudioEditState(studio); }}\n onSave={onSave}\n onDelete={onDelete}\n onAutoTag={onAutoTag}\n onImageChange={onImageChange}\n />\n <h1 className=\"bp3-heading\">\n <EditableText\n disabled={!isEditing}\n value={name}\n placeholder=\"Name\"\n onChange={(value) => setName(value)}\n />\n </h1>\n\n <HTMLTable style={{width: \"100%\"}}>\n <tbody>\n {TableUtils.renderEditableTextTableRow({title: \"URL\", value: url, isEditing, onChange: setUrl})}\n </tbody>\n </HTMLTable>\n </div>\n </div>\n </>\n );\n};\n","/home/peroo/stash/ui/v2/src/components/Studios/StudioList.tsx",["428"],"import _ from \"lodash\";\nimport React, { FunctionComponent } from \"react\";\nimport { QueryHookResult } from \"react-apollo-hooks\";\nimport { FindStudiosQuery, FindStudiosVariables } from \"../../core/generated-graphql\";\nimport { ListHook } from \"../../hooks/ListHook\";\nimport { IBaseProps } from \"../../models/base-props\";\nimport { ListFilterModel } from \"../../models/list-filter/filter\";\nimport { DisplayMode, FilterMode } from \"../../models/list-filter/types\";\nimport { StudioCard } from \"./StudioCard\";\n\ninterface IProps extends IBaseProps {}\n\nexport const StudioList: FunctionComponent<IProps> = (props: IProps) => {\n const listData = ListHook.useList({\n filterMode: FilterMode.Studios,\n props,\n renderContent,\n });\n\n function renderContent(result: QueryHookResult<FindStudiosQuery, FindStudiosVariables>, filter: ListFilterModel) {\n if (!result.data || !result.data.findStudios) { return; }\n if (filter.displayMode === DisplayMode.Grid) {\n return (\n <div className=\"grid\">\n {result.data.findStudios.studios.map((studio) => (<StudioCard key={studio.id} studio={studio} />))}\n </div>\n );\n } else if (filter.displayMode === DisplayMode.List) {\n return <h1>TODO</h1>;\n } else if (filter.displayMode === DisplayMode.Wall) {\n return <h1>TODO</h1>;\n }\n }\n\n return listData.template;\n};\n","/home/peroo/stash/ui/v2/src/components/Studios/Studios.tsx",[],"/home/peroo/stash/ui/v2/src/components/Tags/TagList.tsx",["429","430","431","432","433","434","435","436","437","438","439","440"],"import { Alert, Button, Classes, Dialog, EditableText, FormGroup, HTMLTable, InputGroup, Spinner, Tag } from \"@blueprintjs/core\";\nimport _ from \"lodash\";\nimport React, { FunctionComponent, useEffect, useState } from \"react\";\nimport { QueryHookResult } from \"react-apollo-hooks\";\nimport { Link } from \"react-router-dom\";\nimport { FindGalleriesQuery, FindGalleriesVariables } from \"../../core/generated-graphql\";\nimport * as GQL from \"../../core/generated-graphql\";\nimport { StashService } from \"../../core/StashService\";\nimport { ListHook } from \"../../hooks/ListHook\";\nimport { IBaseProps } from \"../../models/base-props\";\nimport { ListFilterModel } from \"../../models/list-filter/filter\";\nimport { DisplayMode, FilterMode } from \"../../models/list-filter/types\";\nimport { ErrorUtils } from \"../../utils/errors\";\nimport { NavigationUtils } from \"../../utils/navigation\";\nimport { ToastUtils } from \"../../utils/toasts\";\n\ninterface IProps extends IBaseProps {}\n\nexport const TagList: FunctionComponent<IProps> = (props: IProps) => {\n const [tags, setTags] = useState<GQL.AllTagsAllTags[]>([]);\n const [isLoading, setIsLoading] = useState(false);\n\n // Editing / New state\n const [editingTag, setEditingTag] = useState<Partial<GQL.TagDataFragment> | undefined>(undefined);\n const [deletingTag, setDeletingTag] = useState<Partial<GQL.TagDataFragment> | undefined>(undefined);\n const [name, setName] = useState<string>(\"\");\n\n const { data, error, loading } = StashService.useAllTags();\n const updateTag = StashService.useTagUpdate(getTagInput() as GQL.TagUpdateInput);\n const createTag = StashService.useTagCreate(getTagInput() as GQL.TagCreateInput);\n const deleteTag = StashService.useTagDestroy(getDeleteTagInput() as GQL.TagDestroyInput);\n\n const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);\n\n useEffect(() => {\n setIsLoading(loading);\n if (!data || !data.allTags || !!error) { return; }\n setTags(data.allTags);\n }, [data]);\n\n useEffect(() => {\n if (!!editingTag) {\n setName(editingTag.name || \"\");\n } else {\n setName(\"\");\n }\n }, [editingTag]);\n\n useEffect(() => {\n setIsDeleteAlertOpen(!!deletingTag);\n }, [deletingTag]);\n\n function getTagInput() {\n const tagInput: Partial<GQL.TagCreateInput | GQL.TagUpdateInput> = { name };\n if (!!editingTag) { (tagInput as Partial<GQL.TagUpdateInput>).id = editingTag.id; }\n return tagInput;\n }\n\n function getDeleteTagInput() {\n const tagInput: Partial<GQL.TagDestroyInput> = {};\n if (!!deletingTag) { tagInput.id = deletingTag.id; }\n return tagInput;\n }\n\n async function onEdit() {\n try {\n if (!!editingTag && !!editingTag.id) {\n await updateTag();\n ToastUtils.success(\"Updated tag\");\n } else {\n await createTag();\n ToastUtils.success(\"Created tag\");\n }\n setEditingTag(undefined);\n } catch (e) {\n ErrorUtils.handle(e);\n }\n }\n\n async function onAutoTag(tag : GQL.TagDataFragment) {\n if (!tag) {\n return;\n }\n try {\n await StashService.queryMetadataAutoTag({ tags: [tag.id]});\n ToastUtils.success(\"Started auto tagging\");\n } catch (e) {\n ErrorUtils.handle(e);\n }\n }\n\n async function onDelete() {\n try {\n await deleteTag();\n ToastUtils.success(\"Deleted tag\");\n setDeletingTag(undefined);\n } catch (e) {\n ErrorUtils.handle(e);\n }\n }\n\n function renderDeleteAlert() {\n return (\n <Alert\n cancelButtonText=\"Cancel\"\n confirmButtonText=\"Delete\"\n icon=\"trash\"\n intent=\"danger\"\n isOpen={isDeleteAlertOpen}\n onCancel={() => setDeletingTag(undefined)}\n onConfirm={() => onDelete()}\n >\n <p>\n Are you sure you want to delete {deletingTag && deletingTag.name}?\n </p>\n </Alert>\n );\n }\n\n if (!data || !data.allTags || isLoading) { return <Spinner size={Spinner.SIZE_LARGE} />; }\n if (!!error) { return <>{error.message}</>; }\n\n const tagElements = tags.map((tag) => {\n return (\n <>\n {renderDeleteAlert()}\n <div key={tag.id} className=\"tag-list-row\">\n <span onClick={() => setEditingTag(tag)}>{tag.name}</span>\n <div style={{float: \"right\"}}>\n <Button text=\"Auto Tag\" onClick={() => onAutoTag(tag)}></Button>\n <Link className=\"bp3-button\" to={NavigationUtils.makeTagScenesUrl(tag)}>Scenes: {tag.scene_count}</Link>\n <Link className=\"bp3-button\" to={NavigationUtils.makeTagSceneMarkersUrl(tag)}>\n Markers: {tag.scene_marker_count}\n </Link>\n <span>Total: {(tag.scene_count || 0) + (tag.scene_marker_count || 0)}</span>\n <Button intent=\"danger\" icon=\"trash\" onClick={() => setDeletingTag(tag)}></Button>\n </div>\n </div>\n </>\n );\n });\n return (\n <div id=\"tag-list-container\">\n <Button intent=\"primary\" style={{marginTop: \"20px\"}} onClick={() => setEditingTag({})}>New Tag</Button>\n <Dialog\n isOpen={!!editingTag}\n onClose={() => setEditingTag(undefined)}\n title={!!editingTag && !!editingTag.id ? \"Edit Tag\" : \"New Tag\"}\n >\n <div className=\"dialog-content\">\n <FormGroup label=\"Name\">\n <InputGroup\n onChange={(newValue: any) => setName(newValue.target.value)}\n value={name}\n />\n </FormGroup>\n </div>\n <div className={Classes.DIALOG_FOOTER}>\n <div className={Classes.DIALOG_FOOTER_ACTIONS}>\n <Button onClick={() => onEdit()}>{!!editingTag && !!editingTag.id ? \"Update\" : \"Create\"}</Button>\n </div>\n </div>\n </Dialog>\n\n {tagElements}\n </div>\n );\n};\n","/home/peroo/stash/ui/v2/src/components/Tags/Tags.tsx",[],"/home/peroo/stash/ui/v2/src/components/Wall/WallItem.tsx",["441"],"import _ from \"lodash\";\nimport React, { FunctionComponent, useRef, useState, useEffect } from \"react\";\nimport { Link } from \"react-router-dom\";\nimport * as GQL from \"../../core/generated-graphql\";\nimport { VideoHoverHook } from \"../../hooks/VideoHover\";\nimport { TextUtils } from \"../../utils/text\";\nimport { NavigationUtils } from \"../../utils/navigation\";\nimport { StashService } from \"../../core/StashService\";\n\ninterface IWallItemProps {\n scene?: GQL.SlimSceneDataFragment;\n sceneMarker?: GQL.SceneMarkerDataFragment;\n origin?: string;\n onOverlay: (show: boolean) => void;\n clickHandler?: (item: GQL.SlimSceneDataFragment | GQL.SceneMarkerDataFragment) => void;\n}\n\nexport const WallItem: FunctionComponent<IWallItemProps> = (props: IWallItemProps) => {\n const [videoPath, setVideoPath] = useState<string | undefined>(undefined);\n const [previewPath, setPreviewPath] = useState<string>(\"\");\n const [screenshotPath, setScreenshotPath] = useState<string>(\"\");\n const [title, setTitle] = useState<string>(\"\");\n const [tags, setTags] = useState<JSX.Element[]>([]);\n const config = StashService.useConfiguration();\n const videoHoverHook = VideoHoverHook.useVideoHover({resetOnMouseLeave: true});\n const showTextContainer = !!config.data && !!config.data.configuration ? config.data.configuration.interface.wallShowTitle : true;\n\n function onMouseEnter() {\n VideoHoverHook.onMouseEnter(videoHoverHook);\n if (!videoPath || videoPath === \"\") {\n if (!!props.sceneMarker) {\n setVideoPath(props.sceneMarker.stream || \"\");\n } else if (!!props.scene) {\n setVideoPath(props.scene.paths.preview || \"\");\n }\n }\n props.onOverlay(true);\n }\n const debouncedOnMouseEnter = useRef(_.debounce(onMouseEnter, 500));\n\n function onMouseLeave() {\n VideoHoverHook.onMouseLeave(videoHoverHook);\n setVideoPath(\"\");\n debouncedOnMouseEnter.current.cancel();\n props.onOverlay(false);\n }\n\n function onClick() {\n if (props.clickHandler === undefined) { return; }\n if (props.scene !== undefined) {\n props.clickHandler(props.scene);\n } else if (props.sceneMarker !== undefined) {\n props.clickHandler(props.sceneMarker);\n }\n }\n\n let linkSrc: string = \"#\";\n if (props.clickHandler === undefined) {\n if (props.scene !== undefined) {\n linkSrc = `/scenes/${props.scene.id}`;\n } else if (props.sceneMarker !== undefined) {\n linkSrc = NavigationUtils.makeSceneMarkerUrl(props.sceneMarker);\n }\n }\n\n function onTransitionEnd(event: React.TransitionEvent<HTMLDivElement>) {\n const target = (event.target as any);\n if (target.classList.contains(\"double-scale\")) {\n target.parentElement.style.zIndex = 10;\n } else {\n target.parentElement.style.zIndex = null;\n }\n }\n\n useEffect(() => {\n if (!!props.sceneMarker) {\n setPreviewPath(props.sceneMarker.preview);\n setTitle(`${props.sceneMarker!.title} - ${TextUtils.secondsToTimestamp(props.sceneMarker.seconds)}`);\n const thisTags = props.sceneMarker.tags.map((tag) => (<span key={tag.id}>{tag.name}</span>));\n thisTags.unshift(<span key={props.sceneMarker.primary_tag.id}>{props.sceneMarker.primary_tag.name}</span>);\n setTags(thisTags);\n } else if (!!props.scene) {\n setPreviewPath(props.scene.paths.webp || \"\");\n setScreenshotPath(props.scene.paths.screenshot || \"\");\n setTitle(props.scene.title || \"\");\n // tags = props.scene.tags.map((tag) => (<span key={tag.id}>{tag.name}</span>));\n }\n }, [props.sceneMarker, props.scene]);\n\n function previewNotFound() {\n if (previewPath !== screenshotPath) {\n setPreviewPath(screenshotPath);\n }\n }\n\n const className = [\"scene-wall-item-container\"];\n if (videoHoverHook.isHovering.current) { className.push(\"double-scale\"); }\n const style: React.CSSProperties = {};\n if (!!props.origin) { style.transformOrigin = props.origin; }\n return (\n <div className=\"wall grid-item\">\n <div\n className={className.join(\" \")}\n style={style}\n onTransitionEnd={onTransitionEnd}\n onMouseEnter={() => debouncedOnMouseEnter.current()}\n onMouseMove={() => debouncedOnMouseEnter.current()}\n onMouseLeave={onMouseLeave}\n >\n <Link onClick={() => onClick()} to={linkSrc}>\n <video\n src={videoPath}\n poster={screenshotPath}\n style={videoHoverHook.isHovering.current ? {} : {display: \"none\"}}\n autoPlay={true}\n loop={true}\n ref={videoHoverHook.videoEl}\n />\n <img src={previewPath || screenshotPath} onError={() => previewNotFound()} />\n {showTextContainer ?\n <div className=\"scene-wall-item-text-container\">\n <div style={{lineHeight: 1}}>\n {title}\n </div>\n {tags}\n </div> : undefined\n }\n </Link>\n </div>\n </div>\n );\n};\n","/home/peroo/stash/ui/v2/src/components/Wall/WallPanel.tsx",["442"],"import _ from \"lodash\";\nimport React, { FunctionComponent, useState } from \"react\";\nimport * as GQL from \"../../core/generated-graphql\";\nimport \"./Wall.scss\";\nimport { WallItem } from \"./WallItem\";\n\ninterface IWallPanelProps {\n scenes?: GQL.SlimSceneDataFragment[];\n sceneMarkers?: GQL.SceneMarkerDataFragment[];\n clickHandler?: (item: GQL.SlimSceneDataFragment | GQL.SceneMarkerDataFragment) => void;\n}\n\nexport const WallPanel: FunctionComponent<IWallPanelProps> = (props: IWallPanelProps) => {\n const [showOverlay, setShowOverlay] = useState<boolean>(false);\n\n function onOverlay(show: boolean) {\n setShowOverlay(show);\n }\n\n function getOrigin(index: number, rowSize: number, total: number): string {\n const isAtStart = index % rowSize === 0;\n const isAtEnd = index % rowSize === rowSize - 1;\n const endRemaining = total % rowSize;\n\n // First row\n if (total === 1) { return \"top\"; }\n if (index === 0) { return \"top left\"; }\n if (index === rowSize - 1 || (total < rowSize && index === total - 1)) { return \"top right\"; }\n if (index < rowSize) { return \"top\"; }\n\n // Bottom row\n if (isAtEnd && index === total - 1) { return \"bottom right\"; }\n if (isAtStart && index === total - rowSize) { return \"bottom left\"; }\n if (endRemaining !== 0 && index >= total - endRemaining) { return \"bottom\"; }\n if (endRemaining === 0 && index >= total - rowSize) { return \"bottom\"; }\n\n // Everything else\n if (isAtStart) { return \"center left\"; }\n if (isAtEnd) { return \"center right\"; }\n return \"center\";\n }\n\n function maybeRenderScenes() {\n if (props.scenes === undefined) { return; }\n return props.scenes.map((scene, index) => {\n const origin = getOrigin(index, 5, props.scenes!.length);\n return (\n <WallItem\n key={scene.id}\n scene={scene}\n onOverlay={onOverlay}\n clickHandler={props.clickHandler}\n origin={origin}\n />\n );\n });\n }\n\n function maybeRenderSceneMarkers() {\n if (props.sceneMarkers === undefined) { return; }\n return props.sceneMarkers.map((marker, index) => {\n const origin = getOrigin(index, 5, props.sceneMarkers!.length);\n return (\n <WallItem\n key={marker.id}\n sceneMarker={marker}\n onOverlay={onOverlay}\n clickHandler={props.clickHandler}\n origin={origin}\n />\n );\n });\n }\n\n function render() {\n const overlayClassName = showOverlay ? \"visible\" : \"hidden\";\n return (\n <>\n <div className={`wall-overlay ${overlayClassName}`} />\n <div className=\"wall grid\">\n {maybeRenderScenes()}\n {maybeRenderSceneMarkers()}\n </div>\n </>\n );\n }\n\n return render();\n};\n","/home/peroo/stash/ui/v2/src/components/list/AddFilter.tsx",[],"/home/peroo/stash/ui/v2/src/components/list/ListFilter.tsx",["443","444","445","446"],"import {\n AnchorButton,\n Button,\n ButtonGroup,\n ControlGroup,\n HTMLSelect,\n InputGroup,\n Menu,\n MenuItem,\n Popover,\n Tag,\n Tooltip,\n Slider,\n} from \"@blueprintjs/core\";\nimport { debounce } from \"lodash\";\nimport React, { FunctionComponent, SyntheticEvent, useEffect, useRef, useState } from \"react\";\nimport { Criterion } from \"../../models/list-filter/criteria/criterion\";\nimport { ListFilterModel } from \"../../models/list-filter/filter\";\nimport { DisplayMode } from \"../../models/list-filter/types\";\nimport { AddFilter } from \"./AddFilter\";\n\ninterface IListFilterProps {\n onChangePageSize: (pageSize: number) => void;\n onChangeQuery: (query: string) => void;\n onChangeSortDirection: (sortDirection: \"asc\" | \"desc\") => void;\n onChangeSortBy: (sortBy: string) => void;\n onChangeDisplayMode: (displayMode: DisplayMode) => void;\n onAddCriterion: (criterion: Criterion, oldId?: string) => void;\n onRemoveCriterion: (criterion: Criterion) => void;\n zoomIndex?: number;\n onChangeZoom?: (zoomIndex: number) => void;\n onSelectAll?: () => void;\n onSelectNone?: () => void;\n filter: ListFilterModel;\n}\n\nconst PAGE_SIZE_OPTIONS = [\"20\", \"40\", \"60\", \"120\"];\n\nexport const ListFilter: FunctionComponent<IListFilterProps> = (props: IListFilterProps) => {\n let searchCallback: any;\n\n const [editingCriterion, setEditingCriterion] = useState<Criterion | undefined>(undefined);\n\n useEffect(() => {\n searchCallback = debounce((event: any) => {\n props.onChangeQuery(event.target.value);\n }, 500);\n });\n\n function onChangePageSize(event: SyntheticEvent<HTMLSelectElement>) {\n const val = event!.currentTarget!.value;\n props.onChangePageSize(parseInt(val, 10));\n }\n\n function onChangeQuery(event: SyntheticEvent<HTMLInputElement>) {\n event.persist();\n searchCallback(event);\n }\n\n function onChangeSortDirection(_: any) {\n if (props.filter.sortDirection === \"asc\") {\n props.onChangeSortDirection(\"desc\");\n } else {\n props.onChangeSortDirection(\"asc\");\n }\n }\n\n function onChangeSortBy(event: React.MouseEvent<any>) {\n props.onChangeSortBy(event.currentTarget.text);\n }\n\n function onChangeDisplayMode(displayMode: DisplayMode) {\n props.onChangeDisplayMode(displayMode);\n }\n\n function onAddCriterion(criterion: Criterion, oldId?: string) {\n props.onAddCriterion(criterion, oldId);\n }\n\n function onCancelAddCriterion() {\n setEditingCriterion(undefined);\n }\n\n let removedCriterionId = \"\";\n function onRemoveCriterionTag(criterion?: Criterion) {\n if (!criterion) { return; }\n setEditingCriterion(undefined);\n removedCriterionId = criterion.getId();\n props.onRemoveCriterion(criterion);\n }\n function onClickCriterionTag(criterion?: Criterion) {\n if (!criterion || removedCriterionId !== \"\") { return; }\n setEditingCriterion(criterion);\n }\n\n function renderSortByOptions() {\n return props.filter.sortByOptions.map((option) => (\n <MenuItem onClick={onChangeSortBy} text={option} key={option} />\n ));\n }\n\n function renderDisplayModeOptions() {\n function getIcon(option: DisplayMode) {\n switch (option) {\n case DisplayMode.Grid: return \"grid-view\";\n case DisplayMode.List: return \"list\";\n case DisplayMode.Wall: return \"symbol-square\";\n }\n }\n function getLabel(option: DisplayMode) {\n switch (option) {\n case DisplayMode.Grid: return \"Grid\";\n case DisplayMode.List: return \"List\";\n case DisplayMode.Wall: return \"Wall\";\n }\n }\n return props.filter.displayModeOptions.map((option) => (\n <Tooltip content={getLabel(option)} hoverOpenDelay={200}>\n <Button\n key={option}\n active={props.filter.displayMode === option}\n onClick={() => onChangeDisplayMode(option)}\n icon={getIcon(option)}\n />\n </Tooltip>\n ));\n }\n\n function renderFilterTags() {\n return props.filter.criteria.map((criterion) => (\n <Tag\n key={criterion.getId()}\n className=\"tag-item\"\n itemID={criterion.getId()}\n interactive={true}\n onRemove={() => onRemoveCriterionTag(criterion)}\n onClick={() => onClickCriterionTag(criterion)}\n >\n {criterion.getLabel()}\n </Tag>\n ));\n }\n\n function onSelectAll() {\n if (props.onSelectAll) {\n props.onSelectAll();\n }\n }\n\n function onSelectNone() {\n if (props.onSelectNone) {\n props.onSelectNone();\n }\n }\n\n function renderSelectAll() {\n if (props.onSelectAll) {\n return <MenuItem onClick={() => onSelectAll()} text=\"Select All\" />;\n }\n }\n\n function renderSelectNone() {\n if (props.onSelectNone) {\n return <MenuItem onClick={() => onSelectNone()} text=\"Select None\" />;\n }\n }\n\n function renderMore() {\n let options = [];\n options.push(renderSelectAll());\n options.push(renderSelectNone());\n options = options.filter((o) => !!o);\n\n let menuItems = options as JSX.Element[];\n\n function renderMoreOptions() {\n return (\n <>\n {menuItems}\n </>\n )\n }\n\n if (menuItems.length > 0) {\n return (\n <Popover position=\"bottom\">\n <Button icon=\"more\"/>\n <Menu>{renderMoreOptions()}</Menu>\n </Popover>\n );\n }\n }\n\n function onChangeZoom(v : number) {\n if (props.onChangeZoom) {\n props.onChangeZoom(v);\n }\n } \n\n function maybeRenderZoom() {\n if (props.onChangeZoom) {\n return (\n <span className=\"zoom-slider\">\n <Slider \n min={0}\n value={props.zoomIndex}\n initialValue={props.zoomIndex}\n max={3}\n labelRenderer={false}\n onChange={(v) => onChangeZoom(v)}\n />\n </span>\n );\n }\n }\n\n function render() {\n return (\n <>\n <div className=\"filter-container\">\n <InputGroup\n large={true}\n placeholder=\"Search...\"\n defaultValue={props.filter.searchTerm}\n onChange={onChangeQuery}\n className=\"filter-item\"\n />\n <HTMLSelect\n large={true}\n style={{flexBasis: \"min-content\"}}\n options={PAGE_SIZE_OPTIONS}\n onChange={onChangePageSize}\n value={props.filter.itemsPerPage}\n className=\"filter-item\"\n />\n <ButtonGroup className=\"filter-item\">\n <Popover position=\"bottom\">\n <Button large={true}>{props.filter.sortBy}</Button>\n <Menu>{renderSortByOptions()}</Menu>\n </Popover>\n \n <Tooltip \n content={props.filter.sortDirection === \"asc\" ? \"Ascending\" : \"Descending\"}\n hoverOpenDelay={200}\n >\n <Button\n rightIcon={props.filter.sortDirection === \"asc\" ? \"caret-up\" : \"caret-down\"}\n onClick={onChangeSortDirection}\n />\n </Tooltip>\n \n </ButtonGroup>\n\n <AddFilter\n filter={props.filter}\n onAddCriterion={onAddCriterion}\n onCancel={onCancelAddCriterion}\n editingCriterion={editingCriterion}\n />\n\n <ButtonGroup className=\"filter-item\">\n {renderDisplayModeOptions()}\n </ButtonGroup>\n\n {maybeRenderZoom()}\n\n <ButtonGroup className=\"filter-item\">\n {renderMore()}\n </ButtonGroup>\n </div>\n <div style={{display: \"flex\", justifyContent: \"center\", margin: \"10px auto\"}}>\n {renderFilterTags()}\n </div>\n </>\n );\n }\n\n return render();\n};\n","/home/peroo/stash/ui/v2/src/components/list/Pagination.tsx",[],"/home/peroo/stash/ui/v2/src/components/performers/PerformerCard.tsx",[],"/home/peroo/stash/ui/v2/src/components/performers/PerformerDetails/Performer.tsx",["447","448"],"/home/peroo/stash/ui/v2/src/components/performers/PerformerList.tsx",[],"/home/peroo/stash/ui/v2/src/components/performers/PerformerListTable.tsx",[],"/home/peroo/stash/ui/v2/src/components/performers/performers.tsx",[],"/home/peroo/stash/ui/v2/src/components/scenes/SceneCard.tsx",[],"/home/peroo/stash/ui/v2/src/components/scenes/SceneDetails/Scene.tsx",["449","450"],"import {\n Card,\n Spinner,\n Tab,\n Tabs,\n} from \"@blueprintjs/core\";\nimport queryString from \"query-string\";\nimport React, { FunctionComponent, useEffect, useState } from \"react\";\nimport * as GQL from \"../../../core/generated-graphql\";\nimport { StashService } from \"../../../core/StashService\";\nimport { IBaseProps } from \"../../../models\";\nimport { GalleryViewer } from \"../../Galleries/GalleryViewer\";\nimport { ScenePlayer } from \"../ScenePlayer/ScenePlayer\";\nimport { SceneDetailPanel } from \"./SceneDetailPanel\";\nimport { SceneEditPanel } from \"./SceneEditPanel\";\nimport { SceneFileInfoPanel } from \"./SceneFileInfoPanel\";\nimport { SceneMarkersPanel } from \"./SceneMarkersPanel\";\nimport { ScenePerformerPanel } from \"./ScenePerformerPanel\";\n\ninterface ISceneProps extends IBaseProps {}\n\nexport const Scene: FunctionComponent<ISceneProps> = (props: ISceneProps) => {\n const [timestamp, setTimestamp] = useState<number>(0);\n const [scene, setScene] = useState<Partial<GQL.SceneDataFragment>>({});\n const [isLoading, setIsLoading] = useState(false);\n const { data, error, loading } = StashService.useFindScene(props.match.params.id);\n\n useEffect(() => {\n setIsLoading(loading);\n if (!data || !data.findScene || !!error) { return; }\n setScene(StashService.nullToUndefined(data.findScene));\n }, [data]);\n\n useEffect(() => {\n const queryParams = queryString.parse(props.location.search);\n if (!!queryParams.t && typeof queryParams.t === \"string\" && timestamp === 0) {\n const newTimestamp = parseInt(queryParams.t, 10);\n setTimestamp(newTimestamp);\n }\n });\n\n function onClickMarker(marker: GQL.SceneMarkerDataFragment) {\n setTimestamp(marker.seconds);\n }\n\n if (!data || !data.findScene || isLoading || Object.keys(scene).length === 0) {\n return <Spinner size={Spinner.SIZE_LARGE} />;\n }\n const modifiedScene =\n Object.assign({scene_marker_tags: data.sceneMarkerTags}, scene) as GQL.SceneDataFragment; // TODO Hack from angular\n if (!!error) { return <>error...</>; }\n\n return (\n <>\n <ScenePlayer scene={modifiedScene} timestamp={timestamp} />\n <Card id=\"details-container\">\n <Tabs\n renderActiveTabPanelOnly={true}\n large={true}\n >\n <Tab id=\"scene-details-panel\" title=\"Details\" panel={<SceneDetailPanel scene={modifiedScene} />} />\n <Tab\n id=\"scene-markers-panel\"\n title=\"Markers\"\n panel={<SceneMarkersPanel scene={modifiedScene} onClickMarker={onClickMarker} />}\n />\n {modifiedScene.performers.length > 0 ?\n <Tab\n id=\"scene-performer-panel\"\n title=\"Performers\"\n panel={<ScenePerformerPanel scene={modifiedScene} />}\n /> : undefined\n }\n {!!modifiedScene.gallery ?\n <Tab\n id=\"scene-gallery-panel\"\n title=\"Gallery\"\n panel={<GalleryViewer gallery={modifiedScene.gallery} />}\n /> : undefined\n }\n <Tab id=\"scene-file-info-panel\" title=\"File Info\" panel={<SceneFileInfoPanel scene={modifiedScene} />} />\n <Tab\n id=\"scene-edit-panel\"\n title=\"Edit\"\n panel={\n <SceneEditPanel \n scene={modifiedScene} \n onUpdate={(newScene) => setScene(newScene)} \n onDelete={() => props.history.push(\"/scenes\")}\n />}\n />\n </Tabs>\n </Card>\n </>\n );\n};\n","/home/peroo/stash/ui/v2/src/components/scenes/SceneDetails/SceneDetailPanel.tsx",[],"/home/peroo/stash/ui/v2/src/components/scenes/SceneDetails/SceneEditPanel.tsx",[],"/home/peroo/stash/ui/v2/src/components/scenes/SceneDetails/SceneFileInfoPanel.tsx",[],"/home/peroo/stash/ui/v2/src/components/scenes/SceneDetails/SceneMarkersPanel.tsx",[],"/home/peroo/stash/ui/v2/src/components/scenes/SceneDetails/ScenePerformerPanel.tsx",[],"/home/peroo/stash/ui/v2/src/components/scenes/SceneFilenameParser.tsx",["451","452"],"import {\n Card,\n FormGroup,\n InputGroup,\n Button,\n H4,\n Spinner,\n HTMLTable,\n Checkbox,\n H5,\n MenuItem,\n HTMLSelect,\n TagInput,\n Tree,\n ITreeNode,\n} from \"@blueprintjs/core\";\nimport React, { FunctionComponent, useEffect, useState } from \"react\";\nimport { StashService } from \"../../core/StashService\";\nimport * as GQL from \"../../core/generated-graphql\";\nimport { SlimSceneDataFragment, Maybe } from \"../../core/generated-graphql\";\nimport { TextUtils } from \"../../utils/text\";\nimport _ from \"lodash\";\nimport { ToastUtils } from \"../../utils/toasts\";\nimport { ErrorUtils } from \"../../utils/errors\";\nimport { Pagination } from \"../list/Pagination\";\nimport { Select, ItemRenderer, ItemPredicate } from \"@blueprintjs/select\";\nimport { FilterMultiSelect } from \"../select/FilterMultiSelect\";\nimport { FilterSelect } from \"../select/FilterSelect\";\n \nclass ParserResult<T> {\n public value: Maybe<T>;\n public originalValue: Maybe<T>;\n public set: boolean = false;\n\n public setOriginalValue(v : Maybe<T>) {\n this.originalValue = v;\n this.value = v;\n }\n\n public setValue(v : Maybe<T>) {\n if (!!v) {\n this.value = v;\n this.set = !_.isEqual(this.value, this.originalValue);\n }\n }\n}\n\nclass ParserField {\n public field : string;\n public helperText? : string;\n\n constructor(field: string, helperText?: string) {\n this.field = field;\n this.helperText = helperText;\n }\n\n public getFieldPattern() {\n return \"{\" + this.field + \"}\";\n }\n\n static Title = new ParserField(\"title\");\n static Ext = new ParserField(\"ext\", \"File extension\");\n\n static I = new ParserField(\"i\", \"Matches any ignored word\");\n static D = new ParserField(\"d\", \"Matches any delimiter (.-_)\");\n\n static Performer = new ParserField(\"performer\");\n static Studio = new ParserField(\"studio\");\n static Tag = new ParserField(\"tag\");\n\n // date fields\n static Date = new ParserField(\"date\", \"YYYY-MM-DD\");\n static YYYY = new ParserField(\"yyyy\", \"Year\");\n static YY = new ParserField(\"yy\", \"Year (20YY)\");\n static MM = new ParserField(\"mm\", \"Two digit month\");\n static DD = new ParserField(\"dd\", \"Two digit date\");\n static YYYYMMDD = new ParserField(\"yyyymmdd\");\n static YYMMDD = new ParserField(\"yymmdd\");\n static DDMMYYYY = new ParserField(\"ddmmyyyy\");\n static DDMMYY = new ParserField(\"ddmmyy\");\n static MMDDYYYY = new ParserField(\"mmddyyyy\");\n static MMDDYY = new ParserField(\"mmddyy\");\n\n static validFields = [\n ParserField.Title,\n ParserField.Ext,\n ParserField.D,\n ParserField.I,\n ParserField.Performer,\n ParserField.Studio,\n ParserField.Tag,\n ParserField.Date,\n ParserField.YYYY,\n ParserField.YY,\n ParserField.MM,\n ParserField.DD,\n ParserField.YYYYMMDD,\n ParserField.YYMMDD,\n ParserField.DDMMYYYY,\n ParserField.DDMMYY,\n ParserField.MMDDYYYY,\n ParserField.MMDDYY\n ]\n\n static fullDateFields = [\n ParserField.YYYYMMDD,\n ParserField.YYMMDD,\n ParserField.DDMMYYYY,\n ParserField.DDMMYY,\n ParserField.MMDDYYYY,\n ParserField.MMDDYY\n ];\n}\nclass SceneParserResult {\n public id: string;\n public filename: string;\n public title: ParserResult<string> = new ParserResult();\n public date: ParserResult<string> = new ParserResult();\n\n public studio: ParserResult<GQL.SlimSceneDataStudio> = new ParserResult();\n public studioId: ParserResult<string> = new ParserResult();\n public tags: ParserResult<GQL.SlimSceneDataTags[]> = new ParserResult();\n public tagIds: ParserResult<string[]> = new ParserResult();\n public performers: ParserResult<GQL.SlimSceneDataPerformers[]> = new ParserResult();\n public performerIds: ParserResult<string[]> = new ParserResult();\n\n public scene : SlimSceneDataFragment;\n\n constructor(result : GQL.ParseSceneFilenamesResults) {\n this.scene = result.scene;\n\n this.id = this.scene.id;\n this.filename = TextUtils.fileNameFromPath(this.scene.path);\n this.title.setOriginalValue(this.scene.title);\n this.date.setOriginalValue(this.scene.date);\n this.performerIds.setOriginalValue(this.scene.performers.map((p) => p.id));\n this.performers.setOriginalValue(this.scene.performers);\n this.tagIds.setOriginalValue(this.scene.tags.map((t) => t.id));\n this.tags.setOriginalValue(this.scene.tags);\n this.studioId.setOriginalValue(this.scene.studio ? this.scene.studio.id : undefined);\n this.studio.setOriginalValue(this.scene.studio);\n\n this.title.setValue(result.title);\n this.date.setValue(result.date);\n this.performerIds.setValue(result.performer_ids);\n this.tagIds.setValue(result.tag_ids);\n this.studioId.setValue(result.studio_id);\n\n if (result.performer_ids) {\n this.performers.setValue(result.performer_ids.map((p) => {\n return {\n id: p,\n name: \"\",\n favorite: false,\n image_path: \"\"\n };\n }));\n }\n\n if (result.tag_ids) {\n this.tags.setValue(result.tag_ids.map((t) => {\n return {\n id: t,\n name: \"\",\n };\n }));\n }\n\n if (result.studio_id) {\n this.studio.setValue({\n id: result.studio_id,\n name: \"\",\n image_path: \"\"\n });\n }\n }\n\n private static setInput(object: any, key: string, parserResult : ParserResult<any>) {\n if (parserResult.set) {\n object[key] = parserResult.value;\n }\n }\n\n // returns true if any of its fields have set == true\n public isChanged() {\n return this.title.set || this.date.set || this.performerIds.set || this.studioId.set || this.tagIds.set;\n }\n\n public toSceneUpdateInput() {\n var ret = {\n id: this.id,\n title: this.scene.title,\n details: this.scene.details,\n url: this.scene.url,\n date: this.scene.date,\n rating: this.scene.rating,\n gallery_id: this.scene.gallery ? this.scene.gallery.id : undefined,\n studio_id: this.scene.studio ? this.scene.studio.id : undefined,\n performer_ids: this.scene.performers.map((performer) => performer.id),\n tag_ids: this.scene.tags.map((tag) => tag.id)\n };\n\n SceneParserResult.setInput(ret, \"title\", this.title);\n SceneParserResult.setInput(ret, \"date\", this.date);\n SceneParserResult.setInput(ret, \"performer_ids\", this.performerIds);\n SceneParserResult.setInput(ret, \"studio_id\", this.studioId);\n SceneParserResult.setInput(ret, \"tag_ids\", this.tagIds);\n\n return ret;\n }\n};\n\ninterface IParserInput {\n pattern: string,\n ignoreWords: string[],\n whitespaceCharacters: string,\n capitalizeTitle: boolean,\n page: number,\n pageSize: number,\n findClicked: boolean\n}\n\ninterface IParserRecipe {\n pattern: string,\n ignoreWords: string[],\n whitespaceCharacters: string,\n capitalizeTitle: boolean,\n description: string\n}\n\nconst builtInRecipes = [\n {\n pattern: \"{title}\",\n ignoreWords: [],\n whitespaceCharacters: \"\",\n capitalizeTitle: false,\n description: \"Filename\"\n },\n {\n pattern: \"{title}.{ext}\",\n ignoreWords: [],\n whitespaceCharacters: \"\",\n capitalizeTitle: false,\n description: \"Without extension\"\n },\n {\n pattern: \"{}.{yy}.{mm}.{dd}.{title}.XXX.{}.{ext}\",\n ignoreWords: [],\n whitespaceCharacters: \".\",\n capitalizeTitle: true,\n description: \"\"\n },\n {\n pattern: \"{}.{yy}.{mm}.{dd}.{title}.{ext}\",\n ignoreWords: [],\n whitespaceCharacters: \".\",\n capitalizeTitle: true,\n description: \"\"\n },\n {\n pattern: \"{title}.XXX.{}.{ext}\",\n ignoreWords: [],\n whitespaceCharacters: \".\",\n capitalizeTitle: true,\n description: \"\"\n },\n {\n pattern: \"{}.{yy}.{mm}.{dd}.{title}.{i}.{ext}\",\n ignoreWords: [\"cz\", \"fr\"],\n whitespaceCharacters: \".\",\n capitalizeTitle: true,\n description: \"Foreign language\"\n }\n];\n\nexport const SceneFilenameParser: FunctionComponent = () => {\n const [parserResult, setParserResult] = useState<SceneParserResult[]>([]);\n const [parserInput, setParserInput] = useState<IParserInput>(initialParserInput());\n\n const [allTitleSet, setAllTitleSet] = useState<boolean>(false);\n const [allDateSet, setAllDateSet] = useState<boolean>(false);\n const [allPerformerSet, setAllPerformerSet] = useState<boolean>(false);\n const [allTagSet, setAllTagSet] = useState<boolean>(false);\n const [allStudioSet, setAllStudioSet] = useState<boolean>(false);\n\n const [showFields, setShowFields] = useState<Map<string, boolean>>(initialShowFieldsState());\n \n const [totalItems, setTotalItems] = useState<number>(0);\n\n // Network state\n const [isLoading, setIsLoading] = useState(false);\n\n const updateScenes = StashService.useScenesUpdate(getScenesUpdateData());\n\n function initialParserInput() {\n return {\n pattern: \"{title}.{ext}\",\n ignoreWords: [],\n whitespaceCharacters: \"._\",\n capitalizeTitle: true,\n page: 1,\n pageSize: 20,\n findClicked: false\n };\n }\n\n function initialShowFieldsState() {\n return new Map<string, boolean>([\n [\"Title\", true],\n [\"Date\", true],\n [\"Performers\", true],\n [\"Tags\", true],\n [\"Studio\", true]\n ]);\n }\n\n function getParserFilter() {\n return {\n q: parserInput.pattern,\n page: parserInput.page,\n per_page: parserInput.pageSize,\n sort: \"path\",\n direction: GQL.SortDirectionEnum.Asc,\n };\n }\n\n function getParserInput() {\n return {\n ignoreWords: parserInput.ignoreWords,\n whitespaceCharacters: parserInput.whitespaceCharacters,\n capitalizeTitle: parserInput.capitalizeTitle\n };\n }\n\n async function onFind() {\n setParserResult([]);\n\n setIsLoading(true);\n \n try {\n const response = await StashService.queryParseSceneFilenames(getParserFilter(), getParserInput());\n\n let result = response.data.parseSceneFilenames;\n if (!!result) {\n parseResults(result.results);\n setTotalItems(result.count);\n }\n } catch (err) {\n ErrorUtils.handle(err);\n }\n\n setIsLoading(false);\n }\n\n useEffect(() => {\n if(parserInput.findClicked) {\n onFind();\n }\n }, [parserInput]);\n\n function onPageSizeChanged(newSize : number) {\n var newInput = _.clone(parserInput);\n newInput.page = 1;\n newInput.pageSize = newSize;\n setParserInput(newInput);\n }\n\n function onPageChanged(newPage : number) {\n if (newPage !== parserInput.page) {\n var newInput = _.clone(parserInput);\n newInput.page = newPage;\n setParserInput(newInput);\n }\n }\n\n function onFindClicked(input : IParserInput) {\n input.page = 1;\n input.findClicked = true;\n setParserInput(input);\n setTotalItems(0);\n }\n\n function getScenesUpdateData() {\n return parserResult.filter((result) => result.isChanged()).map((result) => result.toSceneUpdateInput());\n }\n\n async function onApply() {\n setIsLoading(true);\n\n try {\n await updateScenes();\n ToastUtils.success(\"Updated scenes\");\n } catch (e) {\n ErrorUtils.handle(e);\n }\n\n setIsLoading(false);\n }\n\n function parseResults(results : GQL.ParseSceneFilenamesResults[]) {\n if (results) {\n var result = results.map((r) => {\n return new SceneParserResult(r);\n }).filter((r) => !!r) as SceneParserResult[];\n\n setParserResult(result);\n determineFieldsToHide();\n }\n }\n\n function determineFieldsToHide() {\n var pattern = parserInput.pattern;\n var titleSet = pattern.includes(\"{title}\");\n var dateSet = pattern.includes(\"{date}\") || \n pattern.includes(\"{dd}\") || // don't worry about other partial date fields since this should be implied\n ParserField.fullDateFields.some((f) => {\n return pattern.includes(\"{\" + f.field + \"}\");\n });\n var performerSet = pattern.includes(\"{performer}\");\n var tagSet = pattern.includes(\"{tag}\");\n var studioSet = pattern.includes(\"{studio}\");\n\n var showFieldsCopy = _.clone(showFields);\n showFieldsCopy.set(\"Title\", titleSet);\n showFieldsCopy.set(\"Date\", dateSet);\n showFieldsCopy.set(\"Performers\", performerSet);\n showFieldsCopy.set(\"Tags\", tagSet);\n showFieldsCopy.set(\"Studio\", studioSet);\n setShowFields(showFieldsCopy);\n }\n\n useEffect(() => {\n var newAllTitleSet = !parserResult.some((r) => {\n return !r.title.set;\n });\n var newAllDateSet = !parserResult.some((r) => {\n return !r.date.set;\n });\n var newAllPerformerSet = !parserResult.some((r) => {\n return !r.performerIds.set;\n });\n var newAllTagSet = !parserResult.some((r) => {\n return !r.tagIds.set;\n });\n var newAllStudioSet = !parserResult.some((r) => {\n return !r.studioId.set;\n });\n\n if (newAllTitleSet !== allTitleSet) {\n setAllTitleSet(newAllTitleSet);\n }\n if (newAllDateSet !== allDateSet) {\n setAllDateSet(newAllDateSet);\n }\n if (newAllPerformerSet !== allPerformerSet) {\n setAllTagSet(newAllPerformerSet);\n }\n if (newAllTagSet !== allTagSet) {\n setAllTagSet(newAllTagSet);\n }\n if (newAllStudioSet !== allStudioSet) {\n setAllStudioSet(newAllStudioSet);\n }\n }, [parserResult]);\n\n function onSelectAllTitleSet(selected : boolean) {\n var newResult = [...parserResult];\n\n newResult.forEach((r) => {\n r.title.set = selected;\n });\n\n setParserResult(newResult);\n setAllTitleSet(selected);\n }\n\n function onSelectAllDateSet(selected : boolean) {\n var newResult = [...parserResult];\n\n newResult.forEach((r) => {\n r.date.set = selected;\n });\n\n setParserResult(newResult);\n setAllDateSet(selected);\n }\n\n function onSelectAllPerformerSet(selected : boolean) {\n var newResult = [...parserResult];\n\n newResult.forEach((r) => {\n r.performerIds.set = selected;\n });\n\n setParserResult(newResult);\n setAllPerformerSet(selected);\n }\n\n function onSelectAllTagSet(selected : boolean) {\n var newResult = [...parserResult];\n\n newResult.forEach((r) => {\n r.tagIds.set = selected;\n });\n\n setParserResult(newResult);\n setAllTagSet(selected);\n }\n\n function onSelectAllStudioSet(selected : boolean) {\n var newResult = [...parserResult];\n\n newResult.forEach((r) => {\n r.studioId.set = selected;\n });\n\n setParserResult(newResult);\n setAllStudioSet(selected);\n }\n\n interface IShowFieldsTreeProps {\n showFields: Map<string, boolean>\n onShowFieldsChanged: (fields : Map<string, boolean>) => void\n }\n\n function ShowFieldsTree(props : IShowFieldsTreeProps) {\n const [displayFieldsExpanded, setDisplayFieldsExpanded] = useState<boolean>();\n\n const treeState: ITreeNode[] = [\n {\n id: 0,\n hasCaret: true,\n label: \"Display fields\",\n childNodes: [\n {\n id: 1,\n label: \"Title\",\n },\n {\n id: 2,\n label: \"Date\",\n },\n {\n id: 3,\n label: \"Performers\",\n },\n {\n id: 4,\n label: \"Tags\",\n },\n {\n id: 5,\n label: \"Studio\",\n }\n ]\n }\n ];\n\n function setNodeState() {\n if (!!treeState[0].childNodes) {\n treeState[0].childNodes.forEach((n) => {\n n.icon = props.showFields.get(n.label as string) ? \"tick\" : \"cross\";\n });\n }\n\n treeState[0].isExpanded = displayFieldsExpanded;\n }\n\n setNodeState();\n\n function expandNode() {\n setDisplayFieldsExpanded(true);\n }\n\n function collapseNode() {\n setDisplayFieldsExpanded(false);\n }\n\n function handleClick(nodeData: ITreeNode) {\n var field = nodeData.label as string;\n var fieldsCopy = _.clone(props.showFields);\n fieldsCopy.set(field, !fieldsCopy.get(field));\n props.onShowFieldsChanged(fieldsCopy);\n }\n\n return (\n <Tree\n contents={treeState}\n onNodeClick={handleClick}\n onNodeCollapse={collapseNode}\n onNodeExpand={expandNode}\n />\n );\n }\n\n interface IParserInputProps {\n input: IParserInput,\n onFind: (input : IParserInput) => void\n }\n\n function ParserInput(props : IParserInputProps) {\n const [pattern, setPattern] = useState<string>(props.input.pattern);\n const [ignoreWords, setIgnoreWords] = useState<string>(props.input.ignoreWords.join(\" \"));\n const [whitespaceCharacters, setWhitespaceCharacters] = useState<string>(props.input.whitespaceCharacters);\n const [capitalizeTitle, setCapitalizeTitle] = useState<boolean>(props.input.capitalizeTitle);\n\n function onFind() {\n props.onFind({\n pattern: pattern,\n ignoreWords: ignoreWords.split(\" \"),\n whitespaceCharacters: whitespaceCharacters,\n capitalizeTitle: capitalizeTitle,\n page: 1,\n pageSize: props.input.pageSize,\n findClicked: props.input.findClicked\n });\n }\n\n const ParserRecipeSelect = Select.ofType<IParserRecipe>();\n\n const renderParserRecipe: ItemRenderer<IParserRecipe> = (input, { handleClick, modifiers }) => {\n if (!modifiers.matchesPredicate) {\n return null;\n }\n return (\n <MenuItem\n key={input.pattern}\n onClick={handleClick}\n text={input.pattern || \"{}\"}\n label={input.description}\n />\n );\n };\n\n const parserRecipePredicate: ItemPredicate<IParserRecipe> = (query, item) => {\n return item.pattern.includes(query);\n };\n\n function setParserRecipe(recipe: IParserRecipe) {\n setPattern(recipe.pattern);\n setIgnoreWords(recipe.ignoreWords.join(\" \"));\n setWhitespaceCharacters(recipe.whitespaceCharacters);\n setCapitalizeTitle(recipe.capitalizeTitle);\n }\n \n const ParserFieldSelect = Select.ofType<ParserField>();\n\n const renderParserField: ItemRenderer<ParserField> = (field, { handleClick, modifiers }) => {\n if (!modifiers.matchesPredicate) {\n return null;\n }\n return (\n <MenuItem\n key={field.field}\n onClick={handleClick}\n text={field.field || \"{}\"}\n label={field.helperText}\n />\n );\n };\n\n const parserFieldPredicate: ItemPredicate<ParserField> = (query, item) => {\n return item.field.includes(query);\n };\n\n const validFields = [new ParserField(\"\", \"Wildcard\")].concat(ParserField.validFields);\n \n function addParserField(field: ParserField) {\n setPattern(pattern + field.getFieldPattern());\n }\n\n const parserFieldSelect = (\n <ParserFieldSelect\n items={validFields}\n onItemSelect={(item) => addParserField(item)}\n itemRenderer={renderParserField}\n itemPredicate={parserFieldPredicate}\n >\n <Button \n text=\"Add field\" \n rightIcon=\"caret-down\" \n />\n </ParserFieldSelect>\n );\n\n const PAGE_SIZE_OPTIONS = [\"20\", \"40\", \"60\", \"120\"];\n\n return (\n <>\n <FormGroup className=\"inputs\">\n <FormGroup \n label=\"Filename pattern:\" \n inline={true}\n helperText=\"Use '\\\\' to escape literal {} characters\"\n >\n <InputGroup\n onChange={(newValue: any) => setPattern(newValue.target.value)}\n value={pattern}\n rightElement={parserFieldSelect}\n />\n </FormGroup>\n\n <FormGroup>\n <FormGroup label=\"Ignored words:\" inline={true} helperText=\"Matches with {i}\">\n <InputGroup\n onChange={(newValue: any) => setIgnoreWords(newValue.target.value)}\n value={ignoreWords}\n />\n </FormGroup>\n </FormGroup>\n \n <FormGroup>\n <H5>Title</H5>\n <FormGroup label=\"Whitespace characters:\" \n inline={true}\n helperText=\"These characters will be replaced with whitespace in the title\">\n <InputGroup\n onChange={(newValue: any) => setWhitespaceCharacters(newValue.target.value)}\n value={whitespaceCharacters}\n />\n </FormGroup>\n <Checkbox\n label=\"Capitalize title\"\n checked={capitalizeTitle}\n onChange={() => setCapitalizeTitle(!capitalizeTitle)}\n inline={true}\n />\n </FormGroup>\n \n {/* TODO - mapping stuff will go here */}\n\n <FormGroup>\n <ParserRecipeSelect\n items={builtInRecipes}\n onItemSelect={(item) => setParserRecipe(item)}\n itemRenderer={renderParserRecipe}\n itemPredicate={parserRecipePredicate}\n >\n <Button \n text=\"Select Parser Recipe\" \n rightIcon=\"caret-down\" \n />\n </ParserRecipeSelect>\n </FormGroup>\n\n <FormGroup>\n <ShowFieldsTree\n key=\"showFields\"\n showFields={showFields}\n onShowFieldsChanged={(fields) => setShowFields(fields)}\n />\n </FormGroup>\n\n <FormGroup>\n <Button text=\"Find\" onClick={() => onFind()} />\n <HTMLSelect\n style={{flexBasis: \"min-content\"}}\n options={PAGE_SIZE_OPTIONS}\n onChange={(event) => onPageSizeChanged(parseInt(event.target.value))}\n value={props.input.pageSize}\n className=\"filter-item\"\n />\n </FormGroup>\n </FormGroup>\n </>\n );\n }\n\n interface ISceneParserFieldProps {\n parserResult : ParserResult<any>\n className? : string\n fieldName : string\n onSetChanged : (set : boolean) => void\n onValueChanged : (value : any) => void\n originalParserResult? : ParserResult<any>\n renderOriginalInputField: (props : ISceneParserFieldProps) => JSX.Element\n renderNewInputField: (props : ISceneParserFieldProps, onChange : (event : any) => void) => JSX.Element\n }\n\n function SceneParserField(props : ISceneParserFieldProps) {\n\n function maybeValueChanged(value : any) {\n if (value !== props.parserResult.value) {\n props.onValueChanged(value);\n }\n }\n\n if (!showFields.get(props.fieldName)) {\n return null;\n }\n\n return (\n <>\n <td>\n <Checkbox\n checked={props.parserResult.set}\n inline={true}\n onChange={() => {props.onSetChanged(!props.parserResult.set)}}\n />\n </td>\n <td>\n <FormGroup>\n {props.renderOriginalInputField(props)}\n {props.renderNewInputField(props, (value) => maybeValueChanged(value))}\n </FormGroup>\n </td>\n </>\n );\n }\n\n function renderOriginalInputGroup(props : ISceneParserFieldProps) {\n var parserResult = props.parserResult;\n\n if (!!props.originalParserResult) {\n parserResult = props.originalParserResult;\n }\n\n return (\n <InputGroup\n key=\"originalValue\"\n className={props.className}\n small={true}\n disabled={true}\n value={parserResult.originalValue || \"\"}\n />\n );\n }\n\n interface IInputGroupWrapperProps {\n parserResult: ParserResult<any>\n onChange : (event : any) => void\n className? : string\n }\n\n function InputGroupWrapper(props : IInputGroupWrapperProps) {\n const [value, setValue] = useState<string>(props.parserResult.value);\n\n useEffect(() => {\n setValue(props.parserResult.value);\n }, [props.parserResult.value]);\n\n return (\n <InputGroup\n key=\"newValue\"\n className={props.className}\n small={true}\n onChange={(event : any) => {setValue(event.target.value)}}\n onBlur={() => props.onChange(value)}\n disabled={!props.parserResult.set}\n value={value || \"\"}\n autoComplete={\"new-password\" /* required to prevent Chrome autofilling */}\n />\n );\n }\n \n function renderNewInputGroup(props : ISceneParserFieldProps, onChange : (value : any) => void) {\n return (\n <InputGroupWrapper\n className={props.className}\n onChange={(value : any) => {onChange(value)}}\n parserResult={props.parserResult}\n />\n );\n }\n\n interface HasName {\n name: string\n }\n\n function renderOriginalSelect(props : ISceneParserFieldProps) {\n var parserResult = props.parserResult;\n\n if (!!props.originalParserResult) {\n parserResult = props.originalParserResult;\n }\n\n var elements = [];\n \n if (parserResult.originalValue) {\n if (parserResult.originalValue.map) {\n elements = parserResult.originalValue.map((element : HasName) => (\n element.name\n ));\n } else {\n elements = [parserResult.originalValue.name];\n }\n }\n\n return (\n <>\n <TagInput\n className={props.className}\n values={elements}\n disabled={true}\n />\n </>\n )\n }\n\n function renderNewMultiSelect(type: \"performers\" | \"tags\", props : ISceneParserFieldProps, onChange : (value : any) => void) {\n return (\n <FilterMultiSelect\n className={props.className}\n type={type}\n onUpdate={(items) => {\n const ids = items.map((i) => i.id);\n onChange(ids);\n }}\n initialIds={props.parserResult.value}\n />\n );\n }\n\n function renderNewPerformerSelect(props : ISceneParserFieldProps, onChange : (value : any) => void) {\n return renderNewMultiSelect(\"performers\", props, onChange);\n }\n\n function renderNewTagSelect(props : ISceneParserFieldProps, onChange : (value : any) => void) {\n return renderNewMultiSelect(\"tags\", props, onChange);\n }\n\n function renderNewStudioSelect(props : ISceneParserFieldProps, onChange : (value : any) => void) {\n return (\n <FilterSelect\n type=\"studios\"\n noSelectionString=\"\"\n className={props.className}\n onSelectItem={(item) => onChange(item ? item.id : undefined)}\n initialId={props.parserResult.value}\n />\n );\n }\n\n interface ISceneParserRowProps {\n scene : SceneParserResult,\n onChange: (changedScene : SceneParserResult) => void\n }\n\n function SceneParserRow(props : ISceneParserRowProps) {\n\n function changeParser(result : ParserResult<any>, set : boolean, value : any) {\n var newParser = _.clone(result);\n newParser.set = set;\n newParser.value = value;\n return newParser;\n }\n\n function onTitleChanged(set : boolean, value: string | undefined) {\n var newResult = _.clone(props.scene);\n newResult.title = changeParser(newResult.title, set, value);\n props.onChange(newResult);\n }\n\n function onDateChanged(set : boolean, value: string | undefined) {\n var newResult = _.clone(props.scene);\n newResult.date = changeParser(newResult.date, set, value);\n props.onChange(newResult);\n }\n\n function onPerformerIdsChanged(set : boolean, value: string[] | undefined) {\n var newResult = _.clone(props.scene);\n newResult.performerIds = changeParser(newResult.performerIds, set, value);\n props.onChange(newResult);\n }\n\n function onTagIdsChanged(set : boolean, value: string[] | undefined) {\n var newResult = _.clone(props.scene);\n newResult.tagIds = changeParser(newResult.tagIds, set, value);\n props.onChange(newResult);\n }\n\n function onStudioIdChanged(set : boolean, value: string | undefined) {\n var newResult = _.clone(props.scene);\n newResult.studioId = changeParser(newResult.studioId, set, value);\n props.onChange(newResult);\n }\n\n return (\n <>\n <tr className=\"scene-parser-row\">\n <td style={{textAlign: \"left\"}}>\n {props.scene.filename}\n </td>\n <SceneParserField \n key=\"title\"\n fieldName=\"Title\"\n className=\"parser-field-title\" \n parserResult={props.scene.title}\n onSetChanged={(set) => onTitleChanged(set, props.scene.title.value)}\n onValueChanged={(value) => onTitleChanged(props.scene.title.set, value)}\n renderOriginalInputField={renderOriginalInputGroup}\n renderNewInputField={renderNewInputGroup}\n />\n <SceneParserField \n key=\"date\"\n fieldName=\"Date\"\n className=\"parser-field-date\"\n parserResult={props.scene.date}\n onSetChanged={(set) => onDateChanged(set, props.scene.date.value)}\n onValueChanged={(value) => onDateChanged(props.scene.date.set, value)}\n renderOriginalInputField={renderOriginalInputGroup}\n renderNewInputField={renderNewInputGroup}\n />\n <SceneParserField \n key=\"performers\"\n fieldName=\"Performers\"\n className=\"parser-field-performers\"\n parserResult={props.scene.performerIds}\n originalParserResult={props.scene.performers}\n onSetChanged={(set) => onPerformerIdsChanged(set, props.scene.performerIds.value)}\n onValueChanged={(value) => onPerformerIdsChanged(props.scene.performerIds.set, value)}\n renderOriginalInputField={renderOriginalSelect}\n renderNewInputField={renderNewPerformerSelect}\n />\n <SceneParserField \n key=\"tags\"\n fieldName=\"Tags\"\n className=\"parser-field-tags\"\n parserResult={props.scene.tagIds}\n originalParserResult={props.scene.tags}\n onSetChanged={(set) => onTagIdsChanged(set, props.scene.tagIds.value)}\n onValueChanged={(value) => onTagIdsChanged(props.scene.tagIds.set, value)}\n renderOriginalInputField={renderOriginalSelect}\n renderNewInputField={renderNewTagSelect}\n />\n <SceneParserField \n key=\"studio\"\n fieldName=\"Studio\"\n className=\"parser-field-studio\"\n parserResult={props.scene.studioId}\n originalParserResult={props.scene.studio}\n onSetChanged={(set) => onStudioIdChanged(set, props.scene.studioId.value)}\n onValueChanged={(value) => onStudioIdChanged(props.scene.studioId.set, value)}\n renderOriginalInputField={renderOriginalSelect}\n renderNewInputField={renderNewStudioSelect}\n />\n </tr>\n </>\n )\n }\n\n function onChange(scene : SceneParserResult, changedScene : SceneParserResult) {\n var newResult = [...parserResult];\n\n var index = newResult.indexOf(scene);\n newResult[index] = changedScene;\n\n setParserResult(newResult);\n }\n\n function renderHeader(fieldName: string, allSet: boolean, onAllSet: (set: boolean) => void) {\n if (!showFields.get(fieldName)) {\n return null;\n }\n\n return (\n <>\n <td>\n <Checkbox\n checked={allSet}\n inline={true}\n onChange={() => {onAllSet(!allSet)}}\n />\n </td>\n <th>{fieldName}</th>\n </>\n )\n }\n\n function renderTable() {\n if (parserResult.length === 0) { return undefined; }\n\n return (\n <>\n <div>\n <div className=\"scene-parser-results\">\n <HTMLTable condensed={true}>\n <thead>\n <tr className=\"scene-parser-row\">\n <th>Filename</th>\n {renderHeader(\"Title\", allTitleSet, onSelectAllTitleSet)}\n {renderHeader(\"Date\", allDateSet, onSelectAllDateSet)}\n {renderHeader(\"Performers\", allPerformerSet, onSelectAllPerformerSet)}\n {renderHeader(\"Tags\", allTagSet, onSelectAllTagSet)}\n {renderHeader(\"Studio\", allStudioSet, onSelectAllStudioSet)}\n </tr>\n </thead>\n <tbody>\n {parserResult.map((scene) => \n <SceneParserRow \n scene={scene} \n key={scene.id}\n onChange={(changedScene) => onChange(scene, changedScene)}/>\n )}\n </tbody>\n </HTMLTable>\n </div>\n <Pagination\n currentPage={parserInput.page}\n itemsPerPage={parserInput.pageSize}\n totalItems={totalItems}\n onChangePage={(page) => onPageChanged(page)}\n />\n <Button intent=\"primary\" text=\"Apply\" onClick={() => onApply()}></Button>\n </div>\n </>\n )\n }\n\n return (\n <Card id=\"parser-container\">\n <H4>Scene Filename Parser</H4>\n <ParserInput\n input={parserInput}\n onFind={(input) => onFindClicked(input)}\n />\n\n {isLoading ? <Spinner size={Spinner.SIZE_LARGE} /> : undefined}\n {renderTable()}\n </Card>\n );\n};\n \n","/home/peroo/stash/ui/v2/src/components/scenes/SceneList.tsx",[],"/home/peroo/stash/ui/v2/src/components/scenes/SceneListTable.tsx",[],"/home/peroo/stash/ui/v2/src/components/scenes/SceneMarkerList.tsx",[],"/home/peroo/stash/ui/v2/src/components/scenes/ScenePlayer/ScenePlayer.tsx",[],"/home/peroo/stash/ui/v2/src/components/scenes/ScenePlayer/ScenePlayerScrubber.tsx",["453","454","455","456","457","458"],"import axios from \"axios\";\nimport React, { CSSProperties, FunctionComponent, RefObject, useEffect, useRef, useState } from \"react\";\nimport * as GQL from \"../../../core/generated-graphql\";\nimport { TextUtils } from \"../../../utils/text\";\nimport \"./ScenePlayerScrubber.scss\";\n\ninterface IScenePlayerScrubberProps {\n scene: GQL.SceneDataFragment;\n position: number;\n onSeek: (seconds: number) => void;\n onScrolled: () => void;\n}\n\ninterface ISceneSpriteItem {\n start: number;\n end: number;\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\nexport const ScenePlayerScrubber: FunctionComponent<IScenePlayerScrubberProps> = (props: IScenePlayerScrubberProps) => {\n const contentEl = useRef<HTMLDivElement>(null);\n const positionIndicatorEl = useRef<HTMLDivElement>(null);\n const scrubberSliderEl = useRef<HTMLDivElement>(null);\n const mouseDown = useRef(false);\n const lastMouseEvent = useRef<any>(null);\n const startMouseEvent = useRef<any>(null);\n const velocity = useRef(0);\n\n const _position = useRef(0);\n function getPostion() { return _position.current; }\n function setPosition(newPostion: number, shouldEmit: boolean = true) {\n if (!scrubberSliderEl.current || !positionIndicatorEl.current) { return; }\n if (shouldEmit) { props.onScrolled(); }\n\n const midpointOffset = scrubberSliderEl.current.clientWidth / 2;\n\n const bounds = getBounds() * -1;\n if (newPostion > midpointOffset) {\n _position.current = midpointOffset;\n } else if (newPostion < bounds - midpointOffset) {\n _position.current = bounds - midpointOffset;\n } else {\n _position.current = newPostion;\n }\n\n scrubberSliderEl.current.style.transform = `translateX(${_position.current}px)`;\n\n const indicatorPosition = (\n (newPostion - midpointOffset) / (bounds - (midpointOffset * 2)) * scrubberSliderEl.current.clientWidth\n );\n positionIndicatorEl.current.style.transform = `translateX(${indicatorPosition}px)`;\n }\n\n const [spriteItems, setSpriteItems] = useState<ISceneSpriteItem[]>([]);\n const [delayedRender, setDelayedRender] = useState(false);\n\n useEffect(() => {\n if (!scrubberSliderEl.current) { return; }\n scrubberSliderEl.current.style.transform = `translateX(${scrubberSliderEl.current.clientWidth / 2}px)`;\n }, [scrubberSliderEl]);\n\n useEffect(() => {\n fetchSpriteInfo();\n }, [props.scene]);\n\n useEffect(() => {\n if (!scrubberSliderEl.current) { return; }\n const duration = Number(props.scene.file.duration);\n const percentage = props.position / duration;\n const position = (\n (scrubberSliderEl.current.scrollWidth * percentage) - (scrubberSliderEl.current.clientWidth / 2)\n ) * -1;\n setPosition(position, false);\n }, [props.position]);\n\n useEffect(() => {\n window.addEventListener(\"mouseup\", onMouseUp, false);\n return () => {\n window.removeEventListener(\"mouseup\", onMouseUp);\n };\n });\n\n useEffect(() => {\n if (!contentEl.current) { return; }\n contentEl.current.addEventListener(\"mousedown\", onMouseDown, false);\n return () => {\n if (!contentEl.current) { return; }\n contentEl.current.removeEventListener(\"mousedown\", onMouseDown);\n };\n });\n\n useEffect(() => {\n if (!contentEl.current) { return; }\n contentEl.current.addEventListener(\"mousemove\", onMouseMove, false);\n return () => {\n if (!contentEl.current) { return; }\n contentEl.current.removeEventListener(\"mousemove\", onMouseMove);\n };\n });\n\n function onMouseUp(this: Window, event: MouseEvent) {\n if (!startMouseEvent.current || !scrubberSliderEl.current) { return; }\n mouseDown.current = false;\n const delta = Math.abs(event.clientX - startMouseEvent.current.clientX);\n if (delta < 1 && event.target instanceof HTMLDivElement) {\n const target: HTMLDivElement = event.target;\n let seekSeconds: number | undefined;\n\n const spriteIdString = target.getAttribute(\"data-sprite-item-id\");\n if (spriteIdString != null) {\n const spritePercentage = event.offsetX / target.clientWidth;\n const offset = target.offsetLeft + (target.clientWidth * spritePercentage);\n const percentage = offset / scrubberSliderEl.current.scrollWidth;\n seekSeconds = percentage * (props.scene.file.duration || 0);\n }\n\n const markerIdString = target.getAttribute(\"data-marker-id\");\n if (markerIdString != null) {\n const marker = props.scene.scene_markers[Number(markerIdString)];\n seekSeconds = marker.seconds;\n }\n\n if (!!seekSeconds) { props.onSeek(seekSeconds); }\n } else if (Math.abs(velocity.current) > 25) {\n const newPosition = getPostion() + (velocity.current * 10);\n setPosition(newPosition);\n velocity.current = 0;\n }\n }\n\n function onMouseDown(this: HTMLDivElement, event: MouseEvent) {\n event.preventDefault();\n mouseDown.current = true;\n lastMouseEvent.current = event;\n startMouseEvent.current = event;\n velocity.current = 0;\n }\n\n function onMouseMove(this: HTMLDivElement, event: MouseEvent) {\n if (!mouseDown.current) { return; }\n\n // negative dragging right (past), positive left (future)\n const delta = event.clientX - lastMouseEvent.current.clientX;\n\n const movement = event.movementX;\n velocity.current = movement;\n\n const newPostion = getPostion() + delta;\n setPosition(newPostion);\n lastMouseEvent.current = event;\n }\n\n function getBounds(): number {\n if (!scrubberSliderEl.current || !positionIndicatorEl.current) { return 0; }\n return scrubberSliderEl.current.scrollWidth - scrubberSliderEl.current.clientWidth;\n }\n\n function goBack() {\n if (!scrubberSliderEl.current) { return; }\n const newPosition = getPostion() + scrubberSliderEl.current.clientWidth;\n setPosition(newPosition);\n }\n\n function goForward() {\n if (!scrubberSliderEl.current) { return; }\n const newPosition = getPostion() - scrubberSliderEl.current.clientWidth;\n setPosition(newPosition);\n }\n\n async function fetchSpriteInfo() {\n if (!props.scene || !props.scene.paths.vtt) { return; }\n\n const response = await axios.get<string>(props.scene.paths.vtt, {responseType: \"text\"});\n if (response.status !== 200) {\n console.log(response.statusText);\n }\n\n // TODO: This is gnarly\n const lines = response.data.split(\"\\n\");\n if (lines.shift() !== \"WEBVTT\") { return; }\n if (lines.shift() !== \"\") { return; }\n let item: ISceneSpriteItem = {start: 0, end: 0, x: 0, y: 0, w: 0, h: 0};\n const newSpriteItems: ISceneSpriteItem[] = [];\n while (lines.length) {\n const line = lines.shift();\n if (line === undefined) { continue; }\n\n if (line.includes(\"#\") && line.includes(\"=\") && line.includes(\",\")) {\n const size = line.split(\"#\")[1].split(\"=\")[1].split(\",\");\n item.x = Number(size[0]);\n item.y = Number(size[1]);\n item.w = Number(size[2]);\n item.h = Number(size[3]);\n\n newSpriteItems.push(item);\n item = {start: 0, end: 0, x: 0, y: 0, w: 0, h: 0};\n } else if (line.includes(\" --> \")) {\n const times = line.split(\" --> \");\n\n const start = times[0].split(\":\");\n item.start = (+start[0]) * 60 * 60 + (+start[1]) * 60 + (+start[2]);\n\n const end = times[1].split(\":\");\n item.end = (+end[0]) * 60 * 60 + (+end[1]) * 60 + (+end[2]);\n }\n }\n\n setSpriteItems(newSpriteItems);\n // TODO: Very hacky. Need to wait for the scroll width to update from the image loading.\n setTimeout(() => {\n setDelayedRender(true);\n }, 100);\n }\n\n function renderTags() {\n function getTagStyle(i: number): CSSProperties {\n if (!scrubberSliderEl.current ||\n spriteItems.length === 0 ||\n getBounds() === 0) { return {}; }\n\n const tags = window.document.getElementsByClassName(\"scrubber-tag\");\n if (tags.length === 0) { return {}; }\n\n let tag: any;\n for (let index = 0; index < tags.length; index++) {\n tag = tags.item(index) as any;\n const id = tag.getAttribute(\"data-marker-id\");\n if (id === i.toString()) {\n break;\n }\n }\n\n const marker = props.scene.scene_markers[i];\n const duration = Number(props.scene.file.duration);\n const percentage = marker.seconds / duration;\n\n const left = (scrubberSliderEl.current.scrollWidth * percentage) - (tag.clientWidth / 2);\n return {\n left: `${left}px`,\n height: 20,\n };\n }\n\n return props.scene.scene_markers.map((marker, index) => {\n const dataAttrs = {\n \"data-marker-id\": index,\n };\n return (\n <div\n key={index}\n className=\"scrubber-tag\"\n style={getTagStyle(index)}\n {...dataAttrs}\n >\n {marker.title}\n </div>\n );\n });\n }\n\n function renderSprites() {\n function getStyleForSprite(index: number): CSSProperties {\n if (!props.scene.paths.vtt) { return {}; }\n const sprite = spriteItems[index];\n const left = sprite.w * index;\n const path = props.scene.paths.vtt.replace(\"_thumbs.vtt\", \"_sprite.jpg\"); // TODO: Gnarly\n return {\n width: `${sprite.w}px`,\n height: `${sprite.h}px`,\n margin: \"0px auto\",\n backgroundPosition: -sprite.x + \"px \" + -sprite.y + \"px\",\n backgroundImage: `url(${path})`,\n left: `${left}px`,\n };\n }\n\n return spriteItems.map((spriteItem, index) => {\n const dataAttrs = {\n \"data-sprite-item-id\": index,\n };\n return (\n <div\n key={index}\n className=\"scrubber-item\"\n style={getStyleForSprite(index)}\n {...dataAttrs}\n >\n <span>{TextUtils.secondsToTimestamp(spriteItem.start)} - {TextUtils.secondsToTimestamp(spriteItem.end)}</span>\n </div>\n );\n });\n }\n\n return (\n <div className=\"scrubber-wrapper\">\n <a className=\"scrubber-button\" id=\"scrubber-back\" onClick={() => goBack()}><</a>\n <div ref={contentEl} className=\"scrubber-content\">\n <div className=\"scrubber-tags-background\" />\n <div ref={positionIndicatorEl} id=\"scrubber-position-indicator\" />\n <div id=\"scrubber-current-position\" />\n <div className=\"scrubber-viewport\">\n <div ref={scrubberSliderEl} className=\"scrubber-slider\">\n <div className=\"scrubber-tags\">\n {renderTags()}\n </div>\n {renderSprites()}\n </div>\n </div>\n </div>\n <a className=\"scrubber-button\" id=\"scrubber-forward\" onClick={() => goForward()}>></a>\n </div>\n );\n};\n","/home/peroo/stash/ui/v2/src/components/scenes/SceneSelectedOptions.tsx",[],"/home/peroo/stash/ui/v2/src/components/scenes/helpers.tsx",[],"/home/peroo/stash/ui/v2/src/components/scenes/scenes.tsx",[],"/home/peroo/stash/ui/v2/src/components/select/FilterMultiSelect.tsx",["459"],"import * as React from \"react\";\n\nimport { MenuItem } from \"@blueprintjs/core\";\nimport { IMultiSelectProps, ItemPredicate, ItemRenderer, MultiSelect } from \"@blueprintjs/select\";\nimport * as GQL from \"../../core/generated-graphql\";\nimport { StashService } from \"../../core/StashService\";\nimport { HTMLInputProps } from \"../../models\";\nimport { ErrorUtils } from \"../../utils/errors\";\nimport { ToastUtils } from \"../../utils/toasts\";\n\nconst InternalPerformerMultiSelect = MultiSelect.ofType<GQL.AllPerformersForFilterAllPerformers>();\nconst InternalTagMultiSelect = MultiSelect.ofType<GQL.AllTagsForFilterAllTags>();\nconst InternalStudioMultiSelect = MultiSelect.ofType<GQL.AllStudiosForFilterAllStudios>();\n\ntype ValidTypes =\n GQL.AllPerformersForFilterAllPerformers |\n GQL.AllTagsForFilterAllTags |\n GQL.AllStudiosForFilterAllStudios;\n\ninterface IProps extends HTMLInputProps, Partial<IMultiSelectProps<ValidTypes>> {\n type: \"performers\" | \"studios\" | \"tags\";\n initialIds?: string[];\n onUpdate: (items: ValidTypes[]) => void;\n}\n\nexport const FilterMultiSelect: React.FunctionComponent<IProps> = (props: IProps) => {\n let MultiSelectImpl = getMultiSelectImpl();\n let InternalMultiSelect = MultiSelectImpl.getInternalMultiSelect();\n const data = MultiSelectImpl.getData();\n \n const [selectedItems, setSelectedItems] = React.useState<ValidTypes[]>([]);\n const [items, setItems] = React.useState<ValidTypes[]>([]);\n const [newTagName, setNewTagName] = React.useState<string>(\"\");\n const createTag = StashService.useTagCreate(getTagInput() as GQL.TagCreateInput);\n\n React.useEffect(() => {\n if (!!data) {\n MultiSelectImpl.translateData();\n }\n }, [data]);\n \n function getTagInput() {\n const tagInput: Partial<GQL.TagCreateInput | GQL.TagUpdateInput> = { name: newTagName };\n return tagInput;\n }\n\n async function onCreateNewObject(item: ValidTypes) {\n var created : any;\n if (props.type === \"tags\") {\n try {\n created = await createTag();\n \n items.push(created.data.tagCreate);\n setItems(items.slice());\n addSelectedItem(created.data.tagCreate);\n \n ToastUtils.success(\"Created tag\");\n } catch (e) {\n ErrorUtils.handle(e);\n }\n }\n }\n\n function createNewTag(query : string) {\n setNewTagName(query);\n return {\n name : query\n };\n }\n\n function createNewRenderer(query: string, active: boolean, handleClick: React.MouseEventHandler<HTMLElement>) {\n // if tag already exists with that name, then don't return anything\n if (items.find((item) => {\n return item.name === query;\n })) {\n return undefined;\n }\n\n return (\n <MenuItem\n icon=\"add\"\n text={`Create \"${query}\"`}\n active={active}\n onClick={handleClick}\n />\n );\n }\n\n React.useEffect(() => {\n if (!!props.initialIds && !!items) {\n const initialItems = items.filter((item) => props.initialIds!.includes(item.id));\n setSelectedItems(initialItems);\n }\n }, [props.initialIds, items]);\n\n function getMultiSelectImpl() {\n let getInternalMultiSelect: () => new (props: IMultiSelectProps<any>) => MultiSelect<any>;\n let getData: () => GQL.AllPerformersForFilterQuery | GQL.AllStudiosForFilterQuery | GQL.AllTagsForFilterQuery | undefined;\n let translateData: () => void;\n let createNewObject: ((query : string) => void) | undefined = undefined; \n\n switch (props.type) {\n case \"performers\": {\n getInternalMultiSelect = () => { return InternalPerformerMultiSelect; };\n getData = () => { const { data } = StashService.useAllPerformersForFilter(); return data; }\n translateData = () => { let perfData = data as GQL.AllPerformersForFilterQuery; setItems(!!perfData && !!perfData.allPerformers ? perfData.allPerformers : []); };\n break;\n }\n case \"studios\": {\n getInternalMultiSelect = () => { return InternalStudioMultiSelect; };\n getData = () => { const { data } = StashService.useAllStudiosForFilter(); return data; }\n translateData = () => { let studioData = data as GQL.AllStudiosForFilterQuery; setItems(!!studioData && !!studioData.allStudios ? studioData.allStudios : []); };\n break;\n }\n case \"tags\": {\n getInternalMultiSelect = () => { return InternalTagMultiSelect; };\n getData = () => { const { data } = StashService.useAllTagsForFilter(); return data; }\n translateData = () => { let tagData = data as GQL.AllTagsForFilterQuery; setItems(!!tagData && !!tagData.allTags ? tagData.allTags : []); };\n createNewObject = createNewTag;\n break;\n }\n default: {\n throw Error(\"Unhandled case in FilterMultiSelect\");\n }\n }\n\n return {\n getInternalMultiSelect: getInternalMultiSelect,\n getData: getData,\n translateData: translateData,\n createNewObject: createNewObject\n };\n }\n\n const renderItem: ItemRenderer<ValidTypes> = (item, itemProps) => {\n if (!itemProps.modifiers.matchesPredicate) { return null; }\n return (\n <MenuItem\n active={itemProps.modifiers.active}\n disabled={itemProps.modifiers.disabled}\n key={item.id}\n onClick={itemProps.handleClick}\n text={item.name}\n />\n );\n };\n\n const filter: ItemPredicate<ValidTypes> = (query, item) => {\n if (selectedItems.includes(item)) { return false; }\n return item.name!.toLowerCase().indexOf(query.toLowerCase()) >= 0;\n };\n\n function addSelectedItem(item: ValidTypes) {\n selectedItems.push(item);\n setSelectedItems(selectedItems);\n props.onUpdate(selectedItems);\n }\n\n function onItemSelect(item: ValidTypes) {\n if (item.id === undefined) {\n // create the new item, if applicable\n onCreateNewObject(item);\n } else {\n addSelectedItem(item);\n }\n }\n\n function onItemRemove(value: string, index: number) {\n const newSelectedItems = selectedItems.filter((_, i) => i !== index);\n setSelectedItems(newSelectedItems);\n props.onUpdate(newSelectedItems);\n }\n\n return (\n <InternalMultiSelect\n items={items}\n selectedItems={selectedItems}\n itemRenderer={renderItem}\n itemPredicate={filter}\n tagRenderer={(tag) => tag.name}\n tagInputProps={{ onRemove: onItemRemove }}\n onItemSelect={onItemSelect}\n resetOnSelect={true}\n popoverProps={{position: \"bottom\"}}\n createNewItemFromQuery={MultiSelectImpl.createNewObject}\n createNewItemRenderer={createNewRenderer}\n {...props}\n />\n );\n};\n","/home/peroo/stash/ui/v2/src/components/select/FilterSelect.tsx",[],"/home/peroo/stash/ui/v2/src/components/select/MarkerTitleSuggest.tsx",[],"/home/peroo/stash/ui/v2/src/components/select/ScrapePerformerSuggest.tsx",[],"/home/peroo/stash/ui/v2/src/components/select/ValidGalleriesSelect.tsx",[],"/home/peroo/stash/ui/v2/src/core/StashService.ts",[],"/home/peroo/stash/ui/v2/src/core/generated-graphql.tsx",[],"/home/peroo/stash/ui/v2/src/hooks/ListHook.tsx",["460","461","462","463"],"import { Spinner } from \"@blueprintjs/core\";\nimport _ from \"lodash\";\nimport queryString from \"query-string\";\nimport React, { useEffect, useState } from \"react\";\nimport { QueryHookResult } from \"react-apollo-hooks\";\nimport { ListFilter } from \"../components/list/ListFilter\";\nimport { Pagination } from \"../components/list/Pagination\";\nimport { StashService } from \"../core/StashService\";\nimport { IBaseProps } from \"../models\";\nimport { Criterion } from \"../models/list-filter/criteria/criterion\";\nimport { ListFilterModel } from \"../models/list-filter/filter\";\nimport { DisplayMode, FilterMode } from \"../models/list-filter/types\";\n\nexport interface IListHookData {\n filter: ListFilterModel;\n template: JSX.Element;\n options: IListHookOptions;\n onSelectChange: (id: string, selected : boolean, shiftKey: boolean) => void;\n}\n\nexport interface IListHookOptions {\n filterMode: FilterMode;\n props: IBaseProps;\n zoomable?: boolean\n renderContent: (result: QueryHookResult<any, any>, filter: ListFilterModel, selectedIds: Set<string>, zoomIndex: number) => JSX.Element | undefined;\n renderSelectedOptions?: (result: QueryHookResult<any, any>, selectedIds: Set<string>) => JSX.Element | undefined;\n}\n\nexport class ListHook {\n public static useList(options: IListHookOptions): IListHookData {\n const [filter, setFilter] = useState<ListFilterModel>(new ListFilterModel(options.filterMode));\n const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());\n const [lastClickedId, setLastClickedId] = useState<string | undefined>(undefined);\n const [totalCount, setTotalCount] = useState<number>(0);\n const [zoomIndex, setZoomIndex] = useState<number>(1);\n\n // Update the filter when the query parameters change\n useEffect(() => {\n const queryParams = queryString.parse(options.props.location.search);\n const newFilter = _.cloneDeep(filter);\n newFilter.configureFromQueryParameters(queryParams);\n setFilter(newFilter);\n\n // TODO: Need this side effect to update the query params properly\n filter.configureFromQueryParameters(queryParams);\n }, [options.props.location.search]);\n\n let result: QueryHookResult<any, any>;\n\n let getData: (filter : ListFilterModel) => QueryHookResult<any, any>;\n let getItems: () => any[];\n let getCount: () => number;\n\n switch (options.filterMode) {\n case FilterMode.Scenes: {\n getData = (filter : ListFilterModel) => { return StashService.useFindScenes(filter); }\n getItems = () => { return !!result.data && !!result.data.findScenes ? result.data.findScenes.scenes : []; }\n getCount = () => { return !!result.data && !!result.data.findScenes ? result.data.findScenes.count : 0; }\n break;\n }\n case FilterMode.SceneMarkers: {\n getData = (filter : ListFilterModel) => { return StashService.useFindSceneMarkers(filter); }\n getItems = () => { return !!result.data && !!result.data.findSceneMarkers ? result.data.findSceneMarkers.scene_markers : []; }\n getCount = () => { return !!result.data && !!result.data.findSceneMarkers ? result.data.findSceneMarkers.count : 0; }\n break;\n }\n case FilterMode.Galleries: {\n getData = (filter : ListFilterModel) => { return StashService.useFindGalleries(filter); }\n getItems = () => { return !!result.data && !!result.data.findGalleries ? result.data.findGalleries.galleries : []; }\n getCount = () => { return !!result.data && !!result.data.findGalleries ? result.data.findGalleries.count : 0; }\n break;\n }\n case FilterMode.Studios: {\n getData = (filter : ListFilterModel) => { return StashService.useFindStudios(filter); }\n getItems = () => { return !!result.data && !!result.data.findStudios ? result.data.findStudios.studios : []; }\n getCount = () => { return !!result.data && !!result.data.findStudios ? result.data.findStudios.count : 0; }\n break;\n }\n case FilterMode.Performers: {\n getData = (filter : ListFilterModel) => { return StashService.useFindPerformers(filter); }\n getItems = () => { return !!result.data && !!result.data.findPerformers ? result.data.findPerformers.performers : []; }\n getCount = () => { return !!result.data && !!result.data.findPerformers ? result.data.findPerformers.count : 0; }\n break;\n }\n default: {\n console.error(\"REMOVE DEFAULT IN LIST HOOK\");\n getData = (filter : ListFilterModel) => { return StashService.useFindScenes(filter); }\n getItems = () => { return !!result.data && !!result.data.findScenes ? result.data.findScenes.scenes : []; }\n getCount = () => { return !!result.data && !!result.data.findScenes ? result.data.findScenes.count : 0; }\n break;\n }\n }\n\n result = getData(filter);\n\n useEffect(() => {\n setTotalCount(getCount());\n\n // select none when data changes\n onSelectNone();\n setLastClickedId(undefined);\n }, [result.data])\n\n // Update the query parameters when the data changes\n useEffect(() => {\n const location = Object.assign({}, options.props.history.location);\n location.search = filter.makeQueryParameters();\n options.props.history.replace(location);\n }, [result.data, filter.displayMode]);\n\n // Update the total count\n useEffect(() => {\n const newFilter = _.cloneDeep(filter);\n newFilter.totalCount = totalCount;\n setFilter(newFilter);\n }, [totalCount]);\n\n function onChangePageSize(pageSize: number) {\n const newFilter = _.cloneDeep(filter);\n newFilter.itemsPerPage = pageSize;\n newFilter.currentPage = 1;\n setFilter(newFilter);\n }\n\n function onChangeQuery(query: string) {\n const newFilter = _.cloneDeep(filter);\n newFilter.searchTerm = query;\n newFilter.currentPage = 1;\n setFilter(newFilter);\n }\n\n function onChangeSortDirection(sortDirection: \"asc\" | \"desc\") {\n const newFilter = _.cloneDeep(filter);\n newFilter.sortDirection = sortDirection;\n setFilter(newFilter);\n }\n\n function onChangeSortBy(sortBy: string) {\n const newFilter = _.cloneDeep(filter);\n newFilter.sortBy = sortBy;\n newFilter.currentPage = 1;\n setFilter(newFilter);\n }\n\n function onChangeDisplayMode(displayMode: DisplayMode) {\n const newFilter = _.cloneDeep(filter);\n newFilter.displayMode = displayMode;\n setFilter(newFilter);\n }\n\n function onAddCriterion(criterion: Criterion, oldId?: string) {\n const newFilter = _.cloneDeep(filter);\n\n // Find if we are editing an existing criteria, then modify that. Or create a new one.\n const existingIndex = newFilter.criteria.findIndex((c) => {\n // If we modified an existing criterion, then look for the old id.\n const id = !!oldId ? oldId : criterion.getId();\n return c.getId() === id;\n });\n if (existingIndex === -1) {\n newFilter.criteria.push(criterion);\n } else {\n newFilter.criteria[existingIndex] = criterion;\n }\n\n // Remove duplicate modifiers\n newFilter.criteria = newFilter.criteria.filter((obj, pos, arr) => {\n return arr.map((mapObj: any) => mapObj.getId()).indexOf(obj.getId()) === pos;\n });\n\n newFilter.currentPage = 1;\n setFilter(newFilter);\n }\n\n function onRemoveCriterion(removedCriterion: Criterion) {\n const newFilter = _.cloneDeep(filter);\n newFilter.criteria = newFilter.criteria.filter((criterion) => criterion.getId() !== removedCriterion.getId());\n newFilter.currentPage = 1;\n setFilter(newFilter);\n }\n\n function onChangePage(page: number) {\n const newFilter = _.cloneDeep(filter);\n newFilter.currentPage = page;\n setFilter(newFilter);\n }\n\n function onSelectChange(id: string, selected : boolean, shiftKey: boolean) {\n if (shiftKey) {\n multiSelect(id, selected);\n } else {\n singleSelect(id, selected);\n }\n }\n\n function singleSelect(id: string, selected: boolean) {\n setLastClickedId(id);\n \n const newSelectedIds = _.clone(selectedIds);\n if (selected) {\n newSelectedIds.add(id);\n } else {\n newSelectedIds.delete(id);\n }\n\n setSelectedIds(newSelectedIds);\n }\n\n function multiSelect(id: string, selected : boolean) {\n let startIndex = 0;\n let thisIndex = -1;\n \n if (!!lastClickedId) {\n startIndex = getItems().findIndex((item) => {\n return item.id === lastClickedId;\n });\n }\n\n thisIndex = getItems().findIndex((item) => {\n return item.id === id;\n });\n\n selectRange(startIndex, thisIndex);\n }\n \n function selectRange(startIndex : number, endIndex : number) {\n if (startIndex > endIndex) {\n let tmp = startIndex;\n startIndex = endIndex;\n endIndex = tmp;\n }\n \n const subset = getItems().slice(startIndex, endIndex + 1);\n const newSelectedIds : Set<string> = new Set();\n\n subset.forEach((item) => {\n newSelectedIds.add(item.id);\n });\n\n setSelectedIds(newSelectedIds);\n }\n\n function onSelectAll() {\n const newSelectedIds : Set<string> = new Set();\n getItems().forEach((item) => {\n newSelectedIds.add(item.id);\n });\n\n setSelectedIds(newSelectedIds);\n setLastClickedId(undefined);\n }\n\n function onSelectNone() {\n const newSelectedIds : Set<string> = new Set();\n setSelectedIds(newSelectedIds);\n setLastClickedId(undefined);\n }\n\n function onChangeZoom(newZoomIndex : number) {\n setZoomIndex(newZoomIndex);\n }\n\n const template = (\n <div>\n <ListFilter\n onChangePageSize={onChangePageSize}\n onChangeQuery={onChangeQuery}\n onChangeSortDirection={onChangeSortDirection}\n onChangeSortBy={onChangeSortBy}\n onChangeDisplayMode={onChangeDisplayMode}\n onAddCriterion={onAddCriterion}\n onRemoveCriterion={onRemoveCriterion}\n onSelectAll={onSelectAll}\n onSelectNone={onSelectNone}\n zoomIndex={options.zoomable ? zoomIndex : undefined}\n onChangeZoom={options.zoomable ? onChangeZoom : undefined}\n filter={filter}\n />\n {options.renderSelectedOptions && selectedIds.size > 0 ? options.renderSelectedOptions(result, selectedIds) : undefined}\n {result.loading ? <Spinner size={Spinner.SIZE_LARGE} /> : undefined}\n {result.error ? <h1>{result.error.message}</h1> : undefined}\n {options.renderContent(result, filter, selectedIds, zoomIndex)}\n <Pagination\n itemsPerPage={filter.itemsPerPage}\n currentPage={filter.currentPage}\n totalItems={totalCount}\n onChangePage={onChangePage}\n />\n </div>\n );\n\n return { filter, template, options, onSelectChange };\n }\n}\n","/home/peroo/stash/ui/v2/src/hooks/LocalForage.ts",[],"/home/peroo/stash/ui/v2/src/hooks/VideoHover.ts",[],"/home/peroo/stash/ui/v2/src/index.tsx",[],"/home/peroo/stash/ui/v2/src/models/base-props.ts",[],"/home/peroo/stash/ui/v2/src/models/index.ts",[],"/home/peroo/stash/ui/v2/src/models/list-filter/criteria/criterion.ts",[],"/home/peroo/stash/ui/v2/src/models/list-filter/criteria/favorite.ts",[],"/home/peroo/stash/ui/v2/src/models/list-filter/criteria/has-markers.ts",[],"/home/peroo/stash/ui/v2/src/models/list-filter/criteria/is-missing.ts",[],"/home/peroo/stash/ui/v2/src/models/list-filter/criteria/none.ts",[],"/home/peroo/stash/ui/v2/src/models/list-filter/criteria/performers.ts",[],"/home/peroo/stash/ui/v2/src/models/list-filter/criteria/rating.ts",[],"/home/peroo/stash/ui/v2/src/models/list-filter/criteria/resolution.ts",[],"/home/peroo/stash/ui/v2/src/models/list-filter/criteria/studios.ts",[],"/home/peroo/stash/ui/v2/src/models/list-filter/criteria/tags.ts",[],"/home/peroo/stash/ui/v2/src/models/list-filter/criteria/utils.ts",[],"/home/peroo/stash/ui/v2/src/models/list-filter/filter.ts",[],"/home/peroo/stash/ui/v2/src/models/list-filter/types.ts",[],"/home/peroo/stash/ui/v2/src/models/react-images.d.ts",[],"/home/peroo/stash/ui/v2/src/models/react-jw-player.d.ts",[],"/home/peroo/stash/ui/v2/src/models/types.ts",[],"/home/peroo/stash/ui/v2/src/react-app-env.d.ts",[],"/home/peroo/stash/ui/v2/src/serviceWorker.ts",[],"/home/peroo/stash/ui/v2/src/utils/color.ts",[],"/home/peroo/stash/ui/v2/src/utils/errors.ts",[],"/home/peroo/stash/ui/v2/src/utils/navigation.ts",[],"/home/peroo/stash/ui/v2/src/utils/table.tsx",[],"/home/peroo/stash/ui/v2/src/utils/text.ts",[],"/home/peroo/stash/ui/v2/src/utils/toasts.ts",[],"/home/peroo/stash/ui/v2/src/utils/zoom.ts",[],{"ruleId":"464","severity":1,"message":"465","line":1,"column":36,"nodeType":"466","endLine":1,"endColumn":45},{"ruleId":"464","severity":1,"message":"467","line":4,"column":8,"nodeType":"466","endLine":4,"endColumn":9},{"ruleId":"468","severity":1,"message":"469","line":23,"column":6,"nodeType":"470","endLine":23,"endColumn":12,"fix":"471"},{"ruleId":"464","severity":1,"message":"467","line":2,"column":8,"nodeType":"466","endLine":2,"endColumn":9},{"ruleId":"472","severity":1,"message":"473","line":39,"column":49,"nodeType":"474","endLine":39,"endColumn":100},{"ruleId":"464","severity":1,"message":"467","line":1,"column":8,"nodeType":"466","endLine":1,"endColumn":9},{"ruleId":"468","severity":1,"message":"475","line":26,"column":6,"nodeType":"470","endLine":26,"endColumn":13,"fix":"476"},{"ruleId":"464","severity":1,"message":"477","line":2,"column":3,"nodeType":"466","endLine":2,"endColumn":5},{"ruleId":"464","severity":1,"message":"478","line":4,"column":3,"nodeType":"466","endLine":4,"endColumn":5},{"ruleId":"464","severity":1,"message":"479","line":7,"column":3,"nodeType":"466","endLine":7,"endColumn":6},{"ruleId":"464","severity":1,"message":"480","line":10,"column":13,"nodeType":"466","endLine":10,"endColumn":16},{"ruleId":"464","severity":1,"message":"481","line":11,"column":10,"nodeType":"466","endLine":11,"endColumn":19},{"ruleId":"464","severity":1,"message":"477","line":5,"column":3,"nodeType":"466","endLine":5,"endColumn":5},{"ruleId":"464","severity":1,"message":"478","line":7,"column":3,"nodeType":"466","endLine":7,"endColumn":5},{"ruleId":"464","severity":1,"message":"479","line":10,"column":3,"nodeType":"466","endLine":10,"endColumn":6},{"ruleId":"468","severity":1,"message":"482","line":69,"column":6,"nodeType":"470","endLine":69,"endColumn":12,"fix":"483"},{"ruleId":"464","severity":1,"message":"467","line":11,"column":8,"nodeType":"466","endLine":11,"endColumn":9},{"ruleId":"468","severity":1,"message":"484","line":51,"column":6,"nodeType":"470","endLine":51,"endColumn":19,"fix":"485"},{"ruleId":"468","severity":1,"message":"486","line":92,"column":6,"nodeType":"470","endLine":92,"endColumn":12,"fix":"487"},{"ruleId":"468","severity":1,"message":"488","line":101,"column":6,"nodeType":"470","endLine":101,"endColumn":20,"fix":"489"},{"ruleId":"468","severity":1,"message":"490","line":117,"column":6,"nodeType":"470","endLine":117,"endColumn":16,"fix":"491"},{"ruleId":"492","severity":1,"message":"493","line":158,"column":18,"nodeType":"494","messageId":"495","endLine":158,"endColumn":20},{"ruleId":"464","severity":1,"message":"496","line":8,"column":3,"nodeType":"466","endLine":8,"endColumn":15},{"ruleId":"464","severity":1,"message":"467","line":11,"column":8,"nodeType":"466","endLine":11,"endColumn":9},{"ruleId":"464","severity":1,"message":"497","line":2,"column":43,"nodeType":"466","endLine":2,"endColumn":59},{"ruleId":"492","severity":1,"message":"493","line":49,"column":21,"nodeType":"494","messageId":"495","endLine":49,"endColumn":23},{"ruleId":"464","severity":1,"message":"467","line":9,"column":8,"nodeType":"466","endLine":9,"endColumn":9},{"ruleId":"468","severity":1,"message":"482","line":32,"column":6,"nodeType":"470","endLine":32,"endColumn":12,"fix":"498"},{"ruleId":"464","severity":1,"message":"467","line":5,"column":8,"nodeType":"466","endLine":5,"endColumn":9},{"ruleId":"464","severity":1,"message":"477","line":1,"column":10,"nodeType":"466","endLine":1,"endColumn":12},{"ruleId":"464","severity":1,"message":"499","line":2,"column":3,"nodeType":"466","endLine":2,"endColumn":9},{"ruleId":"464","severity":1,"message":"500","line":3,"column":3,"nodeType":"466","endLine":3,"endColumn":10},{"ruleId":"464","severity":1,"message":"501","line":4,"column":3,"nodeType":"466","endLine":4,"endColumn":9},{"ruleId":"464","severity":1,"message":"502","line":6,"column":3,"nodeType":"466","endLine":6,"endColumn":13},{"ruleId":"464","severity":1,"message":"467","line":10,"column":8,"nodeType":"466","endLine":10,"endColumn":9},{"ruleId":"468","severity":1,"message":"469","line":54,"column":6,"nodeType":"470","endLine":54,"endColumn":12,"fix":"503"},{"ruleId":"468","severity":1,"message":"504","line":63,"column":6,"nodeType":"470","endLine":63,"endColumn":14,"fix":"505"},{"ruleId":"492","severity":1,"message":"493","line":66,"column":38,"nodeType":"494","messageId":"495","endLine":66,"endColumn":40},{"ruleId":"464","severity":1,"message":"506","line":136,"column":13,"nodeType":"466","endLine":136,"endColumn":19},{"ruleId":"472","severity":1,"message":"473","line":162,"column":11,"nodeType":"474","endLine":162,"endColumn":56},{"ruleId":"464","severity":1,"message":"467","line":1,"column":8,"nodeType":"466","endLine":1,"endColumn":9},{"ruleId":"464","severity":1,"message":"507","line":1,"column":42,"nodeType":"466","endLine":1,"endColumn":54},{"ruleId":"464","severity":1,"message":"508","line":1,"column":67,"nodeType":"466","endLine":1,"endColumn":76},{"ruleId":"464","severity":1,"message":"479","line":1,"column":99,"nodeType":"466","endLine":1,"endColumn":102},{"ruleId":"464","severity":1,"message":"467","line":2,"column":8,"nodeType":"466","endLine":2,"endColumn":9},{"ruleId":"464","severity":1,"message":"509","line":4,"column":10,"nodeType":"466","endLine":4,"endColumn":25},{"ruleId":"464","severity":1,"message":"510","line":6,"column":10,"nodeType":"466","endLine":6,"endColumn":28},{"ruleId":"464","severity":1,"message":"511","line":6,"column":30,"nodeType":"466","endLine":6,"endColumn":52},{"ruleId":"464","severity":1,"message":"512","line":9,"column":10,"nodeType":"466","endLine":9,"endColumn":18},{"ruleId":"464","severity":1,"message":"513","line":11,"column":10,"nodeType":"466","endLine":11,"endColumn":25},{"ruleId":"464","severity":1,"message":"514","line":12,"column":10,"nodeType":"466","endLine":12,"endColumn":21},{"ruleId":"464","severity":1,"message":"515","line":12,"column":23,"nodeType":"466","endLine":12,"endColumn":33},{"ruleId":"468","severity":1,"message":"469","line":39,"column":6,"nodeType":"470","endLine":39,"endColumn":12,"fix":"516"},{"ruleId":"472","severity":1,"message":"473","line":119,"column":11,"nodeType":"474","endLine":119,"endColumn":88},{"ruleId":"464","severity":1,"message":"467","line":1,"column":8,"nodeType":"466","endLine":1,"endColumn":9},{"ruleId":"464","severity":1,"message":"496","line":2,"column":3,"nodeType":"466","endLine":2,"endColumn":15},{"ruleId":"464","severity":1,"message":"517","line":5,"column":3,"nodeType":"466","endLine":5,"endColumn":15},{"ruleId":"464","severity":1,"message":"518","line":16,"column":63,"nodeType":"466","endLine":16,"endColumn":69},{"ruleId":"468","severity":1,"message":"519","line":45,"column":22,"nodeType":"520","endLine":47,"endColumn":12},{"ruleId":"468","severity":1,"message":"469","line":89,"column":6,"nodeType":"470","endLine":89,"endColumn":12,"fix":"521"},{"ruleId":"468","severity":1,"message":"504","line":98,"column":6,"nodeType":"470","endLine":98,"endColumn":17,"fix":"522"},{"ruleId":"468","severity":1,"message":"469","line":32,"column":6,"nodeType":"470","endLine":32,"endColumn":12,"fix":"523"},{"ruleId":"468","severity":1,"message":"524","line":34,"column":3,"nodeType":"466","endLine":34,"endColumn":12,"fix":"525"},{"ruleId":"468","severity":1,"message":"526","line":359,"column":6,"nodeType":"470","endLine":359,"endColumn":19,"fix":"527"},{"ruleId":"468","severity":1,"message":"528","line":464,"column":6,"nodeType":"470","endLine":464,"endColumn":20,"fix":"529"},{"ruleId":"464","severity":1,"message":"530","line":2,"column":51,"nodeType":"466","endLine":2,"endColumn":60},{"ruleId":"464","severity":1,"message":"531","line":58,"column":10,"nodeType":"466","endLine":58,"endColumn":23},{"ruleId":"468","severity":1,"message":"532","line":67,"column":6,"nodeType":"470","endLine":67,"endColumn":19,"fix":"533"},{"ruleId":"468","severity":1,"message":"534","line":77,"column":6,"nodeType":"470","endLine":77,"endColumn":22,"fix":"535"},{"ruleId":"468","severity":1,"message":"536","line":91,"column":17,"nodeType":"466","endLine":91,"endColumn":24},{"ruleId":"468","severity":1,"message":"536","line":100,"column":17,"nodeType":"466","endLine":100,"endColumn":24},{"ruleId":"468","severity":1,"message":"537","line":40,"column":6,"nodeType":"470","endLine":40,"endColumn":12,"fix":"538"},{"ruleId":"468","severity":1,"message":"539","line":46,"column":8,"nodeType":"470","endLine":46,"endColumn":39,"fix":"540"},{"ruleId":"468","severity":1,"message":"541","line":102,"column":8,"nodeType":"470","endLine":102,"endColumn":21,"fix":"542"},{"ruleId":"468","severity":1,"message":"543","line":109,"column":8,"nodeType":"470","endLine":109,"endColumn":41,"fix":"544"},{"ruleId":"468","severity":1,"message":"539","line":116,"column":8,"nodeType":"470","endLine":116,"endColumn":20,"fix":"545"},"@typescript-eslint/no-unused-vars","'useEffect' is defined but never used.","Identifier","'_' is defined but never used.","react-hooks/exhaustive-deps","React Hook useEffect has missing dependencies: 'error' and 'loading'. Either include them or remove the dependency array.","ArrayExpression",{"range":"546","text":"547"},"jsx-a11y/alt-text","img elements must have an alt prop, either with meaningful text, or an empty string for decorative images.","JSXOpeningElement","React Hook useEffect has a missing dependency: 'props.history'. Either include it or remove the dependency array.",{"range":"548","text":"549"},"'H1' is defined but never used.","'H6' is defined but never used.","'Tag' is defined but never used.","'GQL' is defined but never used.","'TextUtils' is defined but never used.","React Hook useEffect has a missing dependency: 'error'. Either include it or remove the dependency array.",{"range":"550","text":"551"},"React Hook useEffect has a missing dependency: 'config.error'. Either include it or remove the dependency array.",{"range":"552","text":"553"},"React Hook useEffect has missing dependencies: 'filterByLogLevel', 'prependLogEntries', and 'updateFilteredEntries'. Either include them or remove the dependency array.",{"range":"554","text":"555"},"React Hook useEffect has missing dependencies: 'appendLogEntries' and 'updateFilteredEntries'. Either include them or remove the dependency array.",{"range":"556","text":"557"},"React Hook useEffect has a missing dependency: 'updateFilteredEntries'. Either include it or remove the dependency array.",{"range":"558","text":"559"},"eqeqeq","Expected '===' and instead saw '=='.","BinaryExpression","unexpected","'AnchorButton' is defined but never used.","'IInputGroupProps' is defined but never used.",{"range":"560","text":"551"},"'Button' is defined but never used.","'Classes' is defined but never used.","'Dialog' is defined but never used.","'HTMLSelect' is defined but never used.",{"range":"561","text":"547"},"React Hook useEffect has a missing dependency: 'isNew'. Either include it or remove the dependency array.",{"range":"562","text":"563"},"'result' is assigned a value but never used.","'EditableText' is defined but never used.","'HTMLTable' is defined but never used.","'QueryHookResult' is defined but never used.","'FindGalleriesQuery' is defined but never used.","'FindGalleriesVariables' is defined but never used.","'ListHook' is defined but never used.","'ListFilterModel' is defined but never used.","'DisplayMode' is defined but never used.","'FilterMode' is defined but never used.",{"range":"564","text":"547"},"'ControlGroup' is defined but never used.","'useRef' is defined but never used.","Assignments to the 'searchCallback' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useEffect.","CallExpression",{"range":"565","text":"547"},{"range":"566","text":"567"},{"range":"568","text":"547"},"React Hook useEffect contains a call to 'setTimestamp'. Without a list of dependencies, this can lead to an infinite chain of updates. To fix this, pass [props.location.search, timestamp] as a second argument to the useEffect Hook.",{"range":"569","text":"570"},"React Hook useEffect has a missing dependency: 'onFind'. Either include it or remove the dependency array.",{"range":"571","text":"572"},"React Hook useEffect has missing dependencies: 'allDateSet', 'allPerformerSet', 'allStudioSet', 'allTagSet', and 'allTitleSet'. Either include them or remove the dependency array.",{"range":"573","text":"574"},"'RefObject' is defined but never used.","'delayedRender' is assigned a value but never used.","React Hook useEffect has a missing dependency: 'fetchSpriteInfo'. Either include it or remove the dependency array.",{"range":"575","text":"576"},"React Hook useEffect has missing dependencies: 'props.scene.file.duration' and 'setPosition'. Either include them or remove the dependency array.",{"range":"577","text":"578"},"The ref value 'contentEl.current' will likely have changed by the time this effect cleanup function runs. If this ref points to a node rendered by React, copy 'contentEl.current' to a variable inside the effect, and use that variable in the cleanup function.","React Hook React.useEffect has a missing dependency: 'MultiSelectImpl'. Either include it or remove the dependency array.",{"range":"579","text":"580"},"React Hook useEffect has a missing dependency: 'filter'. Either include it or remove the dependency array.",{"range":"581","text":"582"},"React Hook useEffect has a missing dependency: 'getCount'. Either include it or remove the dependency array.",{"range":"583","text":"584"},"React Hook useEffect has missing dependencies: 'filter' and 'options.props.history'. Either include them or remove the dependency array.",{"range":"585","text":"586"},{"range":"587","text":"588"},[823,829],"[data, error, loading]",[983,990],"[props.history, tabId]",[2555,2561],"[data, error]",[1955,1968],"[config.data, config.error]",[2796,2802],"[data, filterByLogLevel, prependLogEntries, updateFilteredEntries]",[3020,3034],"[appendLogEntries, existingData, updateFilteredEntries]",[3422,3432],"[logLevel, updateFilteredEntries]",[1097,1103],[1971,1977],[2157,2165],"[isNew, studio]",[1962,1968],[4264,4270],[4459,4470],"[isNew, performer]",[1311,1317],[1589,1589],", [props.location.search, timestamp]",[9936,9949],"[onFind, parserInput]",[12915,12929],"[allDateSet, allPerformerSet, allStudioSet, allTagSet, allTitleSet, parserResult]",[2337,2350],"[fetchSpriteInfo, props.scene]",[2706,2722],"[props.position, props.scene.file.duration, setPosition]",[1668,1674],"[MultiSelectImpl, data]",[2105,2136],"[filter, options.props.location.search]",[4971,4984],"[getCount, result.data]",[5248,5281],"[result.data, filter.displayMode, options.props.history, filter]",[5458,5470],"[filter, totalCount]"] |