diff --git a/ui/v2.5/.editorconfig b/ui/v2.5/.editorconfig new file mode 100644 index 000000000..86a63dc0f --- /dev/null +++ b/ui/v2.5/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/ui/v2.5/.eslintcache b/ui/v2.5/.eslintcache index aae10c69a..7d4f89169 100644 --- a/ui/v2.5/.eslintcache +++ b/ui/v2.5/.eslintcache @@ -1 +1 @@ -[{"/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 = (props: IProps) => {\n return (\n
\n \n \n \n \n \n {/* */}\n \n \n \n \n \n \n \n \n \n
\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 = (props: IProps) => {\n const [gallery, setGallery] = useState>({});\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 ; }\n if (!!error) { return <>{error.message}; }\n return (\n
\n \n
\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 = (props: IProps) => {\n const listData = ListHook.useList({\n filterMode: FilterMode.Galleries,\n props,\n renderContent,\n });\n\n function renderContent(result: QueryHookResult, filter: ListFilterModel) {\n if (!result.data || !result.data.findGalleries) { return; }\n if (filter.displayMode === DisplayMode.Grid) {\n return

TODO

;\n } else if (filter.displayMode === DisplayMode.List) {\n return (\n \n \n \n Preview\n Path\n \n \n \n {result.data.findGalleries.galleries.map((gallery) => (\n \n \n \n {gallery.files.length > 0 ? : undefined}\n \n \n {gallery.path}\n \n ))}\n \n \n );\n } else if (filter.displayMode === DisplayMode.Wall) {\n return

TODO

;\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 = (props: IProps) => {\n const [currentImage, setCurrentImage] = useState(0);\n const [lightboxIsOpen, setLightboxIsOpen] = useState(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
\n \n window.open(photos[currentImage].src, \"_blank\")}\n width={9999}\n />\n
\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 = (props: IProps) => {\n const [tabId, setTabId] = useState(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 \n setTabId(newId as TabId)}\n defaultSelectedTabId={getTabId()}\n >\n } />\n } />\n } />\n } />\n } />\n \n \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 = (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 \n Version:\n {data.version.version}\n \n );\n }\n\n function renderVersion() {\n if (!data || !data.version) { return; }\n return (\n <>\n \n \n {maybeRenderTag()}\n \n Build hash:\n {data.version.hash}\n \n \n Build time:\n {data.version.build_time}\n \n \n \n \n );\n }\n return (\n <>\n

About

\n {!data || loading ? : undefined}\n {!!error ? error.message : 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 = (props: IProps) => {\n // Editing config state\n const [stashes, setStashes] = useState([]);\n const [databasePath, setDatabasePath] = useState(undefined);\n const [generatedPath, setGeneratedPath] = useState(undefined);\n const [maxTranscodeSize, setMaxTranscodeSize] = useState(undefined);\n const [maxStreamingTranscodeSize, setMaxStreamingTranscodeSize] = useState(undefined);\n const [username, setUsername] = useState(undefined);\n const [password, setPassword] = useState(undefined);\n const [logFile, setLogFile] = useState();\n const [logOut, setLogOut] = useState(true);\n const [logLevel, setLogLevel] = useState(\"Info\");\n const [logAccess, setLogAccess] = useState(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 ?

{error.message}

: undefined}\n {(!data || !data.configuration || loading) ? : undefined}\n

Library

\n \n \n \n \n \n \n \n \n setDatabasePath(e.target.value)} />\n \n\n \n setGeneratedPath(e.target.value)} />\n \n \n \n \n \n

Video

\n \n setMaxTranscodeSize(translateQuality(event.target.value))}\n value={resolutionToString(maxTranscodeSize)}\n />\n \n \n setMaxStreamingTranscodeSize(translateQuality(event.target.value))}\n value={resolutionToString(maxStreamingTranscodeSize)}\n />\n \n
\n \n\n \n

Authentication

\n \n setUsername(e.target.value)} />\n
\n \n setPassword(e.target.value)} />\n \n \n\n \n

Logging

\n \n setLogFile(e.target.value)} />\n \n\n \n setLogOut(!logOut)}\n />\n \n\n \n setLogLevel(event.target.value)}\n value={logLevel}\n />\n \n\n \n setLogAccess(!logAccess)}\n />\n \n\n \n \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 = () => {\n const config = StashService.useConfiguration();\n const [soundOnPreview, setSoundOnPreview] = useState();\n const [wallShowTitle, setWallShowTitle] = useState();\n const [maximumLoopDuration, setMaximumLoopDuration] = useState(0);\n const [autostartVideo, setAutostartVideo] = useState();\n const [showStudioAsText, setShowStudioAsText] = useState();\n const [css, setCSS] = useState();\n const [cssEnabled, setCSSEnabled] = useState();\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 ?

{config.error.message}

: undefined}\n {(!config.data || !config.data.configuration || config.loading) ? : undefined}\n

User Interface

\n \n setWallShowTitle(!wallShowTitle)}\n />\n setSoundOnPreview(!soundOnPreview)}\n />\n \n\n \n {\n setShowStudioAsText(!showStudioAsText)\n }}\n />\n \n \n \n {\n setAutostartVideo(!autostartVideo)\n }}\n />\n\n \n setMaximumLoopDuration(value)}\n min={0}\n minorStepSize={1}\n />\n \n \n\n \n {\n setCSSEnabled(!cssEnabled)\n }}\n />\n\n \n \n\n \n \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 = (props: IProps) => {\n const { data, error } = StashService.useLoggingSubscribe();\n const { data: existingData } = StashService.useLogs();\n \n const logEntries = useRef([]);\n const [logLevel, setLogLevel] = useState(\"Info\");\n const [filteredLogEntries, setFilteredLogEntries] = useState([]);\n const lastUpdate = useRef(0);\n const updateTimeout = useRef();\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 {props.logEntry.time} \n {level} \n {props.logEntry.message}\n
\n \n );\n }\n\n function maybeRenderError() {\n if (error) {\n return (\n <>\n Error connecting to log server: {error.message}
\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

Logs

\n
\n \n setLogLevel(event.target.value)}\n value={logLevel}\n />\n \n
\n
\n {maybeRenderError()}\n {filteredLogEntries.map((logEntry) =>\n \n )}\n
\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 = (props: IProps) => {\n const [isImportAlertOpen, setIsImportAlertOpen] = useState(false);\n const [isCleanAlertOpen, setIsCleanAlertOpen] = useState(false);\n const [nameFromMetadata, setNameFromMetadata] = useState(true);\n const [status, setStatus] = useState(\"\");\n const [progress, setProgress] = useState(undefined);\n\n const [autoTagPerformers, setAutoTagPerformers] = useState(true);\n const [autoTagStudios, setAutoTagStudios] = useState(true);\n const [autoTagTags, setAutoTagTags] = useState(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 setIsImportAlertOpen(false)}\n onConfirm={() => onImport()}\n >\n

\n Are you sure you want to import? This will delete the database and re-import from\n your exported metadata.\n

\n \n );\n }\n\n function onClean() {\n setIsCleanAlertOpen(false);\n StashService.queryMetadataClean().then(() => { jobStatus.refetch()});\n }\n\n function renderCleanAlert() {\n return (\n setIsCleanAlertOpen(false)}\n onConfirm={() => onClean()}\n >\n

\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

\n \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 \n )\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 \n Scenes\n \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 setIsDeleteAlertOpen(false)}\n onConfirm={() => props.onDelete()}\n >\n

\n Are you sure you want to delete {name}?\n

\n \n );\n }\n\n\n return (\n <>\n {renderDeleteAlert()}\n \n \n {renderEditButton()}\n {props.isEditing && !props.isNew ? : undefined}\n {renderScraperMenu()}\n {renderImageInput()}\n {renderSaveButton()}\n\n {renderAutoTagButton()}\n {renderScenesButton()}\n {renderDeleteButton()}\n \n \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 = (props: IProps) => {\n const [value, setValue] = useState(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 \n increment()}\n />\n decrement()}\n />\n \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 onReset()}\n />\n )\n }\n }\n\n return (\n \n setValue(e.target.value)}\n onBlur={() => props.onValueChange(stringToSeconds(value))}\n placeholder=\"hh:mm:ss\"\n rightElement={maybeRenderReset()}\n />\n {renderButtons()}\n \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 = (props: IProps) => {\n const [currentDirectory, setCurrentDirectory] = useState(\"\");\n const [isDisplayingDialog, setIsDisplayingDialog] = useState(false);\n const [selectableDirectories, setSelectableDirectories] = useState([]);\n const [selectedDirectories, setSelectedDirectories] = useState([]);\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 setIsDisplayingDialog(false)}\n title=\"Select Directory\"\n >\n
\n setCurrentDirectory(e.target.value)}\n value={currentDirectory}\n rightElement={(!data || !data.directories || loading) ? : undefined}\n />\n {selectableDirectories.map((path) => {\n return
setCurrentDirectory(path)}>{path}
;\n })}\n
\n
\n
\n \n
\n
\n \n );\n }\n\n return (\n <>\n {!!error ?

{error.message}

: undefined}\n {renderDialog()}\n \n {selectedDirectories.map((path) => {\n return ;\n })}\n \n \n \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;\n performer?: Partial;\n marker?: Partial;\n}\n\nexport const TagLink: FunctionComponent = (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 \n {title}\n \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 \n );\n }\n\n return (\n
\n {!data || loading ? : undefined}\n {!!error ? error.message : undefined}\n {renderStats()}\n\n

Notes

\n
\n        {`\n        This is still an early version, some things are still a work in progress.\n        `}\n      
\n
\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 = (props: IProps) => {\n const isNew = props.match.params.id === \"new\";\n\n // Editing state\n const [isEditing, setIsEditing] = useState(isNew);\n\n // Editing studio state\n const [image, setImage] = useState(undefined);\n const [name, setName] = useState(undefined);\n const [url, setUrl] = useState(undefined);\n\n // Studio state\n const [studio, setStudio] = useState>({});\n const [imagePreview, setImagePreview] = useState(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) {\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 ; }\n if (!!error) { return <>error...; }\n }\n\n function getStudioInput() {\n const input: Partial = {\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) {\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
\n
\n \n
\n
\n { setIsEditing(!isEditing); updateStudioEditState(studio); }}\n onSave={onSave}\n onDelete={onDelete}\n onAutoTag={onAutoTag}\n onImageChange={onImageChange}\n />\n

\n setName(value)}\n />\n

