diff --git a/graphql/documents/data/config.graphql b/graphql/documents/data/config.graphql index aa068d4fc..cd7dc6835 100644 --- a/graphql/documents/data/config.graphql +++ b/graphql/documents/data/config.graphql @@ -40,6 +40,7 @@ fragment ConfigGeneralData on ConfigGeneralResult { } fragment ConfigInterfaceData on ConfigInterfaceResult { + menuItems soundOnPreview wallShowTitle wallPlayback diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index 08197f151..5967a2625 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -149,6 +149,8 @@ type ConfigGeneralResult { } input ConfigInterfaceInput { + """Ordered list of items that should be shown in the menu""" + menuItems: [String!] """Enable sound on mouseover previews""" soundOnPreview: Boolean """Show title and tags in wall view""" @@ -164,10 +166,13 @@ input ConfigInterfaceInput { """Custom CSS""" css: String cssEnabled: Boolean + """Interface language""" language: String } type ConfigInterfaceResult { + """Ordered list of items that should be shown in the menu""" + menuItems: [String!] """Enable sound on mouseover previews""" soundOnPreview: Boolean """Show title and tags in wall view""" @@ -178,7 +183,7 @@ type ConfigInterfaceResult { maximumLoopDuration: Int """If true, video will autostart on load in the scene player""" autostartVideo: Boolean - """If true, studio overlays will be shown as text instead of logo images""" + """If true, studio overlays will be shown as text instead of logo images""" showStudioAsText: Boolean """Custom CSS""" css: String diff --git a/pkg/api/resolver_mutation_configure.go b/pkg/api/resolver_mutation_configure.go index 9b106dd04..94a9c5210 100644 --- a/pkg/api/resolver_mutation_configure.go +++ b/pkg/api/resolver_mutation_configure.go @@ -171,6 +171,10 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co } func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.ConfigInterfaceInput) (*models.ConfigInterfaceResult, error) { + if input.MenuItems != nil { + config.Set(config.MenuItems, input.MenuItems) + } + if input.SoundOnPreview != nil { config.Set(config.SoundOnPreview, *input.SoundOnPreview) } diff --git a/pkg/api/resolver_query_configuration.go b/pkg/api/resolver_query_configuration.go index c9682a24f..c848375ea 100644 --- a/pkg/api/resolver_query_configuration.go +++ b/pkg/api/resolver_query_configuration.go @@ -77,6 +77,7 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult { } func makeConfigInterfaceResult() *models.ConfigInterfaceResult { + menuItems := config.GetMenuItems() soundOnPreview := config.GetSoundOnPreview() wallShowTitle := config.GetWallShowTitle() wallPlayback := config.GetWallPlayback() @@ -88,6 +89,7 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult { language := config.GetLanguage() return &models.ConfigInterfaceResult{ + MenuItems: menuItems, SoundOnPreview: &soundOnPreview, WallShowTitle: &wallShowTitle, WallPlayback: &wallPlayback, diff --git a/pkg/manager/config/config.go b/pkg/manager/config/config.go index a993094d5..1a6fe405a 100644 --- a/pkg/manager/config/config.go +++ b/pkg/manager/config/config.go @@ -101,6 +101,10 @@ const Language = "language" const CustomServedFolders = "custom_served_folders" // Interface options +const MenuItems = "menu_items" + +var defaultMenuItems = []string{"scenes", "images", "movies", "markers", "galleries", "performers", "studios", "tags"} + const SoundOnPreview = "sound_on_preview" const WallShowTitle = "wall_show_title" const MaximumLoopDuration = "maximum_loop_duration" @@ -449,6 +453,13 @@ func GetCustomServedFolders() URLMap { } // Interface options +func GetMenuItems() []string { + if viper.IsSet(MenuItems) { + return viper.GetStringSlice(MenuItems) + } + return defaultMenuItems +} + func GetSoundOnPreview() bool { viper.SetDefault(SoundOnPreview, true) return viper.GetBool(SoundOnPreview) diff --git a/ui/v2.5/src/components/Changelog/versions/v050.md b/ui/v2.5/src/components/Changelog/versions/v050.md index 4964b8295..8037c6441 100644 --- a/ui/v2.5/src/components/Changelog/versions/v050.md +++ b/ui/v2.5/src/components/Changelog/versions/v050.md @@ -1,3 +1,6 @@ +### ✨ New Features +* Allow configuration of visible navbar items. + ### 🎨 Improvements * Add gallery tabs to performer and studio pages. * Add gallery scrapers to scraper page. diff --git a/ui/v2.5/src/components/MainNavbar.tsx b/ui/v2.5/src/components/MainNavbar.tsx index 45d1debc2..4246f8f8d 100644 --- a/ui/v2.5/src/components/MainNavbar.tsx +++ b/ui/v2.5/src/components/MainNavbar.tsx @@ -5,7 +5,7 @@ import { MessageDescriptor, useIntl, } from "react-intl"; -import { Nav, Navbar, Button } from "react-bootstrap"; +import { Nav, Navbar, Button, Fade } from "react-bootstrap"; import { IconName } from "@fortawesome/fontawesome-svg-core"; import { LinkContainer } from "react-router-bootstrap"; import { Link, NavLink, useLocation, useHistory } from "react-router-dom"; @@ -14,8 +14,10 @@ import Mousetrap from "mousetrap"; import { SessionUtils } from "src/utils"; import { Icon } from "src/components/Shared"; import { Manual } from "./Help/Manual"; +import { useConfiguration } from "../core/StashService"; interface IMenuItem { + name: string; message: MessageDescriptor; href: string; icon: IconName; @@ -60,55 +62,79 @@ const messages = defineMessages({ }, }); -const menuItems: IMenuItem[] = [ +const allMenuItems: IMenuItem[] = [ { - icon: "play-circle", + name: "scenes", message: messages.scenes, href: "/scenes", + icon: "play-circle", }, { - icon: "image", + name: "images", message: messages.images, href: "/images", + icon: "image", }, { + name: "movies", + message: messages.movies, href: "/movies", icon: "film", - message: messages.movies, }, { + name: "markers", + message: messages.markers, href: "/scenes/markers", icon: "map-marker-alt", - message: messages.markers, }, { + name: "galleries", + message: messages.galleries, href: "/galleries", icon: "image", - message: messages.galleries, }, { + name: "performers", + message: messages.performers, href: "/performers", icon: "user", - message: messages.performers, }, { + name: "studios", + message: messages.studios, href: "/studios", icon: "video", - message: messages.studios, }, { + name: "tags", + message: messages.tags, href: "/tags", icon: "tag", - message: messages.tags, }, ]; export const MainNavbar: React.FC = () => { const history = useHistory(); const location = useLocation(); + const { data: config, loading } = useConfiguration(); + + // Show all menu items by default, unless config says otherwise + const [menuItems, setMenuItems] = useState(allMenuItems); + const [expanded, setExpanded] = useState(false); const [showManual, setShowManual] = useState(false); + useEffect(() => { + const iCfg = config?.configuration?.interface; + if (iCfg?.menuItems) { + setMenuItems( + allMenuItems.filter((menuItem) => + iCfg.menuItems!.includes(menuItem.name) + ) + ); + } + }, [config]); + // react-bootstrap typing bug // eslint-disable-next-line @typescript-eslint/no-explicit-any const navbarRef = useRef(); @@ -239,18 +265,20 @@ export const MainNavbar: React.FC = () => { - + + +