Compare commits

..

8 Commits

Author SHA1 Message Date
shamoon
4028194830 Bump version to 1.5.0 2025-09-22 08:18:10 -07:00
shamoon
4c04a7a45f Merge branch 'dev' 2025-09-22 08:17:08 -07:00
github-actions[bot]
ce344a9db5 New Crowdin translations by GitHub Action (#5800)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-09-22 07:57:35 -07:00
github-actions[bot]
8e90ece498 New Crowdin translations by GitHub Action (#5695)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-09-21 16:30:51 -07:00
shamoon
151ad552ca Fix: dont lose color when switching light / dark (#5796) 2025-09-21 16:29:37 -07:00
shamoon
251cb65e12 Enhancement: mobile QuickLaunch button (#5789) 2025-09-18 22:52:16 -07:00
Ariel David Buena
8ebd0d0b2e Documentation: fix typo (#5770) 2025-09-11 18:46:27 -07:00
shamoon
5fe5a3869e Chore: update mkdocs (#5708) 2025-08-23 16:21:30 -07:00
7 changed files with 771 additions and 721 deletions

View File

@@ -441,6 +441,7 @@ There are a few optional settings for the Quick Launch feature:
- `showSearchSuggestions`: show search suggestions for the internet search. If this is not specified then the setting will be inherited from the search widget. If it is not specified there either, it will default to false. For custom providers the `suggestionUrl` needs to be set in order for this to work.
- `provider`: search engine provider. If none is specified it will try to use the provider set for the Search Widget, if neither are present then internet search will be disabled.
- `hideVisitURL`: disable detecting and offering an option to open URLs. This is false by default, enabling the feature.
- `mobileButtonPosition`: enables and sets the position of the mobile quicklaunch button. Options are `top-left`, `top-right`, `bottom-left`, `bottom-right`. This is empty by default, disabling the feature.
```yaml
quicklaunch:

View File

@@ -32,7 +32,7 @@ More detail on configuring service widgets can be found in the [Service Widgets
## Info Widgets
Info widgets are used to display information in the header, often about your system or environment. Info widgets are defined your `widgets.yaml` file. Here's an example:
Info widgets are used to display information in the header, often about your system or environment. Info widgets are defined in your `widgets.yaml` file. Here's an example:
```yaml
- openmeteo:

View File

@@ -1,6 +1,6 @@
{
"name": "homepage",
"version": "1.4.6",
"version": "1.5.0",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,37 @@
Babel==2.12.1
backrefs==5.9
cairocffi==1.7.1
CairoSVG==2.7.1
certifi==2023.7.22
cffi==1.17.1
cfgv==3.4.0
charset-normalizer==3.2.0
click==8.1.7
colorama==0.4.6
cssselect2==0.7.0
defusedxml==0.7.1
distlib==0.3.9
filelock==3.17.0
ghp-import==2.1.0
identify==2.6.7
idna==3.4
Jinja2==3.1.2
Markdown==3.4.4
MarkupSafe==2.1.3
mergedeep==1.3.4
mkdocs==1.6
mkdocs-material==9.5.26
mkdocs==1.6.0
mkdocs-get-deps==0.2.0
mkdocs-material==9.6.18
mkdocs-material-extensions==1.3.1
mkdocs-redirects==1.2.1
nodeenv==1.9.1
packaging==23.1
paginate==0.5.6
pathspec==0.11.2
pillow==10.4.0
platformdirs==3.10.0
pre-commit==3.5.0
pycparser==2.22
Pygments==2.16.1
pymdown-extensions==10.3
python-dateutil==2.8.2
@@ -24,8 +40,8 @@ pyyaml_env_tag==0.1
regex==2023.8.8
requests==2.31.0
six==1.16.0
tinycss2==1.4.0
urllib3==2.0.5
virtualenv==20.29.2
watchdog==3.0.0
pre-commit==3.5.0
mkdocs-material[imaging]==9.5.26
mkdocs-redirects==1.2.1
webencodings==0.5.1

View File

@@ -1,13 +1,21 @@
import classNames from "classnames";
import { useTranslation } from "next-i18next";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import { FiSearch } from "react-icons/fi";
import useSWR from "swr";
import { SettingsContext } from "utils/contexts/settings";
import ResolvedIcon from "./resolvedicon";
import { getStoredProvider, searchProviders } from "./widgets/search/search";
export default function QuickLaunch({ servicesAndBookmarks, searchString, setSearchString, isOpen, close }) {
const MOBILE_BUTTON_POSITIONS = {
"top-left": "top-4 left-4",
"top-right": "top-4 right-4",
"bottom-left": "bottom-4 left-4",
"bottom-right": "bottom-4 right-4",
};
export default function QuickLaunch({ servicesAndBookmarks, searchString, setSearchString, isOpen, setSearching }) {
const { t } = useTranslation();
const { settings } = useContext(SettingsContext);
@@ -49,6 +57,10 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
);
}
let mobileButtonPosition = settings.quicklaunch?.mobileButtonPosition
? MOBILE_BUTTON_POSITIONS[settings.quicklaunch.mobileButtonPosition]
: null;
function openCurrentItem(newWindow) {
const result = results[currentItemIndex];
window.open(
@@ -59,13 +71,13 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
}
const closeAndReset = useCallback(() => {
close(false);
setSearching(false);
setTimeout(() => {
setSearchString("");
setCurrentItemIndex(null);
setSearchSuggestions([]);
}, 200); // delay a little for animations
}, [close, setSearchString, setCurrentItemIndex, setSearchSuggestions]);
}, [setSearching, setSearchString, setCurrentItemIndex, setSearchSuggestions]);
function handleSearchChange(event) {
const rawSearchString = event.target.value;
@@ -245,86 +257,98 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
}
return (
<div
className={classNames(
"relative z-40 ease-in-out duration-300 transition-opacity",
hidden && !isOpen && "hidden",
!hidden && isOpen && "opacity-100",
!isOpen && "opacity-0",
)}
role="dialog"
aria-modal="true"
>
<div className="fixed inset-0 bg-gray-500 opacity-50" />
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full min-w-full items-start justify-center text-center">
<dialog className="mt-[10%] mx-auto min-w-[90%] max-w-[90%] md:min-w-[40%] md:max-w-[40%] rounded-md p-0 block font-medium text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-50 dark:bg-theme-800">
<input
placeholder="Search"
className={classNames(
results.length > 0 && "rounded-t-md",
results.length === 0 && "rounded-md",
"w-full p-4 m-0 border-0 border-b border-slate-700 focus:border-slate-700 focus:outline-0 focus:ring-0 text-sm md:text-xl text-theme-700 dark:text-theme-200 bg-theme-60 dark:bg-theme-800",
)}
type="text"
autoCorrect="false"
ref={searchField}
value={searchString}
onChange={handleSearchChange}
onKeyDown={handleSearchKeyDown}
/>
{results.length > 0 && (
<ul className="max-h-[60vh] overflow-y-auto m-2">
{results.map((r, i) => (
<li key={[r.name, r.container, r.app, r.href].filter((s) => s).join("-")}>
<button
type="button"
data-index={i}
onMouseEnter={handleItemHover}
onClick={handleItemClick}
onKeyDown={handleItemKeyDown}
className={classNames(
"flex flex-row w-full items-center justify-between rounded-md text-sm md:text-xl py-2 px-4 cursor-pointer text-theme-700 dark:text-theme-200",
i === currentItemIndex && "bg-theme-300/50 dark:bg-theme-700/50",
)}
>
<div className="flex flex-row items-center mr-4 pointer-events-none">
{(r.icon || r.abbr) && (
<div className="w-5 text-xs mr-4">
{r.icon && <ResolvedIcon icon={r.icon} />}
{r.abbr && r.abbr}
</div>
<>
<div
className={classNames(
"relative z-40 ease-in-out duration-300 transition-opacity",
hidden && !isOpen && "hidden",
!hidden && isOpen && "opacity-100",
!isOpen && "opacity-0",
)}
role="dialog"
aria-modal="true"
>
<div className="fixed inset-0 bg-gray-500 opacity-50" />
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full min-w-full items-start justify-center text-center">
<dialog className="mt-[10%] mx-auto min-w-[90%] max-w-[90%] md:min-w-[40%] md:max-w-[40%] rounded-md p-0 block font-medium text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-50 dark:bg-theme-800">
<input
placeholder="Search"
className={classNames(
results.length > 0 && "rounded-t-md",
results.length === 0 && "rounded-md",
"w-full p-4 m-0 border-0 border-b border-slate-700 focus:border-slate-700 focus:outline-0 focus:ring-0 text-sm md:text-xl text-theme-700 dark:text-theme-200 bg-theme-60 dark:bg-theme-800",
)}
type="text"
autoCorrect="false"
ref={searchField}
value={searchString}
onChange={handleSearchChange}
onKeyDown={handleSearchKeyDown}
/>
{results.length > 0 && (
<ul className="max-h-[60vh] overflow-y-auto m-2">
{results.map((r, i) => (
<li key={[r.name, r.container, r.app, r.href].filter((s) => s).join("-")}>
<button
type="button"
data-index={i}
onMouseEnter={handleItemHover}
onClick={handleItemClick}
onKeyDown={handleItemKeyDown}
className={classNames(
"flex flex-row w-full items-center justify-between rounded-md text-sm md:text-xl py-2 px-4 cursor-pointer text-theme-700 dark:text-theme-200",
i === currentItemIndex && "bg-theme-300/50 dark:bg-theme-700/50",
)}
<div className="flex flex-col md:flex-row text-left items-baseline mr-4 pointer-events-none">
{r.type !== "searchSuggestion" && <span className="mr-4">{r.name}</span>}
{r.type === "searchSuggestion" && (
<div className="flex-nowrap">
<span className="whitespace-pre">
{r.name.indexOf(searchString) === 0 ? searchString : ""}
</span>
<span className="whitespace-pre opacity-50">
{r.name.indexOf(searchString) === 0 ? r.name.substring(searchString.length) : r.name}
</span>
>
<div className="flex flex-row items-center mr-4 pointer-events-none">
{(r.icon || r.abbr) && (
<div className="w-5 text-xs mr-4">
{r.icon && <ResolvedIcon icon={r.icon} />}
{r.abbr && r.abbr}
</div>
)}
{r.description && (
<span className="text-xs text-theme-600 text-light">
{searchDescriptions && r.priority < 2 ? highlightText(r.description) : r.description}
</span>
)}
<div className="flex flex-col md:flex-row text-left items-baseline mr-4 pointer-events-none">
{r.type !== "searchSuggestion" && <span className="mr-4">{r.name}</span>}
{r.type === "searchSuggestion" && (
<div className="flex-nowrap">
<span className="whitespace-pre">
{r.name.indexOf(searchString) === 0 ? searchString : ""}
</span>
<span className="whitespace-pre opacity-50">
{r.name.indexOf(searchString) === 0 ? r.name.substring(searchString.length) : r.name}
</span>
</div>
)}
{r.description && (
<span className="text-xs text-theme-600 text-light">
{searchDescriptions && r.priority < 2 ? highlightText(r.description) : r.description}
</span>
)}
</div>
</div>
</div>
<div className="text-xs text-theme-600 font-bold pointer-events-none">
{t(`quicklaunch.${r.type ? r.type.toLowerCase() : "bookmark"}`)}
</div>
</button>
</li>
))}
</ul>
)}
</dialog>
<div className="text-xs text-theme-600 font-bold pointer-events-none">
{t(`quicklaunch.${r.type ? r.type.toLowerCase() : "bookmark"}`)}
</div>
</button>
</li>
))}
</ul>
)}
</dialog>
</div>
</div>
</div>
</div>
{mobileButtonPosition && (
<button
type="button"
onClick={setSearching.bind(this, !isOpen)}
className={`fixed ${mobileButtonPosition} z-40 p-2 rounded-full sm:hidden text-theme-700 dark:text-theme-200 bg-theme-50 dark:bg-theme-800 shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 transition-opacity duration-100`}
style={{ opacity: isOpen ? 0 : 1 }}
>
<FiSearch className="w-4 h-4" />
</button>
)}
</>
);
}

View File

@@ -432,7 +432,7 @@ function Home({ initialSettings }) {
searchString={searchString}
setSearchString={setSearchString}
isOpen={searching}
close={setSearching}
setSearching={setSearching}
/>
<div
id="information-widgets"
@@ -499,6 +499,7 @@ function Home({ initialSettings }) {
export default function Wrapper({ initialSettings, fallback }) {
const { theme } = useContext(ThemeContext);
const { color } = useContext(ColorContext);
let backgroundImage = "";
let opacity = initialSettings?.backgroundOpacity ?? 0;
let backgroundBlur = false;
@@ -527,14 +528,22 @@ export default function Wrapper({ initialSettings, fallback }) {
html.classList.toggle("dark", theme === "dark");
html.classList.add(theme === "dark" ? "scheme-dark" : "scheme-light");
html.classList.remove(...Array.from(html.classList).filter((cls) => cls.startsWith("theme-")));
html.classList.add(`theme-${initialSettings.color || "slate"}`);
const desiredThemeClass = `theme-${color || initialSettings.color || "slate"}`;
const themeClassesToRemove = Array.from(html.classList).filter(
(cls) => cls.startsWith("theme-") && cls !== desiredThemeClass,
);
if (themeClassesToRemove.length) {
html.classList.remove(...themeClassesToRemove);
}
if (!html.classList.contains(desiredThemeClass)) {
html.classList.add(desiredThemeClass);
}
// Remove any previously applied inline styles
body.style.backgroundImage = "";
body.style.backgroundColor = "";
body.style.backgroundAttachment = "";
}, [backgroundImage, opacity, theme, initialSettings.color]);
}, [backgroundImage, opacity, theme, color, initialSettings.color]);
return (
<>