\n\n \n \n {TableUtils.renderEditableTextTableRow({title: \"URL\", value: url, isEditing, onChange: setUrl})}\n \n \n
\n
\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 = (props: IProps) => {\n const listData = ListHook.useList({\n filterMode: FilterMode.Studios,\n props,\n renderContent,\n });\n\n function renderContent(result: QueryHookResult, filter: ListFilterModel) {\n if (!result.data || !result.data.findStudios) { return; }\n if (filter.displayMode === DisplayMode.Grid) {\n return (\n
\n {result.data.findStudios.studios.map((studio) => ())}\n
\n );\n } else if (filter.displayMode === DisplayMode.List) {\n return

TODO

;\n } else if (filter.displayMode === DisplayMode.Wall) {\n return

TODO

;\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 = (props: IProps) => {\n const [tags, setTags] = useState([]);\n const [isLoading, setIsLoading] = useState(false);\n\n // Editing / New state\n const [editingTag, setEditingTag] = useState | undefined>(undefined);\n const [deletingTag, setDeletingTag] = useState | undefined>(undefined);\n const [name, setName] = useState(\"\");\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(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 = { name };\n if (!!editingTag) { (tagInput as Partial).id = editingTag.id; }\n return tagInput;\n }\n\n function getDeleteTagInput() {\n const tagInput: Partial = {};\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 setDeletingTag(undefined)}\n onConfirm={() => onDelete()}\n >\n

\n Are you sure you want to delete {deletingTag && deletingTag.name}?\n

\n \n );\n }\n\n if (!data || !data.allTags || isLoading) { return ; }\n if (!!error) { return <>{error.message}; }\n\n const tagElements = tags.map((tag) => {\n return (\n <>\n {renderDeleteAlert()}\n
\n setEditingTag(tag)}>{tag.name}\n
\n \n Scenes: {tag.scene_count}\n \n Markers: {tag.scene_marker_count}\n \n Total: {(tag.scene_count || 0) + (tag.scene_marker_count || 0)}\n \n
\n
\n \n );\n });\n return (\n
\n \n setEditingTag(undefined)}\n title={!!editingTag && !!editingTag.id ? \"Edit Tag\" : \"New Tag\"}\n >\n
\n \n setName(newValue.target.value)}\n value={name}\n />\n \n
\n
\n
\n \n
\n
\n \n\n {tagElements}\n
\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 = (props: IWallItemProps) => {\n const [videoPath, setVideoPath] = useState(undefined);\n const [previewPath, setPreviewPath] = useState(\"\");\n const [screenshotPath, setScreenshotPath] = useState(\"\");\n const [title, setTitle] = useState(\"\");\n const [tags, setTags] = useState([]);\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) {\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) => ({tag.name}));\n thisTags.unshift({props.sceneMarker.primary_tag.name});\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) => ({tag.name}));\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
\n debouncedOnMouseEnter.current()}\n onMouseMove={() => debouncedOnMouseEnter.current()}\n onMouseLeave={onMouseLeave}\n >\n onClick()} to={linkSrc}>\n \n previewNotFound()} />\n {showTextContainer ?\n
\n
\n {title}\n
\n {tags}\n
: undefined\n }\n \n
\n \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 = (props: IWallPanelProps) => {\n const [showOverlay, setShowOverlay] = useState(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 \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 \n );\n });\n }\n\n function render() {\n const overlayClassName = showOverlay ? \"visible\" : \"hidden\";\n return (\n <>\n
\n
\n {maybeRenderScenes()}\n {maybeRenderSceneMarkers()}\n
\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 = (props: IListFilterProps) => {\n let searchCallback: any;\n\n const [editingCriterion, setEditingCriterion] = useState(undefined);\n\n useEffect(() => {\n searchCallback = debounce((event: any) => {\n props.onChangeQuery(event.target.value);\n }, 500);\n });\n\n function onChangePageSize(event: SyntheticEvent) {\n const val = event!.currentTarget!.value;\n props.onChangePageSize(parseInt(val, 10));\n }\n\n function onChangeQuery(event: SyntheticEvent) {\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) {\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 \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 \n onChangeDisplayMode(option)}\n icon={getIcon(option)}\n />\n \n ));\n }\n\n function renderFilterTags() {\n return props.filter.criteria.map((criterion) => (\n onRemoveCriterionTag(criterion)}\n onClick={() => onClickCriterionTag(criterion)}\n >\n {criterion.getLabel()}\n \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 onSelectAll()} text=\"Select All\" />;\n }\n }\n\n function renderSelectNone() {\n if (props.onSelectNone) {\n return 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 \n \n {renderSortByOptions()}\n \n \n \n \n \n \n \n\n \n\n \n {renderDisplayModeOptions()}\n \n\n {maybeRenderZoom()}\n\n \n {renderMore()}\n \n
\n
\n {renderFilterTags()}\n
\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 = (props: ISceneProps) => {\n const [timestamp, setTimestamp] = useState(0);\n const [scene, setScene] = useState>({});\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 ;\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 \n \n \n } />\n }\n />\n {modifiedScene.performers.length > 0 ?\n }\n /> : undefined\n }\n {!!modifiedScene.gallery ?\n }\n /> : undefined\n }\n } />\n setScene(newScene)} \n onDelete={() => props.history.push(\"/scenes\")}\n />}\n />\n \n \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 {\n public value: Maybe;\n public originalValue: Maybe;\n public set: boolean = false;\n\n public setOriginalValue(v : Maybe) {\n this.originalValue = v;\n this.value = v;\n }\n\n public setValue(v : Maybe) {\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 = new ParserResult();\n public date: ParserResult = new ParserResult();\n\n public studio: ParserResult = new ParserResult();\n public studioId: ParserResult = new ParserResult();\n public tags: ParserResult = new ParserResult();\n public tagIds: ParserResult = new ParserResult();\n public performers: ParserResult = new ParserResult();\n public performerIds: ParserResult = 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) {\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([]);\n const [parserInput, setParserInput] = useState(initialParserInput());\n\n const [allTitleSet, setAllTitleSet] = useState(false);\n const [allDateSet, setAllDateSet] = useState(false);\n const [allPerformerSet, setAllPerformerSet] = useState(false);\n const [allTagSet, setAllTagSet] = useState(false);\n const [allStudioSet, setAllStudioSet] = useState(false);\n\n const [showFields, setShowFields] = useState>(initialShowFieldsState());\n \n const [totalItems, setTotalItems] = useState(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([\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\n onShowFieldsChanged: (fields : Map) => void\n }\n\n function ShowFieldsTree(props : IShowFieldsTreeProps) {\n const [displayFieldsExpanded, setDisplayFieldsExpanded] = useState();\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 \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(props.input.pattern);\n const [ignoreWords, setIgnoreWords] = useState(props.input.ignoreWords.join(\" \"));\n const [whitespaceCharacters, setWhitespaceCharacters] = useState(props.input.whitespaceCharacters);\n const [capitalizeTitle, setCapitalizeTitle] = useState(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();\n\n const renderParserRecipe: ItemRenderer = (input, { handleClick, modifiers }) => {\n if (!modifiers.matchesPredicate) {\n return null;\n }\n return (\n \n );\n };\n\n const parserRecipePredicate: ItemPredicate = (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();\n\n const renderParserField: ItemRenderer = (field, { handleClick, modifiers }) => {\n if (!modifiers.matchesPredicate) {\n return null;\n }\n return (\n \n );\n };\n\n const parserFieldPredicate: ItemPredicate = (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 addParserField(item)}\n itemRenderer={renderParserField}\n itemPredicate={parserFieldPredicate}\n >\n \n \n \n )\n }\n\n return (\n \n

Scene Filename Parser

\n onFindClicked(input)}\n />\n\n {isLoading ? : undefined}\n {renderTable()}\n
\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 = (props: IScenePlayerScrubberProps) => {\n const contentEl = useRef(null);\n const positionIndicatorEl = useRef(null);\n const scrubberSliderEl = useRef(null);\n const mouseDown = useRef(false);\n const lastMouseEvent = useRef(null);\n const startMouseEvent = useRef(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([]);\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(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 \n {marker.title}\n \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 \n {TextUtils.secondsToTimestamp(spriteItem.start)} - {TextUtils.secondsToTimestamp(spriteItem.end)}\n \n );\n });\n }\n\n return (\n
\n goBack()}><\n
\n
\n
\n
\n
\n
\n
\n {renderTags()}\n
\n {renderSprites()}\n
\n
\n
\n goForward()}>>\n
\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();\nconst InternalTagMultiSelect = MultiSelect.ofType();\nconst InternalStudioMultiSelect = MultiSelect.ofType();\n\ntype ValidTypes =\n GQL.AllPerformersForFilterAllPerformers |\n GQL.AllTagsForFilterAllTags |\n GQL.AllStudiosForFilterAllStudios;\n\ninterface IProps extends HTMLInputProps, Partial> {\n type: \"performers\" | \"studios\" | \"tags\";\n initialIds?: string[];\n onUpdate: (items: ValidTypes[]) => void;\n}\n\nexport const FilterMultiSelect: React.FunctionComponent = (props: IProps) => {\n let MultiSelectImpl = getMultiSelectImpl();\n let InternalMultiSelect = MultiSelectImpl.getInternalMultiSelect();\n const data = MultiSelectImpl.getData();\n \n const [selectedItems, setSelectedItems] = React.useState([]);\n const [items, setItems] = React.useState([]);\n const [newTagName, setNewTagName] = React.useState(\"\");\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 = { 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) {\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 \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) => MultiSelect;\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 = (item, itemProps) => {\n if (!itemProps.modifiers.matchesPredicate) { return null; }\n return (\n \n );\n };\n\n const filter: ItemPredicate = (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 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, filter: ListFilterModel, selectedIds: Set, zoomIndex: number) => JSX.Element | undefined;\n renderSelectedOptions?: (result: QueryHookResult, selectedIds: Set) => JSX.Element | undefined;\n}\n\nexport class ListHook {\n public static useList(options: IListHookOptions): IListHookData {\n const [filter, setFilter] = useState(new ListFilterModel(options.filterMode));\n const [selectedIds, setSelectedIds] = useState>(new Set());\n const [lastClickedId, setLastClickedId] = useState(undefined);\n const [totalCount, setTotalCount] = useState(0);\n const [zoomIndex, setZoomIndex] = useState(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;\n\n let getData: (filter : ListFilterModel) => QueryHookResult;\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 = 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 = 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 = new Set();\n setSelectedIds(newSelectedIds);\n setLastClickedId(undefined);\n }\n\n function onChangeZoom(newZoomIndex : number) {\n setZoomIndex(newZoomIndex);\n }\n\n const template = (\n
\n \n {options.renderSelectedOptions && selectedIds.size > 0 ? options.renderSelectedOptions(result, selectedIds) : undefined}\n {result.loading ? : undefined}\n {result.error ?

{result.error.message}

: undefined}\n {options.renderContent(result, filter, selectedIds, zoomIndex)}\n \n
\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]"] \ No newline at end of file +[{"/home/peroo/stash/ui/v2.5/src/App.tsx":"1","/home/peroo/stash/ui/v2.5/src/components/ErrorBoundary.tsx":"2","/home/peroo/stash/ui/v2.5/src/components/Galleries/Galleries.tsx":"3","/home/peroo/stash/ui/v2.5/src/components/Galleries/Gallery.tsx":"4","/home/peroo/stash/ui/v2.5/src/components/Galleries/GalleryList.tsx":"5","/home/peroo/stash/ui/v2.5/src/components/Galleries/GalleryViewer.tsx":"6","/home/peroo/stash/ui/v2.5/src/components/MainNavbar.tsx":"7","/home/peroo/stash/ui/v2.5/src/components/PageNotFound.tsx":"8","/home/peroo/stash/ui/v2.5/src/components/Settings/Settings.tsx":"9","/home/peroo/stash/ui/v2.5/src/components/Settings/SettingsAboutPanel.tsx":"10","/home/peroo/stash/ui/v2.5/src/components/Settings/SettingsConfigurationPanel.tsx":"11","/home/peroo/stash/ui/v2.5/src/components/Settings/SettingsInterfacePanel.tsx":"12","/home/peroo/stash/ui/v2.5/src/components/Settings/SettingsLogsPanel.tsx":"13","/home/peroo/stash/ui/v2.5/src/components/Settings/SettingsTasksPanel/GenerateButton.tsx":"14","/home/peroo/stash/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx":"15","/home/peroo/stash/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx":"16","/home/peroo/stash/ui/v2.5/src/components/Shared/DurationInput.tsx":"17","/home/peroo/stash/ui/v2.5/src/components/Shared/FolderSelect/FolderSelect.tsx":"18","/home/peroo/stash/ui/v2.5/src/components/Shared/TagLink.tsx":"19","/home/peroo/stash/ui/v2.5/src/components/Shared/Toast.tsx":"20","/home/peroo/stash/ui/v2.5/src/components/Stats.tsx":"21","/home/peroo/stash/ui/v2.5/src/components/Studios/StudioCard.tsx":"22","/home/peroo/stash/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx":"23","/home/peroo/stash/ui/v2.5/src/components/Studios/StudioList.tsx":"24","/home/peroo/stash/ui/v2.5/src/components/Studios/Studios.tsx":"25","/home/peroo/stash/ui/v2.5/src/components/Tags/TagList.tsx":"26","/home/peroo/stash/ui/v2.5/src/components/Tags/Tags.tsx":"27","/home/peroo/stash/ui/v2.5/src/components/Wall/WallItem.tsx":"28","/home/peroo/stash/ui/v2.5/src/components/Wall/WallPanel.tsx":"29","/home/peroo/stash/ui/v2.5/src/components/list/AddFilter.tsx":"30","/home/peroo/stash/ui/v2.5/src/components/list/ListFilter.tsx":"31","/home/peroo/stash/ui/v2.5/src/components/list/Pagination.tsx":"32","/home/peroo/stash/ui/v2.5/src/components/performers/PerformerCard.tsx":"33","/home/peroo/stash/ui/v2.5/src/components/performers/PerformerDetails/Performer.tsx":"34","/home/peroo/stash/ui/v2.5/src/components/performers/PerformerList.tsx":"35","/home/peroo/stash/ui/v2.5/src/components/performers/PerformerListTable.tsx":"36","/home/peroo/stash/ui/v2.5/src/components/performers/performers.tsx":"37","/home/peroo/stash/ui/v2.5/src/components/scenes/SceneCard.tsx":"38","/home/peroo/stash/ui/v2.5/src/components/scenes/SceneDetails/Scene.tsx":"39","/home/peroo/stash/ui/v2.5/src/components/scenes/SceneDetails/SceneDetailPanel.tsx":"40","/home/peroo/stash/ui/v2.5/src/components/scenes/SceneDetails/SceneEditPanel.tsx":"41","/home/peroo/stash/ui/v2.5/src/components/scenes/SceneDetails/SceneFileInfoPanel.tsx":"42","/home/peroo/stash/ui/v2.5/src/components/scenes/SceneDetails/SceneMarkersPanel.tsx":"43","/home/peroo/stash/ui/v2.5/src/components/scenes/SceneDetails/ScenePerformerPanel.tsx":"44","/home/peroo/stash/ui/v2.5/src/components/scenes/SceneFilenameParser.tsx":"45","/home/peroo/stash/ui/v2.5/src/components/scenes/SceneList.tsx":"46","/home/peroo/stash/ui/v2.5/src/components/scenes/SceneListTable.tsx":"47","/home/peroo/stash/ui/v2.5/src/components/scenes/SceneMarkerList.tsx":"48","/home/peroo/stash/ui/v2.5/src/components/scenes/ScenePlayer/ScenePlayer.tsx":"49","/home/peroo/stash/ui/v2.5/src/components/scenes/ScenePlayer/ScenePlayerScrubber.tsx":"50","/home/peroo/stash/ui/v2.5/src/components/scenes/SceneSelectedOptions.tsx":"51","/home/peroo/stash/ui/v2.5/src/components/scenes/helpers.tsx":"52","/home/peroo/stash/ui/v2.5/src/components/scenes/scenes.tsx":"53","/home/peroo/stash/ui/v2.5/src/components/select/FilterMultiSelect.tsx":"54","/home/peroo/stash/ui/v2.5/src/components/select/FilterSelect.tsx":"55","/home/peroo/stash/ui/v2.5/src/components/select/MarkerTitleSuggest.tsx":"56","/home/peroo/stash/ui/v2.5/src/components/select/ScrapePerformerSuggest.tsx":"57","/home/peroo/stash/ui/v2.5/src/components/select/ValidGalleriesSelect.tsx":"58","/home/peroo/stash/ui/v2.5/src/core/StashService.ts":"59","/home/peroo/stash/ui/v2.5/src/core/generated-graphql.tsx":"60","/home/peroo/stash/ui/v2.5/src/hooks/ListHook.tsx":"61","/home/peroo/stash/ui/v2.5/src/hooks/LocalForage.ts":"62","/home/peroo/stash/ui/v2.5/src/hooks/VideoHover.ts":"63","/home/peroo/stash/ui/v2.5/src/index.tsx":"64","/home/peroo/stash/ui/v2.5/src/models/base-props.ts":"65","/home/peroo/stash/ui/v2.5/src/models/index.ts":"66","/home/peroo/stash/ui/v2.5/src/models/list-filter/criteria/criterion.ts":"67","/home/peroo/stash/ui/v2.5/src/models/list-filter/criteria/favorite.ts":"68","/home/peroo/stash/ui/v2.5/src/models/list-filter/criteria/has-markers.ts":"69","/home/peroo/stash/ui/v2.5/src/models/list-filter/criteria/is-missing.ts":"70","/home/peroo/stash/ui/v2.5/src/models/list-filter/criteria/none.ts":"71","/home/peroo/stash/ui/v2.5/src/models/list-filter/criteria/performers.ts":"72","/home/peroo/stash/ui/v2.5/src/models/list-filter/criteria/rating.ts":"73","/home/peroo/stash/ui/v2.5/src/models/list-filter/criteria/resolution.ts":"74","/home/peroo/stash/ui/v2.5/src/models/list-filter/criteria/studios.ts":"75","/home/peroo/stash/ui/v2.5/src/models/list-filter/criteria/tags.ts":"76","/home/peroo/stash/ui/v2.5/src/models/list-filter/criteria/utils.ts":"77","/home/peroo/stash/ui/v2.5/src/models/list-filter/filter.ts":"78","/home/peroo/stash/ui/v2.5/src/models/list-filter/types.ts":"79","/home/peroo/stash/ui/v2.5/src/models/react-images.d.ts":"80","/home/peroo/stash/ui/v2.5/src/models/react-jw-player.d.ts":"81","/home/peroo/stash/ui/v2.5/src/models/types.ts":"82","/home/peroo/stash/ui/v2.5/src/react-app-env.d.ts":"83","/home/peroo/stash/ui/v2.5/src/serviceWorker.ts":"84","/home/peroo/stash/ui/v2.5/src/utils/color.ts":"85","/home/peroo/stash/ui/v2.5/src/utils/editabletext.tsx":"86","/home/peroo/stash/ui/v2.5/src/utils/errors.ts":"87","/home/peroo/stash/ui/v2.5/src/utils/image.tsx":"88","/home/peroo/stash/ui/v2.5/src/utils/navigation.ts":"89","/home/peroo/stash/ui/v2.5/src/utils/table.tsx":"90","/home/peroo/stash/ui/v2.5/src/utils/text.ts":"91","/home/peroo/stash/ui/v2.5/src/utils/toasts.ts":"92","/home/peroo/stash/ui/v2.5/src/utils/zoom.ts":"93"},{"size":1818,"mtime":1578432843260,"results":"94","hashOfConfig":"95"},{"size":769,"mtime":1577979127060,"results":"96","hashOfConfig":"95"},{"size":364,"mtime":1577979127060,"results":"97","hashOfConfig":"95"},{"size":1061,"mtime":1578153870063,"results":"98","hashOfConfig":"95"},{"size":1891,"mtime":1578153881712,"results":"99","hashOfConfig":"95"},{"size":1440,"mtime":1578153898526,"results":"100","hashOfConfig":"95"},{"size":2043,"mtime":1577994497113,"results":"101","hashOfConfig":"95"},{"size":154,"mtime":1577979127060,"results":"102","hashOfConfig":"95"},{"size":1885,"mtime":1577979127120,"results":"103","hashOfConfig":"95"},{"size":1109,"mtime":1578154008447,"results":"104","hashOfConfig":"95"},{"size":9581,"mtime":1578153404628,"results":"105","hashOfConfig":"95"},{"size":4412,"mtime":1578153807337,"results":"106","hashOfConfig":"95"},{"size":5105,"mtime":1578151803148,"results":"107","hashOfConfig":"95"},{"size":1767,"mtime":1577979127140,"results":"108","hashOfConfig":"95"},{"size":7437,"mtime":1578151882758,"results":"109","hashOfConfig":"95"},{"size":4140,"mtime":1578083340244,"results":"110","hashOfConfig":"95"},{"size":2898,"mtime":1578077797211,"results":"111","hashOfConfig":"95"},{"size":2923,"mtime":1578078846876,"results":"112","hashOfConfig":"95"},{"size":1174,"mtime":1578006626029,"results":"113","hashOfConfig":"95"},{"size":1319,"mtime":1578435714803,"results":"114","hashOfConfig":"95"},{"size":1957,"mtime":1577992677258,"results":"115","hashOfConfig":"95"},{"size":721,"mtime":1578076709401,"results":"116","hashOfConfig":"95"},{"size":5067,"mtime":1578076542562,"results":"117","hashOfConfig":"95"},{"size":1328,"mtime":1578153890219,"results":"118","hashOfConfig":"95"},{"size":364,"mtime":1577979127156,"results":"119","hashOfConfig":"95"},{"size":4672,"mtime":1578057682269,"results":"120","hashOfConfig":"95"},{"size":244,"mtime":1577979127172,"results":"121","hashOfConfig":"95"},{"size":4925,"mtime":1578153782630,"results":"122","hashOfConfig":"95"},{"size":2720,"mtime":1578153906690,"results":"123","hashOfConfig":"95"},{"size":7005,"mtime":1578299498166,"results":"124","hashOfConfig":"95"},{"size":7997,"mtime":1578172709206,"results":"125","hashOfConfig":"95"},{"size":3262,"mtime":1578165098978,"results":"126","hashOfConfig":"95"},{"size":1462,"mtime":1578083893006,"results":"127","hashOfConfig":"95"},{"size":13899,"mtime":1578086127588,"results":"128","hashOfConfig":"95"},{"size":2485,"mtime":1577979127184,"results":"129","hashOfConfig":"95"},{"size":2554,"mtime":1578084209211,"results":"130","hashOfConfig":"95"},{"size":397,"mtime":1577979127192,"results":"131","hashOfConfig":"95"},{"size":7168,"mtime":1578054332821,"results":"132","hashOfConfig":"95"},{"size":3737,"mtime":1578004207025,"results":"133","hashOfConfig":"95"},{"size":1381,"mtime":1578006480744,"results":"134","hashOfConfig":"95"},{"size":12685,"mtime":1578301620208,"results":"135","hashOfConfig":"95"},{"size":3133,"mtime":1577997627561,"results":"136","hashOfConfig":"95"},{"size":9231,"mtime":1578301845641,"results":"137","hashOfConfig":"95"},{"size":618,"mtime":1577979127196,"results":"138","hashOfConfig":"95"},{"size":29391,"mtime":1578301950830,"results":"139","hashOfConfig":"95"},{"size":3809,"mtime":1577994701125,"results":"140","hashOfConfig":"95"},{"size":3028,"mtime":1577996569786,"results":"141","hashOfConfig":"95"},{"size":2299,"mtime":1577979127196,"results":"142","hashOfConfig":"95"},{"size":5740,"mtime":1578054177135,"results":"143","hashOfConfig":"95"},{"size":10525,"mtime":1578154075579,"results":"144","hashOfConfig":"95"},{"size":8943,"mtime":1578302106743,"results":"145","hashOfConfig":"95"},{"size":905,"mtime":1577996960389,"results":"146","hashOfConfig":"95"},{"size":484,"mtime":1577979127227,"results":"147","hashOfConfig":"95"},{"size":6514,"mtime":1577979127227,"results":"148","hashOfConfig":"95"},{"size":5127,"mtime":1578435644103,"results":"149","hashOfConfig":"95"},{"size":2275,"mtime":1577979127227,"results":"150","hashOfConfig":"95"},{"size":2608,"mtime":1578153443601,"results":"151","hashOfConfig":"95"},{"size":2695,"mtime":1578151970904,"results":"152","hashOfConfig":"95"},{"size":16285,"mtime":1578426905076,"results":"153","hashOfConfig":"95"},{"size":73414,"mtime":1577979127231,"results":"154","hashOfConfig":"95"},{"size":11366,"mtime":1577979127231,"results":"155","hashOfConfig":"95"},{"size":1659,"mtime":1577979127231,"results":"156","hashOfConfig":"95"},{"size":2144,"mtime":1577979127231,"results":"157","hashOfConfig":"95"},{"size":737,"mtime":1577993631244,"results":"158","hashOfConfig":"95"},{"size":124,"mtime":1577979127235,"results":"159","hashOfConfig":"95"},{"size":55,"mtime":1577979127235,"results":"160","hashOfConfig":"95"},{"size":6822,"mtime":1578164766969,"results":"161","hashOfConfig":"95"},{"size":659,"mtime":1577979127255,"results":"162","hashOfConfig":"95"},{"size":664,"mtime":1577979127255,"results":"163","hashOfConfig":"95"},{"size":682,"mtime":1577979127255,"results":"164","hashOfConfig":"95"},{"size":566,"mtime":1577979127255,"results":"165","hashOfConfig":"95"},{"size":966,"mtime":1577979127255,"results":"166","hashOfConfig":"95"},{"size":1013,"mtime":1577979127255,"results":"167","hashOfConfig":"95"},{"size":694,"mtime":1577979127255,"results":"168","hashOfConfig":"95"},{"size":881,"mtime":1577979127255,"results":"169","hashOfConfig":"95"},{"size":1287,"mtime":1577979127255,"results":"170","hashOfConfig":"95"},{"size":1965,"mtime":1577979127255,"results":"171","hashOfConfig":"95"},{"size":12393,"mtime":1577979127255,"results":"172","hashOfConfig":"95"},{"size":278,"mtime":1577979127255,"results":"173","hashOfConfig":"95"},{"size":187,"mtime":1577979127255,"results":"174","hashOfConfig":"95"},{"size":200,"mtime":1577979127255,"results":"175","hashOfConfig":"95"},{"size":74,"mtime":1577979127255,"results":"176","hashOfConfig":"95"},{"size":40,"mtime":1577979127255,"results":"177","hashOfConfig":"95"},{"size":5216,"mtime":1577979127287,"results":"178","hashOfConfig":"95"},{"size":308,"mtime":1577979127287,"results":"179","hashOfConfig":"95"},{"size":1871,"mtime":1577979127287,"results":"180","hashOfConfig":"95"},{"size":529,"mtime":1577979127287,"results":"181","hashOfConfig":"95"},{"size":1049,"mtime":1578153858370,"results":"182","hashOfConfig":"95"},{"size":2396,"mtime":1577979127287,"results":"183","hashOfConfig":"95"},{"size":3507,"mtime":1578302319327,"results":"184","hashOfConfig":"95"},{"size":2241,"mtime":1577979127287,"results":"185","hashOfConfig":"95"},{"size":275,"mtime":1577979127287,"results":"186","hashOfConfig":"95"},{"size":123,"mtime":1577979127319,"results":"187","hashOfConfig":"95"},{"filePath":"188","messages":"189","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"1tsnfwq",{"filePath":"190","messages":"191","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"192","messages":"193","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"194","messages":"195","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":1,"source":"196"},{"filePath":"197","messages":"198","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"199","messages":"200","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"201","messages":"202","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"203","messages":"204","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"205","messages":"206","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":1,"source":"207"},{"filePath":"208","messages":"209","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"210","messages":"211","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":1,"source":"212"},{"filePath":"213","messages":"214","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":1,"source":"215"},{"filePath":"216","messages":"217","errorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":3,"source":"218"},{"filePath":"219","messages":"220","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"221","messages":"222","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"223","messages":"224","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"225","messages":"226","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"227","messages":"228","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"229","messages":"230","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"231","messages":"232","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"233","messages":"234","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"235","messages":"236","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"237","messages":"238","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"239","messages":"240","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"241","messages":"242","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"243","messages":"244","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"245","messages":"246","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"247","messages":"248","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"249","messages":"250","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"251","messages":"252","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"253","messages":"254","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"255","messages":"256","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"257","messages":"258","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"259","messages":"260","errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":2,"source":"261"},{"filePath":"262","messages":"263","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"264","messages":"265","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"266","messages":"267","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"268","messages":"269","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"270","messages":"271","errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":2,"source":"272"},{"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":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"279","messages":"280","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"281","messages":"282","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"283","messages":"284","errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":2,"source":"285"},{"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":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"292","messages":"293","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"294","messages":"295","errorCount":0,"warningCount":5,"fixableErrorCount":0,"fixableWarningCount":2,"source":"296"},{"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":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"303","messages":"304","errorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":1,"source":"305"},{"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":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"312","messages":"313","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"314","messages":"315","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"316","messages":"317","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"318","messages":"319","errorCount":0,"warningCount":4,"fixableErrorCount":0,"fixableWarningCount":4,"source":"320"},{"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":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"327","messages":"328","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"329","messages":"330","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"331","messages":"332","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"333","messages":"334","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"335","messages":"336","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"337","messages":"338","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"339","messages":"340","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"341","messages":"342","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"343","messages":"344","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"345","messages":"346","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"347","messages":"348","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"349","messages":"350","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"351","messages":"352","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"353","messages":"354","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"355","messages":"356","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"357","messages":"358","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"359","messages":"360","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"361","messages":"362","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"363","messages":"364","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"365","messages":"366","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"367","messages":"368","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"369","messages":"370","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"371","messages":"372","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"373","messages":"374","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"375","messages":"376","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"377","messages":"378","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"379","messages":"380","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"381"},{"filePath":"382","messages":"383","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"384","messages":"385","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/home/peroo/stash/ui/v2.5/src/App.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/ErrorBoundary.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/Galleries/Galleries.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/Galleries/Gallery.tsx",["386"],"import React, { useEffect, useState } from \"react\";\nimport { Spinner } from 'react-bootstrap';\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: React.FC = (props: IProps) => {\n const [gallery, setGallery] = useState>({});\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 ; }\n if (!!error) { return <>{error.message}; }\n return (\n
\n \n
\n );\n};\n","/home/peroo/stash/ui/v2.5/src/components/Galleries/GalleryList.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/Galleries/GalleryViewer.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/MainNavbar.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/PageNotFound.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/Settings/Settings.tsx",["387"],"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 = (props: IProps) => {\n const [tabId, setTabId] = useState(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 \n setTabId(newId as TabId)}\n defaultSelectedTabId={getTabId()}\n >\n } />\n } />\n } />\n } />\n } />\n \n \n );\n};\n","/home/peroo/stash/ui/v2.5/src/components/Settings/SettingsAboutPanel.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/Settings/SettingsConfigurationPanel.tsx",["388"],"import {\n AnchorButton,\n Button,\n Divider,\n FormGroup,\n InputGroup,\n Spinner,\n Checkbox,\n HTMLSelect,\n} from \"@blueprintjs/core\";\nimport React, { 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\nexport const SettingsConfigurationPanel: React.FC = () => {\n // Editing config state\n const [stashes, setStashes] = useState([]);\n const [databasePath, setDatabasePath] = useState(undefined);\n const [generatedPath, setGeneratedPath] = useState(undefined);\n const [maxTranscodeSize, setMaxTranscodeSize] = useState(undefined);\n const [maxStreamingTranscodeSize, setMaxStreamingTranscodeSize] = useState(undefined);\n const [username, setUsername] = useState(undefined);\n const [password, setPassword] = useState(undefined);\n const [logFile, setLogFile] = useState();\n const [logOut, setLogOut] = useState(true);\n const [logLevel, setLogLevel] = useState(\"Info\");\n const [logAccess, setLogAccess] = useState(true);\n const [excludes, setExcludes] = useState<(string)[]>([]);\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 excludes,\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 setExcludes(conf.general.excludes);\n }\n }, [data]);\n\n function onStashesChanged(directories: string[]) {\n setStashes(directories);\n }\n\n function excludeRegexChanged(idx: number, value: string) {\n const newExcludes = excludes.map((regex, i)=> {\n const ret = ( idx !== i ) ? regex : value ;\n return ret\n })\n setExcludes(newExcludes);\n }\n\n function excludeRemoveRegex(idx: number) {\n const newExcludes = excludes.filter((_regex, i) => i !== idx );\n\n setExcludes(newExcludes);\n }\n\n function excludeAddRegex() {\n const demo = \"sample\\\\.mp4$\"\n const newExcludes = excludes.concat(demo);\n\n setExcludes(newExcludes);\n }\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 ?

