Add grid grow and service search in search bar
This commit is contained in:
Thomas Camlong
2022-11-30 10:14:31 +09:00
committed by GitHub
28 changed files with 139 additions and 48 deletions

View File

@@ -1,2 +1,2 @@
export const REPO_URL = 'ajnart/homarr'; export const REPO_URL = 'ajnart/homarr';
export const CURRENT_VERSION = 'v0.10.6'; export const CURRENT_VERSION = 'v0.10.7';

View File

@@ -1,6 +1,6 @@
{ {
"name": "homarr", "name": "homarr",
"version": "0.10.6", "version": "0.10.7",
"description": "Homarr - A homepage for your server.", "description": "Homarr - A homepage for your server.",
"license": "MIT", "license": "MIT",
"repository": { "repository": {

View File

@@ -10,5 +10,6 @@
}, },
"credits": { "credits": {
"madeWithLove": "Lavet med ❤️ af @" "madeWithLove": "Lavet med ❤️ af @"
} },
"grow": ""
} }

View File

@@ -10,5 +10,6 @@
}, },
"credits": { "credits": {
"madeWithLove": "Gemacht mit ❤️ von @" "madeWithLove": "Gemacht mit ❤️ von @"
} },
"grow": ""
} }

View File

@@ -10,5 +10,6 @@
}, },
"credits": { "credits": {
"madeWithLove": "Made with ❤️ by @" "madeWithLove": "Made with ❤️ by @"
} },
"grow": "Grow grid (take all space)"
} }

View File

@@ -10,5 +10,6 @@
}, },
"credits": { "credits": {
"madeWithLove": "Hecho con ❤️ por @" "madeWithLove": "Hecho con ❤️ por @"
} },
"grow": "Aumentar cuadrícula (toma todo el espacio)"
} }

View File

@@ -10,5 +10,6 @@
}, },
"credits": { "credits": {
"madeWithLove": "Fait avec ❤️ par @" "madeWithLove": "Fait avec ❤️ par @"
} },
"grow": ""
} }

View File

@@ -10,5 +10,6 @@
}, },
"credits": { "credits": {
"madeWithLove": "נעשה ב- ❤️ ע״י @" "madeWithLove": "נעשה ב- ❤️ ע״י @"
} },
"grow": ""
} }

View File

@@ -10,5 +10,6 @@
}, },
"credits": { "credits": {
"madeWithLove": "Realizzato con ❤️ da @" "madeWithLove": "Realizzato con ❤️ da @"
} },
"grow": ""
} }

View File

@@ -10,5 +10,6 @@
}, },
"credits": { "credits": {
"madeWithLove": "で作った❤️ by @さん" "madeWithLove": "で作った❤️ by @さん"
} },
"grow": ""
} }

View File

@@ -10,5 +10,6 @@
}, },
"credits": { "credits": {
"madeWithLove": "Made with ❤️ by @" "madeWithLove": "Made with ❤️ by @"
} },
"grow": ""
} }

View File

@@ -10,5 +10,6 @@
}, },
"credits": { "credits": {
"madeWithLove": "Maded wif ❤️ by @" "madeWithLove": "Maded wif ❤️ by @"
} },
"grow": ""
} }

View File

@@ -10,5 +10,6 @@
}, },
"credits": { "credits": {
"madeWithLove": "Gemaakt met ❤️ door @" "madeWithLove": "Gemaakt met ❤️ door @"
} },
"grow": ""
} }

View File

@@ -10,5 +10,6 @@
}, },
"credits": { "credits": {
"madeWithLove": "Wykonane z ❤️ przez @" "madeWithLove": "Wykonane z ❤️ przez @"
} },
"grow": ""
} }

View File

@@ -10,5 +10,6 @@
}, },
"credits": { "credits": {
"madeWithLove": "Feito com ❤️ por @" "madeWithLove": "Feito com ❤️ por @"
} },
"grow": ""
} }

View File

@@ -10,5 +10,6 @@
}, },
"credits": { "credits": {
"madeWithLove": "Сделано с ❤️ по @." "madeWithLove": "Сделано с ❤️ по @."
} },
"grow": ""
} }

View File

@@ -10,5 +10,6 @@
}, },
"credits": { "credits": {
"madeWithLove": "Narejeno s ❤️ od @" "madeWithLove": "Narejeno s ❤️ od @"
} },
"grow": ""
} }

View File

@@ -6,25 +6,25 @@
"input": { "input": {
"placeholder": "Sök på webben..." "placeholder": "Sök på webben..."
}, },
"switched-to": "", "switched-to": "Växlade till",
"searchEngines": { "searchEngines": {
"search": { "search": {
"name": "", "name": "Webb",
"description": "" "description": "Sök med din sökmotor (definierad i inställningar)"
}, },
"youtube": { "youtube": {
"name": "", "name": "YouTube",
"description": "" "description": "Sök på YouTube"
}, },
"torrents": { "torrents": {
"name": "", "name": "Torrents",
"description": "" "description": "Sök torrents"
}, },
"overseerr": { "overseerr": {
"name": "Overseerr", "name": "Overseerr",
"description": "" "description": "Sök efter filmer och TV-program med Overseerr (modulen måste vara aktiverad)"
} }
}, },
"tip": "", "tip": "Du kan välja sökfältet med kortkommandot ",
"switchedSearchEngine": "" "switchedSearchEngine": "Växlade till att söka med {{searchEngine}}"
} }