{error.message}

: undefined}\n {(!data || !data.configuration || loading) ? : undefined}\n

Library

\n \n \n \n \n \n \n \n \n setDatabasePath(e.target.value)} />\n \n\n \n setGeneratedPath(e.target.value)} />\n \n\n \n\n { (excludes) ? excludes.map((regexp, i) => {\n return(\n excludeRegexChanged(i, e.target.value)}\n rightElement={\n \n );\n};\n","/home/peroo/stash/ui/v2.5/src/components/Settings/SettingsInterfacePanel.tsx",["389"],"import {\n Button,\n Checkbox,\n Divider,\n FormGroup,\n Spinner,\n TextArea,\n NumericInput\n} from \"@blueprintjs/core\";\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 = () => {\n const config = StashService.useConfiguration();\n const [soundOnPreview, setSoundOnPreview] = useState();\n const [wallShowTitle, setWallShowTitle] = useState();\n const [maximumLoopDuration, setMaximumLoopDuration] = useState(0);\n const [autostartVideo, setAutostartVideo] = useState();\n const [showStudioAsText, setShowStudioAsText] = useState();\n const [css, setCSS] = useState();\n const [cssEnabled, setCSSEnabled] = useState();\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 ?

{config.error.message}

: undefined}\n {(!config.data || !config.data.configuration || config.loading) ? : undefined}\n

User Interface

\n \n setWallShowTitle(!wallShowTitle)}\n />\n setSoundOnPreview(!soundOnPreview)}\n />\n \n\n \n {\n setShowStudioAsText(!showStudioAsText)\n }}\n />\n \n \n \n {\n setAutostartVideo(!autostartVideo)\n }}\n />\n\n \n setMaximumLoopDuration(value)}\n min={0}\n minorStepSize={1}\n />\n \n \n\n \n {\n setCSSEnabled(!cssEnabled)\n }}\n />\n\n \n \n\n \n \n \n );\n};\n","/home/peroo/stash/ui/v2.5/src/components/Settings/SettingsLogsPanel.tsx",["390","391","392"],"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 = (props: IProps) => {\n const { data, error } = StashService.useLoggingSubscribe();\n const { data: existingData } = StashService.useLogs();\n \n const logEntries = useRef([]);\n const [logLevel, setLogLevel] = useState(\"Info\");\n const [filteredLogEntries, setFilteredLogEntries] = useState([]);\n const lastUpdate = useRef(0);\n const updateTimeout = useRef();\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 {props.logEntry.time} \n {level} \n {props.logEntry.message}\n
\n \n );\n }\n\n function maybeRenderError() {\n if (error) {\n return (\n <>\n Error connecting to log server: {error.message}
\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

Logs

\n
\n \n setLogLevel(event.target.value)}\n value={logLevel}\n />\n \n
\n
\n {maybeRenderError()}\n {filteredLogEntries.map((logEntry) =>\n \n )}\n
\n \n );\n};\n","/home/peroo/stash/ui/v2.5/src/components/Settings/SettingsTasksPanel/GenerateButton.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/Shared/DurationInput.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/Shared/FolderSelect/FolderSelect.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/Shared/TagLink.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/Shared/Toast.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/Stats.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/Studios/StudioCard.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/Studios/StudioList.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/Studios/Studios.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/Tags/TagList.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/Tags/Tags.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/Wall/WallItem.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/Wall/WallPanel.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/list/AddFilter.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/list/ListFilter.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/list/Pagination.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/performers/PerformerCard.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/performers/PerformerDetails/Performer.tsx",["393","394"],"import _ from \"lodash\";\nimport { Button, Form, Modal, Spinner, Table } from 'react-bootstrap';\nimport { FontAwesomeIcon } from \"@fortawesome/react-fontawesome\";\nimport React, { 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 { ScrapePerformerSuggest } from \"../../select/ScrapePerformerSuggest\";\nimport { DetailsEditNavbar } from \"../../Shared/DetailsEditNavbar\";\nimport { ToastUtils } from \"../../../utils/toasts\";\nimport { EditableTextUtils } from \"../../../utils/editabletext\";\nimport { ImageUtils } from \"../../../utils/image\";\n\ninterface IPerformerProps extends IBaseProps {}\n\nexport const Performer: React.FC = (props: IPerformerProps) => {\n const isNew = props.match.params.id === \"new\";\n\n // Editing state\n const [isEditing, setIsEditing] = useState(isNew);\n const [isDisplayingScraperDialog, setIsDisplayingScraperDialog] = useState(undefined);\n const [scrapePerformerDetails, setScrapePerformerDetails] = useState(undefined);\n\n // Editing performer state\n const [image, setImage] = useState(undefined);\n const [name, setName] = useState(undefined);\n const [aliases, setAliases] = useState(undefined);\n const [favorite, setFavorite] = useState(undefined);\n const [birthdate, setBirthdate] = useState(undefined);\n const [ethnicity, setEthnicity] = useState(undefined);\n const [country, setCountry] = useState(undefined);\n const [eyeColor, setEyeColor] = useState(undefined);\n const [height, setHeight] = useState(undefined);\n const [measurements, setMeasurements] = useState(undefined);\n const [fakeTits, setFakeTits] = useState(undefined);\n const [careerLength, setCareerLength] = useState(undefined);\n const [tattoos, setTattoos] = useState(undefined);\n const [piercings, setPiercings] = useState(undefined);\n const [url, setUrl] = useState(undefined);\n const [twitter, setTwitter] = useState(undefined);\n const [instagram, setInstagram] = useState(undefined);\n\n // Performer state\n const [performer, setPerformer] = useState>({});\n const [imagePreview, setImagePreview] = useState(undefined);\n\n // Network state\n const [isLoading, setIsLoading] = useState(false);\n\n const Scrapers = StashService.useListPerformerScrapers();\n const [queryableScrapers, setQueryableScrapers] = useState([]);\n\n const { data, error, loading } = StashService.useFindPerformer(props.match.params.id);\n const updatePerformer = StashService.usePerformerUpdate(getPerformerInput() as GQL.PerformerUpdateInput);\n const createPerformer = StashService.usePerformerCreate(getPerformerInput() as GQL.PerformerCreateInput);\n const deletePerformer = StashService.usePerformerDestroy(getPerformerInput() as GQL.PerformerDestroyInput);\n\n function updatePerformerEditState(state: Partial) {\n if ((state as GQL.PerformerDataFragment).favorite !== undefined) {\n setFavorite((state as GQL.PerformerDataFragment).favorite);\n }\n setName(state.name);\n setAliases(state.aliases);\n setBirthdate(state.birthdate);\n setEthnicity(state.ethnicity);\n setCountry(state.country);\n setEyeColor(state.eye_color);\n setHeight(state.height);\n setMeasurements(state.measurements);\n setFakeTits(state.fake_tits);\n setCareerLength(state.career_length);\n setTattoos(state.tattoos);\n setPiercings(state.piercings);\n setUrl(state.url);\n setTwitter(state.twitter);\n setInstagram(state.instagram);\n }\n\n useEffect(() => {\n setIsLoading(loading);\n if (!data || !data.findPerformer || !!error) { return; }\n setPerformer(data.findPerformer);\n }, [data]);\n\n useEffect(() => {\n setImagePreview(performer.image_path);\n setImage(undefined);\n updatePerformerEditState(performer);\n if (!isNew) {\n setIsEditing(false);\n }\n }, [performer]);\n\n function onImageLoad(this: FileReader) {\n setImagePreview(this.result as string);\n setImage(this.result as string);\n }\n\n ImageUtils.addPasteImageHook(onImageLoad);\n \n useEffect(() => {\n var newQueryableScrapers : GQL.ListPerformerScrapersListPerformerScrapers[] = [];\n\n if (!!Scrapers.data && Scrapers.data.listPerformerScrapers) {\n newQueryableScrapers = Scrapers.data.listPerformerScrapers.filter((s) => {\n return s.performer && s.performer.supported_scrapes.includes(GQL.ScrapeType.Name);\n });\n }\n\n setQueryableScrapers(newQueryableScrapers);\n\n }, [Scrapers.data]);\n\n if ((!isNew && !isEditing && (!data || !data.findPerformer)) || isLoading) {\n return ; \n }\n if (!!error) { return <>error...; }\n\n function getPerformerInput() {\n const performerInput: Partial = {\n name,\n aliases,\n favorite,\n birthdate,\n ethnicity,\n country,\n eye_color: eyeColor,\n height,\n measurements,\n fake_tits: fakeTits,\n career_length: careerLength,\n tattoos,\n piercings,\n url,\n twitter,\n instagram,\n image,\n };\n\n if (!isNew) {\n (performerInput as GQL.PerformerUpdateInput).id = props.match.params.id;\n }\n return performerInput;\n }\n\n async function onSave() {\n setIsLoading(true);\n try {\n if (!isNew) {\n const result = await updatePerformer();\n setPerformer(result.data.performerUpdate);\n } else {\n const result = await createPerformer();\n setPerformer(result.data.performerCreate);\n props.history.push(`/performers/${result.data.performerCreate.id}`);\n }\n } catch (e) {\n ErrorUtils.handle(e);\n }\n setIsLoading(false);\n }\n\n async function onDelete() {\n setIsLoading(true);\n try {\n await deletePerformer();\n } catch (e) {\n ErrorUtils.handle(e);\n }\n setIsLoading(false);\n \n // redirect to performers page\n props.history.push(`/performers`);\n }\n\n async function onAutoTag() {\n if (!performer || !performer.id) {\n return;\n }\n try {\n await StashService.queryMetadataAutoTag({ performers: [performer.id]});\n ToastUtils.success(\"Started auto tagging\");\n } catch (e) {\n ErrorUtils.handle(e);\n }\n }\n\n function onImageChange(event: React.FormEvent) {\n ImageUtils.onImageChange(event, onImageLoad);\n }\n\n function onDisplayFreeOnesDialog(scraper: GQL.ListPerformerScrapersListPerformerScrapers) {\n setIsDisplayingScraperDialog(scraper);\n }\n\n function getQueryScraperPerformerInput() {\n if (!scrapePerformerDetails) {\n return {};\n }\n\n let ret = _.clone(scrapePerformerDetails);\n delete ret.__typename;\n return ret as GQL.ScrapedPerformerInput;\n }\n\n async function onScrapePerformer() {\n setIsDisplayingScraperDialog(undefined);\n setIsLoading(true);\n try {\n if (!scrapePerformerDetails || !isDisplayingScraperDialog) { return; }\n const result = await StashService.queryScrapePerformer(isDisplayingScraperDialog.id, getQueryScraperPerformerInput());\n if (!result.data || !result.data.scrapePerformer) { return; }\n updatePerformerEditState(result.data.scrapePerformer);\n } catch (e) {\n ErrorUtils.handle(e);\n }\n setIsLoading(false);\n }\n\n async function onScrapePerformerURL() {\n if (!url) { return; }\n setIsLoading(true);\n try {\n const result = await StashService.queryScrapePerformerURL(url);\n if (!result.data || !result.data.scrapePerformerURL) { return; }\n updatePerformerEditState(result.data.scrapePerformerURL);\n } catch (e) {\n ErrorUtils.handle(e);\n } finally {\n setIsLoading(false);\n }\n }\n\n function renderEthnicity() {\n return TableUtils.renderHtmlSelect({\n title: \"Ethnicity\",\n value: ethnicity,\n isEditing,\n onChange: (value: string) => setEthnicity(value),\n selectOptions: [\"white\", \"black\", \"asian\", \"hispanic\"],\n });\n }\n\n function renderScraperDialog() {\n return (\n setIsDisplayingScraperDialog(undefined)}\n >\n \n Scrape\n \n \n
\n setScrapePerformerDetails(query)}\n />\n
\n
\n \n \n \n \n );\n }\n\n function urlScrapable(url: string) : boolean {\n return !!url && !!Scrapers.data && Scrapers.data.listPerformerScrapers && Scrapers.data.listPerformerScrapers.some((s) => {\n return !!s.performer && !!s.performer.urls && s.performer.urls.some((u) => { return url.includes(u); });\n });\n }\n\n function maybeRenderScrapeButton() {\n if (!url || !isEditing || !urlScrapable(url)) {\n return undefined;\n }\n return (\n \n )\n }\n\n function renderURLField() {\n return (\n \n \n URL \n {maybeRenderScrapeButton()}\n \n \n {EditableTextUtils.renderInputGroup({\n value: url, isEditing, onChange: setUrl, placeholder: \"URL\"\n })}\n \n \n );\n }\n\n return (\n <>\n {renderScraperDialog()}\n
\n
\n \"\"\n
\n
\n { setIsEditing(!isEditing); updatePerformerEditState(performer); }}\n onSave={onSave}\n onDelete={onDelete}\n onImageChange={onImageChange}\n scrapers={queryableScrapers}\n onDisplayScraperDialog={onDisplayFreeOnesDialog}\n onAutoTag={onAutoTag}\n />\n

\n { !isEditing\n ? {name}\n : setName(event.target.value)} />\n }\n

\n
\n \n Aliases:\n {EditableTextUtils.renderInputGroup({\n value: aliases, isEditing: isEditing, placeholder: \"Aliases\", onChange: setAliases\n })}\n \n
\n
\n Favorite:\n setFavorite(!favorite)}\n >\n \n \n
\n\n \n \n {TableUtils.renderInputGroup(\n {title: \"Birthdate (YYYY-MM-DD)\", value: birthdate, isEditing, onChange: setBirthdate})}\n {renderEthnicity()}\n {TableUtils.renderInputGroup(\n {title: \"Eye Color\", value: eyeColor, isEditing, onChange: setEyeColor})}\n {TableUtils.renderInputGroup(\n {title: \"Country\", value: country, isEditing, onChange: setCountry})}\n {TableUtils.renderInputGroup(\n {title: \"Height (CM)\", value: height, isEditing, onChange: setHeight})}\n {TableUtils.renderInputGroup(\n {title: \"Measurements\", value: measurements, isEditing, onChange: setMeasurements})}\n {TableUtils.renderInputGroup(\n {title: \"Fake Tits\", value: fakeTits, isEditing, onChange: setFakeTits})}\n {TableUtils.renderInputGroup(\n {title: \"Career Length\", value: careerLength, isEditing, onChange: setCareerLength})}\n {TableUtils.renderInputGroup(\n {title: \"Tattoos\", value: tattoos, isEditing, onChange: setTattoos})}\n {TableUtils.renderInputGroup(\n {title: \"Piercings\", value: piercings, isEditing, onChange: setPiercings})}\n {renderURLField()}\n {TableUtils.renderInputGroup(\n {title: \"Twitter\", value: twitter, isEditing, onChange: setTwitter})}\n {TableUtils.renderInputGroup(\n {title: \"Instagram\", value: instagram, isEditing, onChange: setInstagram})}\n \n
\n
\n
\n \n );\n};\n","/home/peroo/stash/ui/v2.5/src/components/performers/PerformerList.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/performers/PerformerListTable.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/performers/performers.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/scenes/SceneCard.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/scenes/SceneDetails/Scene.tsx",["395","396"],"import { Card, Spinner, Tab, Tabs } from 'react-bootstrap';\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 = (props: ISceneProps) => {\n const [timestamp, setTimestamp] = useState(0);\n const [autoplay, setAutoplay] = useState(false);\n const [scene, setScene] = useState>({});\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 if (queryParams.autoplay && typeof queryParams.autoplay === \"string\") {\n setAutoplay(queryParams.autoplay === \"true\");\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 ;\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 \n \n \n \n \n \n \n \n \n {modifiedScene.performers.length > 0 ?\n \n \n : ''\n }\n {!!modifiedScene.gallery ?\n \n \n : ''\n }\n \n \n \n \n setScene(newScene)} \n onDelete={() => props.history.push(\"/scenes\")}\n />\n \n \n \n \n );\n};\n","/home/peroo/stash/ui/v2.5/src/components/scenes/SceneDetails/SceneDetailPanel.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/scenes/SceneDetails/SceneEditPanel.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/scenes/SceneDetails/SceneFileInfoPanel.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/scenes/SceneDetails/SceneMarkersPanel.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/scenes/SceneDetails/ScenePerformerPanel.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/scenes/SceneFilenameParser.tsx",["397","398"],"import { Badge, Button, Card, Collapse, Dropdown, DropdownButton, Form, Table, Spinner } from 'react-bootstrap';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome'\nimport React, { 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 { FilterSelect, StudioSelect } from \"../select/FilterSelect\";\n \nclass ParserResult {\n public value: Maybe;\n public originalValue: Maybe;\n public set: boolean = false;\n\n public setOriginalValue(v : Maybe) {\n this.originalValue = v;\n this.value = v;\n }\n\n public setValue(v : Maybe) {\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 = new ParserResult();\n public date: ParserResult = new ParserResult();\n\n public studio: ParserResult = new ParserResult();\n public studioId: ParserResult = new ParserResult();\n public tags: ParserResult = new ParserResult();\n public tagIds: ParserResult = new ParserResult();\n public performers: ParserResult = new ParserResult();\n public performerIds: ParserResult = 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) {\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: React.FC = () => {\n const [parserResult, setParserResult] = useState([]);\n const [parserInput, setParserInput] = useState(initialParserInput());\n\n const [allTitleSet, setAllTitleSet] = useState(false);\n const [allDateSet, setAllDateSet] = useState(false);\n const [allPerformerSet, setAllPerformerSet] = useState(false);\n const [allTagSet, setAllTagSet] = useState(false);\n const [allStudioSet, setAllStudioSet] = useState(false);\n\n const [showFields, setShowFields] = useState>(initialShowFieldsState());\n \n const [totalItems, setTotalItems] = useState(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([\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 IShowFieldsProps {\n fields: Map\n onShowFieldsChanged: (fields : Map) => void\n }\n\n function ShowFields(props: IShowFieldsProps) {\n const [open, setOpen] = useState(false);\n\n function handleClick(label: string) {\n const copy = new Map(props.fields);\n copy.set(label, !props.fields.get(label));\n props.onShowFieldsChanged(copy);\n }\n\n const fieldRows = [...props.fields.entries()].map(([label, enabled]) => (\n
{handleClick(label)}}>\n \n {label}\n
\n ));\n\n return (\n
\n
setOpen(!open)}>\n \n Display fields\n
\n \n
\n {fieldRows}\n
\n
\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(props.input.pattern);\n const [ignoreWords, setIgnoreWords] = useState(props.input.ignoreWords.join(\" \"));\n const [whitespaceCharacters, setWhitespaceCharacters] = useState(props.input.whitespaceCharacters);\n const [capitalizeTitle, setCapitalizeTitle] = useState(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 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 validFields = [new ParserField(\"\", \"Wildcard\")].concat(ParserField.validFields);\n \n function addParserField(field: ParserField) {\n setPattern(pattern + field.getFieldPattern());\n }\n\n const PAGE_SIZE_OPTIONS = [\"20\", \"40\", \"60\", \"120\"];\n\n return (\n \n \n setPattern(newValue.target.value)}\n value={pattern}\n />\n \n { validFields.map(item => (\n addParserField(item)}>\n {item.field}{item.helperText}\n \n ))}\n \n
Use '\\\\' to escape literal {} characters
\n
\n\n \n Ignored words::\n setIgnoreWords(newValue.target.value)}\n value={ignoreWords}\n />\n
Matches with {\"{i}\"}
\n
\n \n \n
Title
\n Whitespace characters:\n setWhitespaceCharacters(newValue.target.value)}\n value={whitespaceCharacters}\n />\n \n Capitalize title\n setCapitalizeTitle(!capitalizeTitle)}\n />\n \n
These characters will be replaced with whitespace in the title
\n
\n \n {/* TODO - mapping stuff will go here */}\n\n \n \n { builtInRecipes.map(item => (\n setParserRecipe(item)}>\n {item.pattern}{item.description}\n \n ))}\n \n \n\n \n setShowFields(fields)}\n />\n \n\n \n \n onPageSizeChanged(parseInt(event.target.value))}\n defaultValue={props.input.pageSize}\n className=\"filter-item\"\n >\n { PAGE_SIZE_OPTIONS.map(val => ) }\n \n \n
\n );\n }\n\n interface ISceneParserFieldProps {\n parserResult : ParserResult\n className? : string\n fieldName : string\n onSetChanged : (set : boolean) => void\n onValueChanged : (value : any) => void\n originalParserResult? : ParserResult\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 \n {props.onSetChanged(!props.parserResult.set)}}\n />\n \n \n \n {props.renderOriginalInputField(props)}\n {props.renderNewInputField(props, (value) => maybeValueChanged(value))}\n \n \n \n );\n }\n\n function renderOriginalInputGroup(props: ISceneParserFieldProps) {\n var parserResult = props.originalParserResult || props.parserResult;\n\n return (\n \n );\n }\n\n interface IInputGroupWrapperProps {\n parserResult: ParserResult\n onChange : (event : any) => void\n className? : string\n }\n\n function InputGroupWrapper(props: IInputGroupWrapperProps) {\n return (\n props.onChange(event.target.value)}\n />\n );\n }\n \n function renderNewInputGroup(props : ISceneParserFieldProps, onChange : (value : any) => void) {\n return (\n {onChange(value)}}\n parserResult={props.parserResult}\n />\n );\n }\n\n interface HasName {\n name: string\n }\n\n function renderOriginalSelect(props : ISceneParserFieldProps) {\n const parserResult = props.originalParserResult || props.parserResult;\n\n const elements = parserResult.originalValue\n ? Array.isArray(parserResult.originalValue)\n ? parserResult.originalValue.map((el:HasName) => el.name)\n : parserResult.originalValue.name\n : [];\n\n return (\n
\n { elements.map((name:string) => {name}) }\n
\n );\n }\n\n function renderNewMultiSelect(type: \"performers\" | \"tags\", props : ISceneParserFieldProps, onChange : (value : any) => void) {\n return (\n {\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 onChange(items[0]?.id)}\n initialIds={props.parserResult.value ? [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, 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 \n {props.scene.filename}\n \n onTitleChanged(set, props.scene.title.value)}\n onValueChanged={(value) => onTitleChanged(props.scene.title.set, value)}\n renderOriginalInputField={renderOriginalInputGroup}\n renderNewInputField={renderNewInputGroup}\n />\n onDateChanged(set, props.scene.date.value)}\n onValueChanged={(value) => onDateChanged(props.scene.date.set, value)}\n renderOriginalInputField={renderOriginalInputGroup}\n renderNewInputField={renderNewInputGroup}\n />\n onPerformerIdsChanged(set, props.scene.performerIds.value)}\n onValueChanged={(value) => onPerformerIdsChanged(props.scene.performerIds.set, value)}\n renderOriginalInputField={renderOriginalSelect}\n renderNewInputField={renderNewPerformerSelect}\n />\n onTagIdsChanged(set, props.scene.tagIds.value)}\n onValueChanged={(value) => onTagIdsChanged(props.scene.tagIds.set, value)}\n renderOriginalInputField={renderOriginalSelect}\n renderNewInputField={renderNewTagSelect}\n />\n onStudioIdChanged(set, props.scene.studioId.value)}\n onValueChanged={(value) => onStudioIdChanged(props.scene.studioId.set, value)}\n renderOriginalInputField={renderOriginalSelect}\n renderNewInputField={renderNewStudioSelect}\n />\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 \n {onAllSet(!allSet)}}\n />\n \n {fieldName}\n \n )\n }\n\n function renderTable() {\n if (parserResult.length === 0) { return undefined; }\n\n return (\n <>\n
\n
\n \n \n \n \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 \n \n \n {parserResult.map((scene) => \n onChange(scene, changedScene)}/>\n )}\n \n
Filename
\n
\n onPageChanged(page)}\n />\n \n
\n \n )\n }\n\n return (\n \n

Scene Filename Parser

\n onFindClicked(input)}\n />\n\n {isLoading ? : undefined}\n {renderTable()}\n
\n );\n};\n \n","/home/peroo/stash/ui/v2.5/src/components/scenes/SceneList.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/scenes/SceneListTable.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/scenes/SceneMarkerList.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/scenes/ScenePlayer/ScenePlayer.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/scenes/ScenePlayer/ScenePlayerScrubber.tsx",["399","400","401","402","403"],"import axios from \"axios\";\nimport React, { CSSProperties, 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: React.FC = (props: IScenePlayerScrubberProps) => {\n const contentEl = useRef(null);\n const positionIndicatorEl = useRef(null);\n const scrubberSliderEl = useRef(null);\n const mouseDown = useRef(false);\n const lastMouseEvent = useRef(null);\n const startMouseEvent = useRef(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([]);\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(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 \n {marker.title}\n
\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 \n {TextUtils.secondsToTimestamp(spriteItem.start)} - {TextUtils.secondsToTimestamp(spriteItem.end)}\n
\n );\n });\n }\n\n return (\n
\n goBack()}><\n
\n
\n
\n
\n
\n
\n
\n {renderTags()}\n
\n {renderSprites()}\n
\n
\n
\n goForward()}>>\n
\n );\n};\n","/home/peroo/stash/ui/v2.5/src/components/scenes/SceneSelectedOptions.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/scenes/helpers.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/scenes/scenes.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/select/FilterMultiSelect.tsx",["404","405"],"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();\nconst InternalTagMultiSelect = MultiSelect.ofType();\nconst InternalStudioMultiSelect = MultiSelect.ofType();\n\ntype ValidTypes =\n GQL.AllPerformersForFilterAllPerformers |\n GQL.AllTagsForFilterAllTags |\n GQL.AllStudiosForFilterAllStudios;\n\ninterface IProps extends HTMLInputProps, Partial> {\n type: \"performers\" | \"studios\" | \"tags\";\n initialIds?: string[];\n onUpdate: (items: ValidTypes[]) => void;\n}\n\nexport const FilterMultiSelect: React.FunctionComponent = (props: IProps) => {\n let MultiSelectImpl = getMultiSelectImpl();\n let InternalMultiSelect = MultiSelectImpl.getInternalMultiSelect();\n const data = MultiSelectImpl.getData();\n \n const [selectedItems, setSelectedItems] = React.useState([]);\n const [items, setItems] = React.useState([]);\n const [newTagName, setNewTagName] = React.useState(\"\");\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 = { 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) {\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 \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) => MultiSelect;\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 \"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 = (item, itemProps) => {\n if (!itemProps.modifiers.matchesPredicate) { return null; }\n return (\n \n );\n };\n\n const filter: ItemPredicate = (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 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.5/src/components/select/FilterSelect.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/select/MarkerTitleSuggest.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/select/ScrapePerformerSuggest.tsx",[],"/home/peroo/stash/ui/v2.5/src/components/select/ValidGalleriesSelect.tsx",[],"/home/peroo/stash/ui/v2.5/src/core/StashService.ts",[],"/home/peroo/stash/ui/v2.5/src/core/generated-graphql.tsx",[],"/home/peroo/stash/ui/v2.5/src/hooks/ListHook.tsx",["406","407","408","409"],"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\ninterface IListHookOperation {\n text: string;\n onClick: (result: QueryHookResult, filter: ListFilterModel, selectedIds: Set) => void;\n}\n\nexport interface IListHookOptions {\n filterMode: FilterMode;\n props: IBaseProps;\n zoomable?: boolean;\n otherOperations?: IListHookOperation[];\n renderContent: (result: QueryHookResult, filter: ListFilterModel, selectedIds: Set, zoomIndex: number) => JSX.Element | undefined;\n renderSelectedOptions?: (result: QueryHookResult, selectedIds: Set) => JSX.Element | undefined;\n}\n\nexport class ListHook {\n public static useList(options: IListHookOptions): IListHookData {\n const [filter, setFilter] = useState(new ListFilterModel(options.filterMode));\n const [selectedIds, setSelectedIds] = useState>(new Set());\n const [lastClickedId, setLastClickedId] = useState(undefined);\n const [totalCount, setTotalCount] = useState(0);\n const [zoomIndex, setZoomIndex] = useState(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;\n\n let getData: (filter : ListFilterModel) => QueryHookResult;\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 = 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 = 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 = new Set();\n setSelectedIds(newSelectedIds);\n setLastClickedId(undefined);\n }\n\n function onChangeZoom(newZoomIndex : number) {\n setZoomIndex(newZoomIndex);\n }\n\n const otherOperations = options.otherOperations ? options.otherOperations.map((o) => {\n return {\n text: o.text,\n onClick: () => {\n o.onClick(result, filter, selectedIds);\n }\n }\n }) : undefined;\n\n const template = (\n
\n \n {options.renderSelectedOptions && selectedIds.size > 0 ? options.renderSelectedOptions(result, selectedIds) : undefined}\n {result.loading ? : undefined}\n {result.error ?

{result.error.message}

: undefined}\n {options.renderContent(result, filter, selectedIds, zoomIndex)}\n \n
\n );\n\n return { filter, template, options, onSelectChange };\n }\n}\n","/home/peroo/stash/ui/v2.5/src/hooks/LocalForage.ts",[],"/home/peroo/stash/ui/v2.5/src/hooks/VideoHover.ts",[],"/home/peroo/stash/ui/v2.5/src/index.tsx",[],"/home/peroo/stash/ui/v2.5/src/models/base-props.ts",[],"/home/peroo/stash/ui/v2.5/src/models/index.ts",[],"/home/peroo/stash/ui/v2.5/src/models/list-filter/criteria/criterion.ts",[],"/home/peroo/stash/ui/v2.5/src/models/list-filter/criteria/favorite.ts",[],"/home/peroo/stash/ui/v2.5/src/models/list-filter/criteria/has-markers.ts",[],"/home/peroo/stash/ui/v2.5/src/models/list-filter/criteria/is-missing.ts",[],"/home/peroo/stash/ui/v2.5/src/models/list-filter/criteria/none.ts",[],"/home/peroo/stash/ui/v2.5/src/models/list-filter/criteria/performers.ts",[],"/home/peroo/stash/ui/v2.5/src/models/list-filter/criteria/rating.ts",[],"/home/peroo/stash/ui/v2.5/src/models/list-filter/criteria/resolution.ts",[],"/home/peroo/stash/ui/v2.5/src/models/list-filter/criteria/studios.ts",[],"/home/peroo/stash/ui/v2.5/src/models/list-filter/criteria/tags.ts",[],"/home/peroo/stash/ui/v2.5/src/models/list-filter/criteria/utils.ts",[],"/home/peroo/stash/ui/v2.5/src/models/list-filter/filter.ts",[],"/home/peroo/stash/ui/v2.5/src/models/list-filter/types.ts",[],"/home/peroo/stash/ui/v2.5/src/models/react-images.d.ts",[],"/home/peroo/stash/ui/v2.5/src/models/react-jw-player.d.ts",[],"/home/peroo/stash/ui/v2.5/src/models/types.ts",[],"/home/peroo/stash/ui/v2.5/src/react-app-env.d.ts",[],"/home/peroo/stash/ui/v2.5/src/serviceWorker.ts",[],"/home/peroo/stash/ui/v2.5/src/utils/color.ts",[],"/home/peroo/stash/ui/v2.5/src/utils/editabletext.tsx",[],"/home/peroo/stash/ui/v2.5/src/utils/errors.ts",[],"/home/peroo/stash/ui/v2.5/src/utils/image.tsx",[],"/home/peroo/stash/ui/v2.5/src/utils/navigation.ts",[],"/home/peroo/stash/ui/v2.5/src/utils/table.tsx",[],"/home/peroo/stash/ui/v2.5/src/utils/text.ts",["410"],"export class TextUtils {\n\n public static truncate(value?: string, limit: number = 100, tail: string = \"...\"): string {\n if (!value) { return \"\"; }\n return value.length > limit ? value.substring(0, limit) + tail : value;\n }\n\n public static fileSize(bytes: number = 0, precision: number = 2): string {\n if (isNaN(parseFloat(String(bytes))) || !isFinite(bytes)) { return \"?\"; }\n\n let unit = 0;\n while ( bytes >= 1024 ) {\n bytes /= 1024;\n unit++;\n }\n\n return bytes.toFixed(+precision) + \" \" + this.units[unit];\n }\n\n public static secondsToTimestamp(seconds: number): string {\n let ret = new Date(seconds * 1000).toISOString().substr(11, 8);\n\n if (ret.startsWith(\"00\")) {\n // strip hours if under one hour\n ret = ret.substr(3);\n }\n if (ret.startsWith(\"0\")) {\n // for duration under a minute, leave one leading zero\n ret = ret.substr(1);\n }\n return ret;\n }\n\n public static fileNameFromPath(path: string): string {\n if (!!path === false) { return \"No File Name\"; }\n return path.replace(/^.*[\\\\\\/]/, \"\");\n }\n\n public static age(dateString?: string, fromDateString?: string): number {\n if (!dateString) { return 0; }\n\n const birthdate = new Date(dateString);\n const fromDate = !!fromDateString ? new Date(fromDateString) : new Date();\n\n let age = fromDate.getFullYear() - birthdate.getFullYear();\n if (birthdate.getMonth() > fromDate.getMonth() ||\n (birthdate.getMonth() >= fromDate.getMonth() && birthdate.getDay() > fromDate.getDay())) {\n age -= 1;\n }\n\n return age;\n }\n\n public static bitRate(bitrate: number) {\n const megabits = bitrate / 1000000;\n return `${megabits.toFixed(2)} megabits per second`;\n }\n\n public static resolution(height: number) {\n if (height >= 240 && height < 480) {\n return \"240p\";\n } else if (height >= 480 && height < 720) {\n return \"480p\";\n } else if (height >= 720 && height < 1080) {\n return \"720p\";\n } else if (height >= 1080 && height < 2160) {\n return \"1080p\";\n } else if (height >= 2160) {\n return \"4K\";\n } else {\n return undefined;\n }\n }\n\n private static units = [\n \"bytes\",\n \"kB\",\n \"MB\",\n \"GB\",\n \"TB\",\n \"PB\",\n ];\n}\n","/home/peroo/stash/ui/v2.5/src/utils/toasts.ts",[],"/home/peroo/stash/ui/v2.5/src/utils/zoom.ts",[],{"ruleId":"411","severity":1,"message":"412","line":20,"column":6,"nodeType":"413","endLine":20,"endColumn":12,"fix":"414"},{"ruleId":"411","severity":1,"message":"415","line":26,"column":6,"nodeType":"413","endLine":26,"endColumn":13,"fix":"416"},{"ruleId":"411","severity":1,"message":"417","line":67,"column":6,"nodeType":"413","endLine":67,"endColumn":12,"fix":"418"},{"ruleId":"411","severity":1,"message":"419","line":49,"column":6,"nodeType":"413","endLine":49,"endColumn":19,"fix":"420"},{"ruleId":"411","severity":1,"message":"421","line":92,"column":6,"nodeType":"413","endLine":92,"endColumn":12,"fix":"422"},{"ruleId":"411","severity":1,"message":"423","line":101,"column":6,"nodeType":"413","endLine":101,"endColumn":20,"fix":"424"},{"ruleId":"411","severity":1,"message":"425","line":117,"column":6,"nodeType":"413","endLine":117,"endColumn":16,"fix":"426"},{"ruleId":"411","severity":1,"message":"412","line":85,"column":6,"nodeType":"413","endLine":85,"endColumn":12,"fix":"427"},{"ruleId":"411","severity":1,"message":"428","line":94,"column":6,"nodeType":"413","endLine":94,"endColumn":17,"fix":"429"},{"ruleId":"411","severity":1,"message":"412","line":28,"column":6,"nodeType":"413","endLine":28,"endColumn":12,"fix":"430"},{"ruleId":"411","severity":1,"message":"431","line":30,"column":3,"nodeType":"432","endLine":30,"endColumn":12,"fix":"433"},{"ruleId":"411","severity":1,"message":"434","line":343,"column":6,"nodeType":"413","endLine":343,"endColumn":19,"fix":"435"},{"ruleId":"411","severity":1,"message":"436","line":448,"column":6,"nodeType":"413","endLine":448,"endColumn":20,"fix":"437"},{"ruleId":"438","severity":1,"message":"439","line":58,"column":10,"nodeType":"432","endLine":58,"endColumn":23},{"ruleId":"411","severity":1,"message":"440","line":67,"column":6,"nodeType":"413","endLine":67,"endColumn":19,"fix":"441"},{"ruleId":"411","severity":1,"message":"442","line":77,"column":6,"nodeType":"413","endLine":77,"endColumn":22,"fix":"443"},{"ruleId":"411","severity":1,"message":"444","line":91,"column":17,"nodeType":"432","endLine":91,"endColumn":24},{"ruleId":"411","severity":1,"message":"444","line":100,"column":17,"nodeType":"432","endLine":100,"endColumn":24},{"ruleId":"411","severity":1,"message":"445","line":40,"column":6,"nodeType":"413","endLine":40,"endColumn":12,"fix":"446"},{"ruleId":"447","severity":1,"message":"448","line":123,"column":9,"nodeType":"449","messageId":"450","endLine":123,"endColumn":53},{"ruleId":"411","severity":1,"message":"451","line":52,"column":8,"nodeType":"413","endLine":52,"endColumn":39,"fix":"452"},{"ruleId":"411","severity":1,"message":"453","line":108,"column":8,"nodeType":"413","endLine":108,"endColumn":21,"fix":"454"},{"ruleId":"411","severity":1,"message":"455","line":115,"column":8,"nodeType":"413","endLine":115,"endColumn":41,"fix":"456"},{"ruleId":"411","severity":1,"message":"451","line":122,"column":8,"nodeType":"413","endLine":122,"endColumn":20,"fix":"457"},{"ruleId":"458","severity":1,"message":"459","line":36,"column":32,"nodeType":"460","messageId":"461","endLine":36,"endColumn":33,"suggestions":"462"},"react-hooks/exhaustive-deps","React Hook useEffect has missing dependencies: 'error' and 'loading'. Either include them or remove the dependency array.","ArrayExpression",{"range":"463","text":"464"},"React Hook useEffect has a missing dependency: 'props.history'. Either include it or remove the dependency array.",{"range":"465","text":"466"},"React Hook useEffect has a missing dependency: 'error'. Either include it or remove the dependency array.",{"range":"467","text":"468"},"React Hook useEffect has a missing dependency: 'config.error'. Either include it or remove the dependency array.",{"range":"469","text":"470"},"React Hook useEffect has missing dependencies: 'filterByLogLevel', 'prependLogEntries', and 'updateFilteredEntries'. Either include them or remove the dependency array.",{"range":"471","text":"472"},"React Hook useEffect has missing dependencies: 'appendLogEntries' and 'updateFilteredEntries'. Either include them or remove the dependency array.",{"range":"473","text":"474"},"React Hook useEffect has a missing dependency: 'updateFilteredEntries'. Either include it or remove the dependency array.",{"range":"475","text":"476"},{"range":"477","text":"464"},"React Hook useEffect has a missing dependency: 'isNew'. Either include it or remove the dependency array.",{"range":"478","text":"479"},{"range":"480","text":"464"},"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.","Identifier",{"range":"481","text":"482"},"React Hook useEffect has a missing dependency: 'onFind'. Either include it or remove the dependency array.",{"range":"483","text":"484"},"React Hook useEffect has missing dependencies: 'allDateSet', 'allPerformerSet', 'allStudioSet', 'allTagSet', and 'allTitleSet'. Either include them or remove the dependency array.",{"range":"485","text":"486"},"@typescript-eslint/no-unused-vars","'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":"487","text":"488"},"React Hook useEffect has missing dependencies: 'props.scene.file.duration' and 'setPosition'. Either include them or remove the dependency array.",{"range":"489","text":"490"},"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":"491","text":"492"},"no-throw-literal","Expected an error object to be thrown.","ThrowStatement","object","React Hook useEffect has a missing dependency: 'filter'. Either include it or remove the dependency array.",{"range":"493","text":"494"},"React Hook useEffect has a missing dependency: 'getCount'. Either include it or remove the dependency array.",{"range":"495","text":"496"},"React Hook useEffect has missing dependencies: 'filter' and 'options.props.history'. Either include them or remove the dependency array.",{"range":"497","text":"498"},{"range":"499","text":"500"},"no-useless-escape","Unnecessary escape character: \\/.","Literal","unnecessaryEscape",["501","502"],[766,772],"[data, error, loading]",[983,990],"[props.history, tabId]",[2592,2598],"[data, error]",[1925,1938],"[config.data, config.error]",[2796,2802],"[data, filterByLogLevel, prependLogEntries, updateFilteredEntries]",[3020,3034],"[appendLogEntries, existingData, updateFilteredEntries]",[3422,3432],"[logLevel, updateFilteredEntries]",[4401,4407],[4596,4607],"[isNew, performer]",[1360,1366],[1772,1772],", [props.location.search, timestamp]",[9771,9784],"[onFind, parserInput]",[12750,12764],"[allDateSet, allPerformerSet, allStudioSet, allTagSet, allTitleSet, parserResult]",[2298,2311],"[fetchSpriteInfo, props.scene]",[2667,2683],"[props.position, props.scene.file.duration, setPosition]",[1668,1674],"[MultiSelectImpl, data]",[2305,2336],"[filter, options.props.location.search]",[5171,5184],"[getCount, result.data]",[5448,5481],"[result.data, filter.displayMode, options.props.history, filter]",[5658,5670],"[filter, totalCount]",{"messageId":"503","fix":"504","desc":"505"},{"messageId":"506","fix":"507","desc":"508"},"removeEscape",{"range":"509","text":"510"},"Remove the `\\`. This maintains the current functionality.","escapeBackslash",{"range":"511","text":"512"},"Replace the `\\` with `\\\\` to include the actual backslash character.",[1068,1069],"",[1068,1068],"\\"] \ No newline at end of file diff --git a/ui/v2.5/.eslintrc.json b/ui/v2.5/.eslintrc.json index 1c1fff5fd..fbbe988f8 100644 --- a/ui/v2.5/.eslintrc.json +++ b/ui/v2.5/.eslintrc.json @@ -1,8 +1,8 @@ { - "extends": [ - "react-app" - ], - "rules": { - "jsx-a11y/anchor-is-valid": "off" - } + "extends": [ + "react-app" + ], + "rules": { + "jsx-a11y/anchor-is-valid": "off" + } } diff --git a/ui/v2.5/package.json b/ui/v2.5/package.json index caf9e0bdb..6b69c1a54 100644 --- a/ui/v2.5/package.json +++ b/ui/v2.5/package.json @@ -3,8 +3,6 @@ "version": "0.1.0", "private": true, "dependencies": { - "@blueprintjs/core": "3.22.1", - "@blueprintjs/select": "3.11.2", "@fortawesome/fontawesome-svg-core": "^1.2.26", "@fortawesome/free-solid-svg-icons": "^5.12.0", "@fortawesome/react-fontawesome": "^0.1.8", @@ -12,11 +10,13 @@ "apollo-link-ws": "^1.0.19", "axios": "0.18.1", "bootstrap": "^4.4.1", + "classnames": "^2.2.6", "formik": "1.5.7", "graphql": "14.3.1", "localforage": "1.7.3", "lodash": "4.17.13", "node-sass": "4.12.0", + "normalize.css": "^8.0.1", "query-string": "6.5.0", "react": "~16.12.0", "react-apollo": "2.5.6", @@ -53,6 +53,7 @@ "not op_mini all" ], "devDependencies": { + "@types/classnames": "^2.2.9", "@types/jest": "24.0.13", "@types/lodash": "4.14.132", "@types/node": "11.13.0", diff --git a/ui/v2.5/src/App.tsx b/ui/v2.5/src/App.tsx index a5a457ea1..4b4b1a6e9 100755 --- a/ui/v2.5/src/App.tsx +++ b/ui/v2.5/src/App.tsx @@ -11,7 +11,7 @@ import { Stats } from "./components/Stats"; import Studios from "./components/Studios/Studios"; import Tags from "./components/Tags/Tags"; import { SceneFilenameParser } from "./components/scenes/SceneFilenameParser"; -import { ToastProvider } from './components/Shared/Toast'; +import { ToastProvider } from 'src/hooks/Toast'; import { library } from '@fortawesome/fontawesome-svg-core' import { fas } from '@fortawesome/free-solid-svg-icons' diff --git a/ui/v2.5/src/components/Galleries/Gallery.tsx b/ui/v2.5/src/components/Galleries/Gallery.tsx index f2167354e..a895549ce 100644 --- a/ui/v2.5/src/components/Galleries/Gallery.tsx +++ b/ui/v2.5/src/components/Galleries/Gallery.tsx @@ -1,26 +1,20 @@ -import React, { useEffect, useState } from "react"; +import React from "react"; import { Spinner } from 'react-bootstrap'; -import * as GQL from "../../core/generated-graphql"; -import { StashService } from "../../core/StashService"; -import { IBaseProps } from "../../models"; +import { useParams } from 'react-router-dom'; +import { StashService } from "src/core/StashService"; import { GalleryViewer } from "./GalleryViewer"; -interface IProps extends IBaseProps {} +export const Gallery: React.FC = () => { + const { id = '' } = useParams(); -export const Gallery: React.FC = (props: IProps) => { - const [gallery, setGallery] = useState>({}); - const [isLoading, setIsLoading] = useState(false); + const { data, error, loading } = StashService.useFindGallery(id); + const gallery = data?.findGallery; - const { data, error, loading } = StashService.useFindGallery(props.match.params.id); + if (loading || !gallery) + return ; + if (error) + return
{error.message}
; - useEffect(() => { - setIsLoading(loading); - if (!data || !data.findGallery || !!error) { return; } - setGallery(data.findGallery); - }, [data]); - - if (!data || !data.findGallery || isLoading) { return ; } - if (!!error) { return <>{error.message}; } return (
diff --git a/ui/v2.5/src/components/Galleries/GalleryList.tsx b/ui/v2.5/src/components/Galleries/GalleryList.tsx index 29a1b905f..49f306307 100644 --- a/ui/v2.5/src/components/Galleries/GalleryList.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryList.tsx @@ -2,11 +2,11 @@ import React from "react"; import { Table } from 'react-bootstrap'; import { QueryHookResult } from "react-apollo-hooks"; import { Link } from "react-router-dom"; -import { FindGalleriesQuery, FindGalleriesVariables } from "../../core/generated-graphql"; -import { ListHook } from "../../hooks/ListHook"; -import { IBaseProps } from "../../models/base-props"; -import { ListFilterModel } from "../../models/list-filter/filter"; -import { DisplayMode, FilterMode } from "../../models/list-filter/types"; +import { FindGalleriesQuery, FindGalleriesVariables } from "src/core/generated-graphql"; +import { ListHook } from "src/hooks"; +import { IBaseProps } from "src/models/base-props"; +import { ListFilterModel } from "src/models/list-filter/filter"; +import { DisplayMode, FilterMode } from "src/models/list-filter/types"; interface IProps extends IBaseProps {} diff --git a/ui/v2.5/src/components/Galleries/GalleryViewer.tsx b/ui/v2.5/src/components/Galleries/GalleryViewer.tsx index 55bc2e99d..25d96ef4b 100644 --- a/ui/v2.5/src/components/Galleries/GalleryViewer.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryViewer.tsx @@ -1,17 +1,17 @@ import React, { FunctionComponent, useState } from "react"; import Lightbox from "react-images"; import Gallery from "react-photo-gallery"; -import * as GQL from "../../core/generated-graphql"; +import * as GQL from "src/core/generated-graphql"; interface IProps { gallery: GQL.GalleryDataFragment; } -export const GalleryViewer: FunctionComponent = (props: IProps) => { +export const GalleryViewer: FunctionComponent = ({ gallery }) => { const [currentImage, setCurrentImage] = useState(0); const [lightboxIsOpen, setLightboxIsOpen] = useState(false); - function openLightbox(event: any, obj: any) { + function openLightbox(_event: React.MouseEvent, obj: {index: number}) { setCurrentImage(obj.index); setLightboxIsOpen(true); } @@ -26,8 +26,8 @@ export const GalleryViewer: FunctionComponent = (props: IProps) => { setCurrentImage(currentImage + 1); } - const photos = props.gallery.files.map((file) => ({src: file.path || "", caption: file.name})); - const thumbs = props.gallery.files.map((file) => ({src: `${file.path}?thumb=true` || "", width: 1, height: 1})); + const photos = gallery.files.map((file) => ({src: file.path || "", caption: file.name})); + const thumbs = gallery.files.map((file) => ({src: `${file.path}?thumb=true` || "", width: 1, height: 1})); return (
diff --git a/ui/v2.5/src/components/MainNavbar.tsx b/ui/v2.5/src/components/MainNavbar.tsx index 410d69b28..802b78671 100644 --- a/ui/v2.5/src/components/MainNavbar.tsx +++ b/ui/v2.5/src/components/MainNavbar.tsx @@ -71,6 +71,7 @@ export const MainNavbar: React.FC = () => { activeClassName="active" exact={true} to={i.href} + key={i.href} > + + + )) : '' + } -

- + rel="noopener noreferrer" + target="_blank" + > + Regexps of files/paths to exclude from Scan and add to Clean + +

- - - - - -

Video

- - setMaxTranscodeSize(translateQuality(event.target.value))} - value={resolutionToString(maxTranscodeSize)} - /> - - - setMaxStreamingTranscodeSize(translateQuality(event.target.value))} - value={resolutionToString(maxStreamingTranscodeSize)} - /> - -
- + + - +
+ + +

Video

+ + Maximum transcode size + + onChange={(event:React.FormEvent) => setMaxTranscodeSize(translateQuality(event.currentTarget.value))} + value={resolutionToString(maxTranscodeSize)} + > + { transcodeQualities.map(q => ())} + + Maximum size for generated transcodes + + + Maximum streaming transcode size + ) => setMaxStreamingTranscodeSize(translateQuality(event.currentTarget.value))} + value={resolutionToString(maxStreamingTranscodeSize)} + > + { transcodeQualities.map(q => ())} + + Maximum size for transcoded streams + +
+ +
+ +

Authentication

- - setUsername(e.target.value)} /> - - - setPassword(e.target.value)} /> - -
+ + Username + ) => setUsername(e.currentTarget.value)} /> + Username to access Stash. Leave blank to disable user authentication + + + Password + ) => setPassword(e.currentTarget.value)} /> + Password to access Stash. Leave blank to disable user authentication + + + +
-

Logging

- - setLogFile(e.target.value)} /> - + + Log file + ) => setLogFile(e.currentTarget.value)} /> + Path to the file to output logging to. Blank to disable file logging. Requires restart. + - - + setLogOut(!logOut)} /> - + Logs to the terminal in addition to a file. Always true if file logging is disabled. Requires restart. + - - setLogLevel(event.target.value)} + + Log Level + ) => setLogLevel(event.currentTarget.value)} value={logLevel} - /> - + > + { ["Debug", "Info", "Warning", "Error"].map(o => ()) } + + - - + setLogAccess(!logAccess)} /> - + Logs http access to the terminal. Requires restart. + - - +
+ + ); }; diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel.tsx index 49013a3d8..7f665ddba 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel.tsx @@ -1,20 +1,10 @@ -import { - Button, - Checkbox, - Divider, - FormGroup, - Spinner, - TextArea, - NumericInput -} from "@blueprintjs/core"; -import React, { FunctionComponent, useEffect, useState } from "react"; -import { StashService } from "../../core/StashService"; -import { ErrorUtils } from "../../utils/errors"; -import { ToastUtils } from "../../utils/toasts"; +import React, { useEffect, useState } from "react"; +import { Button, Form, Spinner } from 'react-bootstrap'; +import { StashService } from "src/core/StashService"; +import { useToast } from 'src/hooks'; -interface IProps {} - -export const SettingsInterfacePanel: FunctionComponent = () => { +export const SettingsInterfacePanel: React.FC = () => { + const Toast = useToast(); const config = StashService.useConfiguration(); const [soundOnPreview, setSoundOnPreview] = useState(); const [wallShowTitle, setWallShowTitle] = useState(); @@ -35,66 +25,63 @@ export const SettingsInterfacePanel: FunctionComponent = () => { }); useEffect(() => { - if (!config.data || !config.data.configuration || !!config.error) { return; } - if (!!config.data.configuration.interface) { - let iCfg = config.data.configuration.interface; - setSoundOnPreview(iCfg.soundOnPreview !== undefined ? iCfg.soundOnPreview : true); - setWallShowTitle(iCfg.wallShowTitle !== undefined ? iCfg.wallShowTitle : true); - setMaximumLoopDuration(iCfg.maximumLoopDuration || 0); - setAutostartVideo(iCfg.autostartVideo !== undefined ? iCfg.autostartVideo : false); - setShowStudioAsText(iCfg.showStudioAsText !== undefined ? iCfg.showStudioAsText : false); - setCSS(config.data.configuration.interface.css || ""); - setCSSEnabled(config.data.configuration.interface.cssEnabled || false); - } - }, [config.data]); + if (config.error) + return; + + const iCfg = config?.data?.configuration?.interface; + setSoundOnPreview(iCfg?.soundOnPreview ?? true); + setWallShowTitle(iCfg?.wallShowTitle ?? true); + setMaximumLoopDuration(iCfg?.maximumLoopDuration ?? 0); + setAutostartVideo(iCfg?.autostartVideo ?? false); + setShowStudioAsText(iCfg?.showStudioAsText ?? false); + setCSS(iCfg?.css ?? ""); + setCSSEnabled(iCfg?.cssEnabled ?? false); + }, [config]); async function onSave() { try { const result = await updateInterfaceConfig(); console.log(result); - ToastUtils.success("Updated config"); + Toast.success({ content: "Updated config" }); } catch (e) { - ErrorUtils.handle(e); + Toast.error(e); } } return ( <> - {!!config.error ?

{config.error.message}

: undefined} - {(!config.data || !config.data.configuration || config.loading) ? : undefined} + {config.error ?

{config.error.message}

: ''} + {(!config?.data?.configuration || config.loading) ? : ''}

User Interface

- - + Scene / Marker Wall + setWallShowTitle(!wallShowTitle)} /> - setSoundOnPreview(!soundOnPreview)} /> - + Configuration for wall items + - - + Scene List + { setShowStudioAsText(!showStudioAsText) }} /> - - - - + + + Scene Player + { @@ -102,25 +89,22 @@ export const SettingsInterfacePanel: FunctionComponent = () => { }} /> - - + Maximum loop duration + setMaximumLoopDuration(value)} + defaultValue={maximumLoopDuration} + onChange={(event:React.FormEvent) => setMaximumLoopDuration(Number.parseInt(event.currentTarget.value) ?? 0)} min={0} - minorStepSize={1} + step={1} /> - - + Maximum scene duration - in seconds - where scene player will loop the video - 0 to disable + + - - + Custom CSS + { @@ -128,16 +112,17 @@ export const SettingsInterfacePanel: FunctionComponent = () => { }} /> - - + + Page must be reloaded for changes to take effect. + - - +
+ ); }; diff --git a/ui/v2.5/src/components/Settings/SettingsLogsPanel.tsx b/ui/v2.5/src/components/Settings/SettingsLogsPanel.tsx index 946485f5b..8270a25ea 100644 --- a/ui/v2.5/src/components/Settings/SettingsLogsPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsLogsPanel.tsx @@ -1,13 +1,9 @@ -import { - H4, FormGroup, HTMLSelect, -} from "@blueprintjs/core"; -import React, { FunctionComponent, useState, useEffect, useRef } from "react"; -import * as GQL from "../../core/generated-graphql"; -import { StashService } from "../../core/StashService"; +import React, { useState, useEffect, useRef } from "react"; +import { Form, Col } from 'react-bootstrap'; +import * as GQL from "src/core/generated-graphql"; +import { StashService } from "src/core/StashService"; -interface IProps {} - -function convertTime(logEntry : GQL.LogEntryDataFragment) { +function convertTime(logEntry: GQL.LogEntryDataFragment) { function pad(val : number) { var ret = val.toString(); if (val <= 9) { @@ -44,17 +40,17 @@ class LogEntry { } } -export const SettingsLogsPanel: FunctionComponent = (props: IProps) => { +export const SettingsLogsPanel: React.FC = () => { const { data, error } = StashService.useLoggingSubscribe(); const { data: existingData } = StashService.useLogs(); - + const logEntries = useRef([]); const [logLevel, setLogLevel] = useState("Info"); const [filteredLogEntries, setFilteredLogEntries] = useState([]); const lastUpdate = useRef(0); const updateTimeout = useRef(); - // maximum number of log entries to display. Subsequent entries will truncate + // maximum number of log entries to display. Subsequent entries will truncate // the list, dropping off the oldest entries first. const MAX_LOG_ENTRIES = 200; @@ -83,7 +79,7 @@ export const SettingsLogsPanel: FunctionComponent = (props: IProps) => { // filter subscribed data as it comes in, otherwise we'll end up // truncating stuff that wasn't filtered out convertedData = convertedData.filter(filterByLogLevel) - + // put newest entries at the top convertedData.reverse(); prependLogEntries(convertedData); @@ -167,16 +163,21 @@ export const SettingsLogsPanel: FunctionComponent = (props: IProps) => { return ( <> -

Logs

-
- - setLogLevel(event.target.value)} - value={logLevel} - /> - -
+

Logs

+ + + Log Level + + + setLogLevel(event.currentTarget.value)} + > + { logLevels.map(level => ()) } + + +
{maybeRenderError()} {filteredLogEntries.map((logEntry) => diff --git a/ui/v2.5/src/components/Settings/SettingsTasksPanel/GenerateButton.tsx b/ui/v2.5/src/components/Settings/SettingsTasksPanel/GenerateButton.tsx index a37bea831..21fcbe4e8 100644 --- a/ui/v2.5/src/components/Settings/SettingsTasksPanel/GenerateButton.tsx +++ b/ui/v2.5/src/components/Settings/SettingsTasksPanel/GenerateButton.tsx @@ -1,53 +1,32 @@ -import { - Button, - Checkbox, - FormGroup, -} from "@blueprintjs/core"; -import React, { FunctionComponent, useState } from "react"; -import { StashService } from "../../../core/StashService"; -import { ErrorUtils } from "../../../utils/errors"; -import { ToastUtils } from "../../../utils/toasts"; +import React, { useState } from "react"; +import { Button, Form } from 'react-bootstrap'; +import { StashService } from "src/core/StashService"; +import { useToast } from 'src/hooks'; -interface IProps {} - -export const GenerateButton: FunctionComponent = () => { - const [sprites, setSprites] = useState(true); - const [previews, setPreviews] = useState(true); - const [markers, setMarkers] = useState(true); - const [transcodes, setTranscodes] = useState(true); +export const GenerateButton: React.FC = () => { + const Toast = useToast(); + const [sprites, setSprites] = useState(true); + const [previews, setPreviews] = useState(true); + const [markers, setMarkers] = useState(true); + const [transcodes, setTranscodes] = useState(true); async function onGenerate() { try { await StashService.queryMetadataGenerate({sprites, previews, markers, transcodes}); - ToastUtils.success("Started generating"); + Toast.success({ content: "Started generating" }); } catch (e) { - ErrorUtils.handle(e); + Toast.error(e); } } return ( - - setSprites(!sprites)} /> - setPreviews(!previews)} - /> - setMarkers(!markers)} - /> - setTranscodes(!transcodes)} - /> - + Generate supporting image, sprite, video, vtt and other files. + ); }; diff --git a/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx b/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx index c0e056a92..a1a3735e0 100644 --- a/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx @@ -1,24 +1,18 @@ -import { - Alert, - Button, - Checkbox, - Divider, - FormGroup, - ProgressBar, -} from "@blueprintjs/core"; import React, { useState, useEffect } from "react"; -import { StashService } from "../../../core/StashService"; -import { ErrorUtils } from "../../../utils/errors"; -import { ToastUtils } from "../../../utils/toasts"; -import { GenerateButton } from "./GenerateButton"; +import { Button, Form, ProgressBar } from 'react-bootstrap'; import { Link } from "react-router-dom"; +import { StashService } from "src/core/StashService"; +import { useToast } from 'src/hooks'; +import { Modal } from 'src/components/Shared'; +import { GenerateButton } from "./GenerateButton"; export const SettingsTasksPanel: React.FC = () => { + const Toast = useToast(); const [isImportAlertOpen, setIsImportAlertOpen] = useState(false); const [isCleanAlertOpen, setIsCleanAlertOpen] = useState(false); const [useFileMetadata, setUseFileMetadata] = useState(false); const [status, setStatus] = useState(""); - const [progress, setProgress] = useState(undefined); + const [progress, setProgress] = useState(0); const [autoTagPerformers, setAutoTagPerformers] = useState(true); const [autoTagStudios, setAutoTagStudios] = useState(true); @@ -53,7 +47,7 @@ export const SettingsTasksPanel: React.FC = () => { setStatus(statusToText(jobStatus.data.jobStatus.status)); var newProgress = jobStatus.data.jobStatus.progress; if (newProgress < 0) { - setProgress(undefined); + setProgress(0); } else { setProgress(newProgress); } @@ -65,7 +59,7 @@ export const SettingsTasksPanel: React.FC = () => { setStatus(statusToText(metadataUpdate.data.metadataUpdate.status)); var newProgress = metadataUpdate.data.metadataUpdate.progress; if (newProgress < 0) { - setProgress(undefined); + setProgress(0); } else { setProgress(newProgress); } @@ -79,20 +73,17 @@ export const SettingsTasksPanel: React.FC = () => { function renderImportAlert() { return ( - setIsImportAlertOpen(false)} - onConfirm={() => onImport()} + setIsImportAlertOpen(false) }} >

Are you sure you want to import? This will delete the database and re-import from your exported metadata.

-
+ ); } @@ -103,31 +94,28 @@ export const SettingsTasksPanel: React.FC = () => { function renderCleanAlert() { return ( - setIsCleanAlertOpen(false)} - onConfirm={() => onClean()} + setIsCleanAlertOpen(false) }} >

Are you sure you want to Clean? This will delete db information and generated content for all scenes that are no longer found in the filesystem.

-
+ ); } async function onScan() { try { await StashService.queryMetadataScan({useFileMetadata: useFileMetadata}); - ToastUtils.success("Started scan"); + Toast.success({ content: "Started scan" }); jobStatus.refetch(); } catch (e) { - ErrorUtils.handle(e); + Toast.error(e); } } @@ -143,10 +131,10 @@ export const SettingsTasksPanel: React.FC = () => { async function onAutoTag() { try { await StashService.queryMetadataAutoTag(getAutoTagInput()); - ToastUtils.success("Started auto tagging"); + Toast.success({ content: "Started auto tagging" }); jobStatus.refetch(); } catch (e) { - ErrorUtils.handle(e); + Toast.error(e); } } @@ -156,21 +144,19 @@ export const SettingsTasksPanel: React.FC = () => { } return ( - <> - - + ); } function renderJobStatus() { return ( <> - +
Status: {status}
- {!!status && status !== "Idle" ? : undefined} -
+ { status !== "Idle" ? : '' } + {maybeRenderStop()} ); @@ -185,83 +171,70 @@ export const SettingsTasksPanel: React.FC = () => { {renderJobStatus()} - +

Library

- - + setUseFileMetadata(!useFileMetadata)} /> - + Scan for new content and add it to the database. + - +

Auto Tagging

- - + setAutoTagPerformers(!autoTagPerformers)} /> - setAutoTagStudios(!autoTagStudios)} /> - setAutoTagTags(!autoTagTags)} /> - + Auto-tag content based on filenames. + - - - Scene Filename Parser - - - + + + + +