View File

@@ -10,5 +10,6 @@
}, },
"credits": { "credits": {
"madeWithLove": "Gjort med ❤️ av @" "madeWithLove": "Gjort med ❤️ av @"
} },
"grow": "Växande rutnät (ta allt utrymme)"
} }

View File

@@ -10,5 +10,6 @@
}, },
"credits": { "credits": {
"madeWithLove": "Зроблено з ❤️ by @" "madeWithLove": "Зроблено з ❤️ by @"
} },
"grow": ""
} }

View File

@@ -10,5 +10,6 @@
}, },
"credits": { "credits": {
"madeWithLove": "From @ with ❤️" "madeWithLove": "From @ with ❤️"
} },
"grow": ""
} }

View File

@@ -10,5 +10,6 @@
}, },
"credits": { "credits": {
"madeWithLove": "用❤️创造,出品于" "madeWithLove": "用❤️创造,出品于"
} },
"grow": ""
} }

View File

@@ -95,7 +95,7 @@ const AppShelf = (props: any) => {
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
> >
<SortableContext items={config.services}> <SortableContext items={config.services}>
<Grid gutter="lg" align="center"> <Grid gutter="lg" grow={config.settings.grow}>
{filtered.map((service) => ( {filtered.map((service) => (
<Grid.Col key={service.id} span="content"> <Grid.Col key={service.id} span="content">
<SortableItem service={service} key={service.id} id={service.id}> <SortableItem service={service} key={service.id} id={service.id}>
@@ -143,7 +143,14 @@ const AppShelf = (props: any) => {
value={idx.toString()} value={idx.toString()}
> >
<Accordion.Control> <Accordion.Control>
<Title order={5}>{category}</Title> <Title
order={5}
style={{
minWidth: 0,
}}
>
{category}
</Title>
</Accordion.Control> </Accordion.Control>
<Accordion.Panel>{getItems(category)}</Accordion.Panel> <Accordion.Panel>{getItems(category)}</Accordion.Panel>
</Accordion.Item> </Accordion.Item>

View File

@@ -6,6 +6,7 @@ import { ColorSelector } from './ColorSelector';
import { OpacitySelector } from './OpacitySelector'; import { OpacitySelector } from './OpacitySelector';
import { AppCardWidthSelector } from './AppCardWidthSelector'; import { AppCardWidthSelector } from './AppCardWidthSelector';
import { ShadeSelector } from './ShadeSelector'; import { ShadeSelector } from './ShadeSelector';
import { GrowthSelector } from './GrowthSelector';
export default function TitleChanger() { export default function TitleChanger() {
const { config, setConfig } = useConfig(); const { config, setConfig } = useConfig();
@@ -74,6 +75,7 @@ export default function TitleChanger() {
<Button type="submit">{t('buttons.submit')}</Button> <Button type="submit">{t('buttons.submit')}</Button>
</Stack> </Stack>
</form> </form>
<GrowthSelector />
<ColorSelector type="primary" /> <ColorSelector type="primary" />
<ColorSelector type="secondary" /> <ColorSelector type="secondary" />
<ShadeSelector /> <ShadeSelector />

View File

@@ -0,0 +1,30 @@
import { Switch } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { useState } from 'react';
import { useConfig } from '../../tools/state';
export function GrowthSelector() {
const { config, setConfig } = useConfig();
const defaultPosition = config?.settings?.grow || false;
const [growState, setGrowState] = useState(defaultPosition);
const { t } = useTranslation('settings/common.json');
const toggleGrowState = () => {
setGrowState(!growState);
setConfig({
...config,
settings: {
...config.settings,
grow: !growState,
},
});
};
return (
<Switch
label={t('settings/common:grow')}
checked={growState === true}
onChange={() => toggleGrowState()}
size="md"
/>
);
}

View File

@@ -48,15 +48,11 @@ export function WidgetsPositionSwitch() {
}; };
return ( return (
<Group> <Switch
<div className={classes.root}> label={t('label')}
<Switch checked={widgetPosition === 'left'}
checked={widgetPosition === 'left'} onChange={() => toggleWidgetPosition()}
onChange={() => toggleWidgetPosition()} size="md"
size="md" />
/>
</div>
{t('label')}
</Group>
); );
} }

View File

@@ -1,5 +1,6 @@
import { import {
ActionIcon, ActionIcon,
Autocomplete,
Box, Box,
createStyles, createStyles,
Divider, Divider,
@@ -11,7 +12,7 @@ import {
Tooltip, Tooltip,
} from '@mantine/core'; } from '@mantine/core';
import { IconSearch, IconBrandYoutube, IconDownload, IconMovie } from '@tabler/icons'; import { IconSearch, IconBrandYoutube, IconDownload, IconMovie } from '@tabler/icons';
import React, { useEffect, useRef, useState } from 'react'; import React, { forwardRef, useEffect, useRef, useState } from 'react';
import { useDebouncedValue, useHotkeys } from '@mantine/hooks'; import { useDebouncedValue, useHotkeys } from '@mantine/hooks';
import { showNotification } from '@mantine/notifications'; import { showNotification } from '@mantine/notifications';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
@@ -21,6 +22,7 @@ import { useConfig } from '../../tools/state';
import { OverseerrModule } from '../overseerr'; import { OverseerrModule } from '../overseerr';
import Tip from '../../components/layout/Tip'; import Tip from '../../components/layout/Tip';
import { OverseerrMediaDisplay } from '../common'; import { OverseerrMediaDisplay } from '../common';
import SmallServiceItem from '../../components/AppShelf/SmallServiceItem';
export const SearchModule: IModule = { export const SearchModule: IModule = {
title: 'Search', title: 'Search',
@@ -97,6 +99,25 @@ export function SearchModuleComponent() {
}, },
]; ];
const [selectedSearchEngine, setSearchEngine] = useState<ItemProps>(searchEnginesList[0]); const [selectedSearchEngine, setSearchEngine] = useState<ItemProps>(searchEnginesList[0]);
const matchingServices = config.services.filter((service) => {
if (searchQuery === '' || searchQuery === undefined) {
return false;
}
return service.name.toLowerCase().includes(searchQuery.toLowerCase());
});
const autocompleteData = matchingServices.map((service) => ({
label: service.name,
value: service.name,
icon: service.icon,
url: service.openedUrl ?? service.url,
}));
const AutoCompleteItem = forwardRef<HTMLDivElement, any>(
({ label, value, icon, url, ...others }: any, ref) => (
<div ref={ref} {...others}>
<SmallServiceItem service={{ label, value, icon, url }} />
</div>
)
);
useEffect(() => { useEffect(() => {
// Refresh the default search engine every time the config for it changes #521 // Refresh the default search engine every time the config for it changes #521
setSearchEngine(searchEnginesList[0]); setSearchEngine(searchEnginesList[0]);
@@ -123,7 +144,7 @@ export function SearchModuleComponent() {
//TODO: Fix the bug where clicking anything inside the Modal to ask for a movie //TODO: Fix the bug where clicking anything inside the Modal to ask for a movie
// will close it (Because it closes the underlying Popover) // will close it (Because it closes the underlying Popover)
return ( return (
<Box style={{ width: '100%', maxWidth: 400, minWidth: 300 }}> <Box style={{ width: '100%', maxWidth: 400 }}>
<Popover <Popover
opened={OverseerrResults.length > 0 && opened && searchQuery.length > 3} opened={OverseerrResults.length > 0 && opened && searchQuery.length > 3}
position="bottom" position="bottom"
@@ -134,17 +155,30 @@ export function SearchModuleComponent() {
transition="pop-top-right" transition="pop-top-right"
> >
<Popover.Target> <Popover.Target>
<TextInput <Autocomplete
ref={textInput} ref={textInput}
onFocusCapture={() => setOpened(true)} onFocusCapture={() => setOpened(true)}
autoFocus autoFocus
rightSection={<SearchModuleMenu />} rightSection={<SearchModuleMenu />}
placeholder={t(`searchEngines.${selectedSearchEngine.value}.description`)} placeholder={t(`searchEngines.${selectedSearchEngine.value}.description`)}
value={searchQuery} value={searchQuery}
onChange={(event) => tryMatchSearchEngine(event.currentTarget.value, setSearchQuery)} onChange={(currentString) => tryMatchSearchEngine(currentString, setSearchQuery)}
itemComponent={AutoCompleteItem}
data={autocompleteData}
onItemSubmit={(item) => {
setOpened(false);
if (item.url) {
setSearchQuery('');
window.open(item.openedUrl ? item.openedUrl : item.url, openInNewTab);
}
}}
// Replace %s if it is in selectedSearchEngine.url with searchQuery, otherwise append searchQuery at the end of it // Replace %s if it is in selectedSearchEngine.url with searchQuery, otherwise append searchQuery at the end of it
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === 'Enter' && searchQuery.length > 0) { if (
event.key === 'Enter' &&
searchQuery.length > 0 &&
autocompleteData.length === 0
) {
if (selectedSearchEngine.url.includes('%s')) { if (selectedSearchEngine.url.includes('%s')) {
window.open(selectedSearchEngine.url.replace('%s', searchQuery), openInNewTab); window.open(selectedSearchEngine.url.replace('%s', searchQuery), openInNewTab);
} else { } else {
@@ -152,6 +186,8 @@ export function SearchModuleComponent() {
} }
} }
}} }}
radius="md"
size="md"
/> />
</Popover.Target> </Popover.Target>
<Popover.Dropdown> <Popover.Dropdown>

View File

@@ -14,6 +14,7 @@ export interface Settings {
customCSS?: string; customCSS?: string;
appOpacity?: number; appOpacity?: number;
widgetPosition?: string; widgetPosition?: string;
grow?: boolean;
appCardWidth?: number; appCardWidth?: number;
} }