Generated Content

- - + Check for missing files and remove them from the database. This is a destructive action. + + +

Metadata

- - + Export the database content into JSON format. + - - + Import from exported JSON. This is a destructive action. + ); }; diff --git a/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx b/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx index b5cf5abd6..5891d668a 100644 --- a/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx +++ b/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx @@ -1,8 +1,8 @@ import { Button, Form, Modal, Nav, Navbar, OverlayTrigger, Popover } from 'react-bootstrap'; import React, { useState } from "react"; import { Link } from "react-router-dom"; -import * as GQL from "../../core/generated-graphql"; -import { NavigationUtils } from "../../utils/navigation"; +import * as GQL from "src/core/generated-graphql"; +import { NavUtils } from "src/utils"; interface IProps { performer?: Partial; @@ -92,10 +92,10 @@ export const DetailsEditNavbar: React.FC = (props: IProps) => { function renderScenesButton() { if (props.isEditing) { return; } let linkSrc: string = "#"; - if (!!props.performer) { - linkSrc = NavigationUtils.makePerformerScenesUrl(props.performer); - } else if (!!props.studio) { - linkSrc = NavigationUtils.makeStudioScenesUrl(props.studio); + if (props.performer) { + linkSrc = NavUtils.makePerformerScenesUrl(props.performer); + } else if (props.studio) { + linkSrc = NavUtils.makeStudioScenesUrl(props.studio); } return ( diff --git a/ui/v2.5/src/components/Shared/DurationInput.tsx b/ui/v2.5/src/components/Shared/DurationInput.tsx index 08663d983..9f3c6a885 100644 --- a/ui/v2.5/src/components/Shared/DurationInput.tsx +++ b/ui/v2.5/src/components/Shared/DurationInput.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from "react"; import { Button, ButtonGroup, InputGroup, Form } from 'react-bootstrap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { TextUtils } from "../../utils/text"; +import { TextUtils } from "src/utils"; interface IProps { disabled?: boolean @@ -35,7 +35,7 @@ export const DurationInput: React.FC = (props: IProps) => { if (!v) { return 0; } - + let splits = v.split(":"); if (splits.length > 3) { @@ -122,9 +122,7 @@ export const DurationInput: React.FC = (props: IProps) => { onChange={(e : any) => setValue(e.target.value)} onBlur={() => props.onValueChange(stringToSeconds(value))} placeholder="hh:mm:ss" - > - {renderButtons()} - + /> {maybeRenderReset()} diff --git a/ui/v2.5/src/components/Shared/FolderSelect/FolderSelect.tsx b/ui/v2.5/src/components/Shared/FolderSelect/FolderSelect.tsx index 56936a43d..532cfba64 100644 --- a/ui/v2.5/src/components/Shared/FolderSelect/FolderSelect.tsx +++ b/ui/v2.5/src/components/Shared/FolderSelect/FolderSelect.tsx @@ -1,6 +1,6 @@ -import { Button, InputGroup, Form, Modal, Spinner } from 'react-bootstrap'; import React, { useEffect, useState } from "react"; -import { StashService } from "../../../core/StashService"; +import { Button, InputGroup, Form, Modal, Spinner } from 'react-bootstrap'; +import { StashService } from "src/core/StashService"; interface IProps { directories: string[]; @@ -17,7 +17,7 @@ export const FolderSelect: React.FC = (props: IProps) => { setSelectedDirectories(props.directories); }, [props.directories]); - const selectableDirectories:string[] = data && data.directories && !error ? StashService.nullToUndefined(data.directories) : []; + const selectableDirectories:string[] = data?.directories ?? []; function onSelectDirectory() { selectedDirectories.push(currentDirectory); @@ -55,7 +55,7 @@ export const FolderSelect: React.FC = (props: IProps) => { {(!data || !data.directories || loading) ? : undefined} - + /> {selectableDirectories.map((path) => { return
setCurrentDirectory(path)}>{path}
; @@ -71,14 +71,14 @@ export const FolderSelect: React.FC = (props: IProps) => { return ( <> - {!!error ?

{error.message}

: undefined} + {error ?

{error.message}

: ''} {renderDialog()} {selectedDirectories.map((path) => { return ; })} - + ); diff --git a/ui/v2.5/src/components/Shared/Icon.tsx b/ui/v2.5/src/components/Shared/Icon.tsx new file mode 100644 index 000000000..f59238c54 --- /dev/null +++ b/ui/v2.5/src/components/Shared/Icon.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { IconName } from '@fortawesome/fontawesome-svg-core'; + +interface IIcon { + icon: IconName; + className?: string; + color?: string; +} + +const Icon: React.FC = ({ icon, className, color }) => ( + +); + +export default Icon; diff --git a/ui/v2.5/src/components/Shared/Modal.tsx b/ui/v2.5/src/components/Shared/Modal.tsx new file mode 100644 index 000000000..c1c489f5f --- /dev/null +++ b/ui/v2.5/src/components/Shared/Modal.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { Button, Modal } from 'react-bootstrap'; +import { Icon } from 'src/components/Shared'; +import { IconName } from '@fortawesome/fontawesome-svg-core'; + +interface IButton { + text?: string; + variant?: 'danger'|'primary'; + onClick?: () => void; +} + +interface IModal { + show: boolean; + onHide?: () => void; + header?: string; + icon?: IconName; + cancel?: IButton; + accept?: IButton; +} + +const ModalComponent: React.FC = ({ children, show, icon, header, cancel, accept, onHide }) => (( + + + { icon ? : '' } + { header ?? '' } + + {children} + +
+ { cancel + ? + : '' + } + { } +
+
+
+)); + +export default ModalComponent; diff --git a/ui/v2.5/src/components/select/FilterSelect.tsx b/ui/v2.5/src/components/Shared/Select.tsx similarity index 51% rename from ui/v2.5/src/components/select/FilterSelect.tsx rename to ui/v2.5/src/components/Shared/Select.tsx index b3d07eb1a..96b5f1279 100644 --- a/ui/v2.5/src/components/select/FilterSelect.tsx +++ b/ui/v2.5/src/components/Shared/Select.tsx @@ -1,11 +1,11 @@ -import React, { useState } from "react"; +import React, { useState, useCallback } from "react"; import Select, { ValueType } from 'react-select'; import CreatableSelect from 'react-select/creatable'; +import { debounce } from 'lodash'; -import { ErrorUtils } from "../../utils/errors"; -import * as GQL from "../../core/generated-graphql"; -import { StashService } from "../../core/StashService"; -import useToast from '../Shared/Toast'; +import * as GQL from "src/core/generated-graphql"; +import { StashService } from "src/core/StashService"; +import { useToast } from 'src/hooks'; type ValidTypes = GQL.AllPerformersForFilterAllPerformers | @@ -14,7 +14,7 @@ type ValidTypes = type Option = { value:string, label:string }; interface ITypeProps { - type: 'performers' | 'studios' | 'tags'; + type?: 'performers' | 'studios' | 'tags'; } interface IFilterProps { initialIds: string[]; @@ -32,8 +32,66 @@ interface ISelectProps { isLoading: boolean; onChange: (item: ValueType