Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
553be7da33 | ||
|
|
260b850e1a | ||
|
|
726a4fddd3 | ||
|
|
318c094f27 | ||
|
|
6e0d3807e4 | ||
|
|
10e9dc06dd | ||
|
|
e84687e5fc | ||
|
|
361d41065c | ||
|
|
4c0fbc0b42 | ||
|
|
ef8e380956 | ||
|
|
5db28b1607 | ||
|
|
dbfd4cf050 | ||
|
|
ffd298a2b6 | ||
|
|
9b1b5906e7 | ||
|
|
19bd14c63d | ||
|
|
b69343af56 | ||
|
|
94ee90eebb | ||
|
|
72b3097ad1 | ||
|
|
225f910fe8 | ||
|
|
10d9ffc740 | ||
|
|
4202d25d62 | ||
|
|
6a905e1b49 | ||
|
|
72e08f484f | ||
|
|
64dbb9c025 | ||
|
|
af2e0235bf | ||
|
|
bf85818f8b | ||
|
|
1840713179 | ||
|
|
b11bffb7cf | ||
|
|
bfb26a9402 | ||
|
|
c3b11be2d0 | ||
|
|
ecfb89de40 | ||
|
|
e1eab70f93 | ||
|
|
adb341c0fa | ||
|
|
25ccdffeb9 | ||
|
|
b98d399a9c | ||
|
|
f36e7b8abb | ||
|
|
667322d14e | ||
|
|
9b440c0da3 |
@@ -1,14 +1,9 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
stories: ['../src/components/**/*.story.mdx', '../src/components/**/*.story.*'],
|
stories: ['../src/components/**/*.story.mdx', '../src/components/**/*.story.*'],
|
||||||
addons: [
|
addons: [
|
||||||
'storybook-dark-mode',
|
|
||||||
'@storybook/addon-links',
|
'@storybook/addon-links',
|
||||||
'storybook-addon-mock/register',
|
'storybook-addon-mock/register',
|
||||||
'@storybook/addon-essentials',
|
'@storybook/addon-essentials',
|
||||||
{
|
|
||||||
name: 'storybook-addon-turbo-build',
|
|
||||||
options: { optimizationLevel: 2 },
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
typescript: {
|
typescript: {
|
||||||
check: false,
|
check: false,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useDarkMode } from 'storybook-dark-mode';
|
|
||||||
import { MantineProvider, ColorSchemeProvider } from '@mantine/core';
|
import { MantineProvider, ColorSchemeProvider } from '@mantine/core';
|
||||||
import { NotificationsProvider } from '@mantine/notifications';
|
import { NotificationsProvider } from '@mantine/notifications';
|
||||||
|
|
||||||
@@ -7,11 +6,7 @@ export const parameters = { layout: 'fullscreen' };
|
|||||||
function ThemeWrapper(props: { children: React.ReactNode }) {
|
function ThemeWrapper(props: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<ColorSchemeProvider colorScheme="light" toggleColorScheme={() => {}}>
|
<ColorSchemeProvider colorScheme="light" toggleColorScheme={() => {}}>
|
||||||
<MantineProvider
|
<MantineProvider withGlobalStyles withNormalizeCSS>
|
||||||
theme={{ colorScheme: useDarkMode() ? 'dark' : 'light' }}
|
|
||||||
withGlobalStyles
|
|
||||||
withNormalizeCSS
|
|
||||||
>
|
|
||||||
<NotificationsProvider>{props.children}</NotificationsProvider>
|
<NotificationsProvider>{props.children}</NotificationsProvider>
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
</ColorSchemeProvider>
|
</ColorSchemeProvider>
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -1,6 +1,9 @@
|
|||||||
<h3 align="center">Homarr</h3>
|
<h3 align="center">Homarr</h3>
|
||||||
<br>
|
<br>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
<i>Don't forget to star the repo if you enjoy the Homarr project!</i>
|
||||||
|
<br>
|
||||||
|
<img src="https://img.shields.io/github/stars/ajnart/homarr?label=%E2%AD%90%20Stars&style=flat-square?branch=master&kill_cache=1%22">
|
||||||
<a href="https://github.com/ajnart/homarr/actions/workflows/docker.yml">
|
<a href="https://github.com/ajnart/homarr/actions/workflows/docker.yml">
|
||||||
<img title="Docker CI Status" src="https://github.com/ajnart/homarr/actions/workflows/docker.yml/badge.svg" alt="CI Status"></a>
|
<img title="Docker CI Status" src="https://github.com/ajnart/homarr/actions/workflows/docker.yml/badge.svg" alt="CI Status"></a>
|
||||||
<a href="https://github.com/ajnart/homarr/releases/latest">
|
<a href="https://github.com/ajnart/homarr/releases/latest">
|
||||||
@@ -9,7 +12,8 @@
|
|||||||
<img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/ajnart/homarr?label=Downloads%20"></a>
|
<img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/ajnart/homarr?label=Downloads%20"></a>
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img align="end" width=600 src="https://user-images.githubusercontent.com/49837342/168315259-b778c816-10fe-44db-bd25-3eea6f31b233.png" />
|
<img align="end" width=600 src="https://user-images.githubusercontent.com/71191962/169860380-856634fb-4f41-47cb-ba54-6a9e7b3b9c81.gif" />
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<p align = "center">
|
<p align = "center">
|
||||||
A homepage for <i>your</i> server.
|
A homepage for <i>your</i> server.
|
||||||
@@ -21,7 +25,7 @@
|
|||||||
<br />
|
<br />
|
||||||
<a href = "https://discord.gg/aCsmEV5RgA" > <img title="Discord" src="https://discordapp.com/api/guilds/972958686051962910/widget.png?style=shield" > </a>
|
<a href = "https://discord.gg/aCsmEV5RgA" > <img title="Discord" src="https://discordapp.com/api/guilds/972958686051962910/widget.png?style=shield" > </a>
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# 📃 Table of Contents
|
# 📃 Table of Contents
|
||||||
@@ -32,6 +36,8 @@
|
|||||||
- [⚡ Installation](#-installation)
|
- [⚡ Installation](#-installation)
|
||||||
- [🐳 Deploying from Docker Image](#-deploying-from-docker-image)
|
- [🐳 Deploying from Docker Image](#-deploying-from-docker-image)
|
||||||
- [🛠️ Building from Source](#%EF%B8%8F-building-from-source)
|
- [🛠️ Building from Source](#%EF%B8%8F-building-from-source)
|
||||||
|
- [📖 Guides](#-guides)
|
||||||
|
- [🔁 Drag and Drop (Rearrange)](#-drag-and-drop-rearrange)
|
||||||
- [🔧 Configuration](#-configuration)
|
- [🔧 Configuration](#-configuration)
|
||||||
- [🧩 Integrations](#--integrations)
|
- [🧩 Integrations](#--integrations)
|
||||||
- [🧑🤝🧑 Multiple Configs](#-multiple-configs)
|
- [🧑🤝🧑 Multiple Configs](#-multiple-configs)
|
||||||
@@ -53,8 +59,6 @@ Homarr is a simple and lightweight homepage for your server, that helps you easi
|
|||||||
|
|
||||||
## 💥 Known Issues
|
## 💥 Known Issues
|
||||||
- Posters on the Calendar get blocked by adblockers. (IMDb posters)
|
- Posters on the Calendar get blocked by adblockers. (IMDb posters)
|
||||||
- Editing a service creates a duplicate (#97)
|
|
||||||
- Used search engine not properly selected (#35)
|
|
||||||
|
|
||||||
**[⤴️ Back to Top](#-table-of-contents)**
|
**[⤴️ Back to Top](#-table-of-contents)**
|
||||||
|
|
||||||
@@ -106,6 +110,11 @@ _Requirements_:
|
|||||||
- Start the NextJS web server: ``yarn start``
|
- Start the NextJS web server: ``yarn start``
|
||||||
- *Note: If you want to update the code in real time, launch with ``yarn dev``*
|
- *Note: If you want to update the code in real time, launch with ``yarn dev``*
|
||||||
|
|
||||||
|
## 📖 Guides
|
||||||
|
|
||||||
|
### 🔁 Drag and Drop (Rearrange)
|
||||||
|
You can rearrange items by Drag and Dropping them to any position. To Drag an Drop, click and hold an icon for 250ms and then drag it to the desired position.
|
||||||
|
|
||||||
## 🔧 Configuration
|
## 🔧 Configuration
|
||||||
|
|
||||||
### 🧩 Integrations
|
### 🧩 Integrations
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "config",
|
|
||||||
"services": [
|
|
||||||
{
|
|
||||||
"type": "Other",
|
|
||||||
"name": "YouTube",
|
|
||||||
"icon": "https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/youtube.png",
|
|
||||||
"url": "https://youtube.com/"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "Other",
|
|
||||||
"name": "YouTube ",
|
|
||||||
"icon": "https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/youtube.png",
|
|
||||||
"url": "https://youtube.com/"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"searchBar": true,
|
|
||||||
"searchUrl": "Custom",
|
|
||||||
"enabledModules": [
|
|
||||||
"Date",
|
|
||||||
"Calendar",
|
|
||||||
"Weather"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "config_new",
|
|
||||||
"services": [
|
|
||||||
{
|
|
||||||
"type": "Other",
|
|
||||||
"name": "example",
|
|
||||||
"icon": "https://c.tenor.com/o656qFKDzeUAAAAC/rick-astley-never-gonna-give-you-up.gif",
|
|
||||||
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"searchBar": true,
|
|
||||||
"searchUrl": "https://duckduckgo.com/?q=",
|
|
||||||
"enabledModules": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,15 +2,14 @@
|
|||||||
"name": "default",
|
"name": "default",
|
||||||
"services": [
|
"services": [
|
||||||
{
|
{
|
||||||
"type": "Other",
|
|
||||||
"name": "example",
|
"name": "example",
|
||||||
|
"id": "09c45847-8afc-4c1a-9697-f03192de948a",
|
||||||
|
"type": "Other",
|
||||||
"icon": "https://c.tenor.com/o656qFKDzeUAAAAC/rick-astley-never-gonna-give-you-up.gif",
|
"icon": "https://c.tenor.com/o656qFKDzeUAAAAC/rick-astley-never-gonna-give-you-up.gif",
|
||||||
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
"searchBar": true,
|
"searchUrl": "https://bing.com/search?q="
|
||||||
"searchUrl": "https://bing.com/search?q=",
|
|
||||||
"enabledModules": []
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
export const REPO_URL = 'ajnart/homarr';
|
export const REPO_URL = 'ajnart/homarr';
|
||||||
export const CURRENT_VERSION = 'v0.4.0';
|
export const CURRENT_VERSION = 'v0.5.0';
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
|
const { env } = require('process');
|
||||||
|
|
||||||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||||
enabled: process.env.ANALYZE === 'true',
|
enabled: process.env.ANALYZE === 'true',
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = withBundleAnalyzer({
|
module.exports = withBundleAnalyzer({
|
||||||
reactStrictMode: true,
|
reactStrictMode: false,
|
||||||
eslint: {
|
eslint: {
|
||||||
ignoreDuringBuilds: true,
|
ignoreDuringBuilds: true,
|
||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
outputStandalone: true,
|
outputStandalone: true,
|
||||||
},
|
},
|
||||||
|
basePath: env.BASE_URL,
|
||||||
});
|
});
|
||||||
|
|||||||
153
package.json
153
package.json
@@ -1,91 +1,80 @@
|
|||||||
{
|
{
|
||||||
"name": "homarr",
|
"name": "homarr",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"private": "false",
|
"private": "false",
|
||||||
"description": "Homarr - A homepage for your server.",
|
"description": "Homarr - A homepage for your server.",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/ajnart/homarr"
|
"url": "https://github.com/ajnart/homarr"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
"start": "next start --port 7575",
|
"start": "next start",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"export": "next build && next export",
|
"export": "next build && next export",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"jest": "jest",
|
"jest": "jest",
|
||||||
"jest:watch": "jest --watch",
|
"jest:watch": "jest --watch",
|
||||||
"prettier:check": "prettier --check \"**/*.{ts,tsx}\"",
|
"prettier:check": "prettier --check \"**/*.{ts,tsx}\"",
|
||||||
"prettier:write": "prettier --write \"**/*.{ts,tsx}\"",
|
"prettier:write": "prettier --write \"**/*.{ts,tsx}\"",
|
||||||
"test": "npm run prettier:check && npm run lint && npm run typecheck && npm run jest",
|
"test": "npm run prettier:check && npm run lint && npm run typecheck && npm run jest",
|
||||||
"storybook": "start-storybook -p 7001",
|
"storybook": "start-storybook -p 7001",
|
||||||
"storybook:build": "build-storybook",
|
"storybook:build": "build-storybook",
|
||||||
"ci": "yarn test && yarn lint --fix && yarn typecheck && yarn prettier:write"
|
"ci": "yarn test && yarn lint --fix && yarn typecheck && yarn prettier:write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mantine/core": "^4.2.4",
|
"@dnd-kit/core": "^6.0.1",
|
||||||
"@mantine/dates": "^4.2.4",
|
"@dnd-kit/sortable": "^7.0.0",
|
||||||
"@mantine/dropzone": "^4.2.4",
|
"@mantine/core": "^4.2.6",
|
||||||
"@mantine/form": "^4.2.4",
|
"@mantine/dates": "^4.2.6",
|
||||||
"@mantine/hooks": "^4.2.4",
|
"@mantine/dropzone": "^4.2.6",
|
||||||
"@mantine/modals": "^4.2.4",
|
"@mantine/form": "^4.2.6",
|
||||||
"@mantine/next": "^4.2.4",
|
"@mantine/hooks": "^4.2.6",
|
||||||
"@mantine/notifications": "^4.2.4",
|
"@mantine/next": "^4.2.6",
|
||||||
"@mantine/prism": "^4.2.4",
|
"@mantine/notifications": "^4.2.6",
|
||||||
"@mantine/rte": "^4.2.4",
|
"@mantine/prism": "^4.2.6",
|
||||||
"@mantine/spotlight": "^4.2.4",
|
"axios": "^0.27.2",
|
||||||
"@modulz/radix-icons": "^4.0.0",
|
"cookies-next": "^2.0.4",
|
||||||
"axios": "^0.27.2",
|
"dayjs": "^1.11.2",
|
||||||
"cookies-next": "^2.0.4",
|
"framer-motion": "^6.3.1",
|
||||||
"dayjs": "^1.11.2",
|
"js-file-download": "^0.4.12",
|
||||||
"framer-motion": "^6.3.1",
|
"next": "12.1.6",
|
||||||
"js-file-download": "^0.4.12",
|
"prism-react-renderer": "^1.3.1",
|
||||||
"next": "12.1.5-canary.4",
|
"react": "^17.0.1",
|
||||||
"prism-react-renderer": "^1.3.1",
|
"react-dom": "^17.0.1",
|
||||||
"react": "18.0.0",
|
"tabler-icons-react": "^1.46.0",
|
||||||
"react-dom": "18.0.0",
|
"uuid": "^8.3.2"
|
||||||
"tabler-icons-react": "^1.46.0"
|
},
|
||||||
},
|
"devDependencies": {
|
||||||
"devDependencies": {
|
"@babel/core": "^7.17.8",
|
||||||
"@babel/core": "^7.17.8",
|
"@next/bundle-analyzer": "^12.1.4",
|
||||||
"@next/bundle-analyzer": "^12.1.4",
|
"@next/eslint-plugin-next": "^12.1.4",
|
||||||
"@next/eslint-plugin-next": "^12.1.4",
|
"@storybook/react": "^6.5.4",
|
||||||
"@storybook/addon-essentials": "^6.4.22",
|
"@types/node": "^17.0.23",
|
||||||
"@storybook/addon-links": "^6.4.22",
|
"@types/react": "17.0.43",
|
||||||
"@storybook/react": "^6.4.22",
|
"@types/uuid": "^8.3.4",
|
||||||
"@testing-library/dom": "^8.12.0",
|
"@typescript-eslint/eslint-plugin": "^5.16.0",
|
||||||
"@testing-library/jest-dom": "^5.16.3",
|
"@typescript-eslint/parser": "^5.16.0",
|
||||||
"@testing-library/react": "^13.0.0",
|
"eslint": "^8.11.0",
|
||||||
"@testing-library/user-event": "^14.0.4",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
"@types/jest": "^27.4.1",
|
"eslint-config-airbnb-typescript": "^16.1.0",
|
||||||
"@types/node": "^17.0.23",
|
"eslint-config-mantine": "1.1.0",
|
||||||
"@types/react": "17.0.43",
|
"eslint-plugin-import": "^2.25.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.16.0",
|
"eslint-plugin-jest": "^26.1.3",
|
||||||
"@typescript-eslint/parser": "^5.16.0",
|
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||||
"babel-loader": "^8.2.4",
|
"eslint-plugin-react": "^7.29.4",
|
||||||
"eslint": "^8.11.0",
|
"eslint-plugin-react-hooks": "^4.3.0",
|
||||||
"eslint-config-airbnb": "19.0.4",
|
"eslint-plugin-storybook": "^0.5.11",
|
||||||
"eslint-config-airbnb-typescript": "^16.1.4",
|
"eslint-plugin-testing-library": "^5.2.0",
|
||||||
"eslint-config-mantine": "1.1.0",
|
"eslint-plugin-unused-imports": "^2.0.0",
|
||||||
"eslint-plugin-import": "^2.25.4",
|
"jest": "^28.1.0",
|
||||||
"eslint-plugin-jest": "^26.1.3",
|
"prettier": "^2.6.2",
|
||||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
"require-from-string": "^2.0.2",
|
||||||
"eslint-plugin-react": "^7.29.4",
|
"typescript": "4.6.4"
|
||||||
"eslint-plugin-react-hooks": "^4.3.0",
|
},
|
||||||
"eslint-plugin-storybook": "^0.5.11",
|
"resolutions": {
|
||||||
"eslint-plugin-testing-library": "^5.2.0",
|
"@types/react": "17.0.30"
|
||||||
"eslint-plugin-unused-imports": "^2.0.0",
|
}
|
||||||
"jest": "^27.5.1",
|
|
||||||
"prettier": "^2.6.2",
|
|
||||||
"storybook-addon-mock": "^2.3.2",
|
|
||||||
"storybook-addon-turbo-build": "^1.1.0",
|
|
||||||
"storybook-dark-mode": "^1.0.9",
|
|
||||||
"ts-jest": "^27.1.4",
|
|
||||||
"typescript": "4.6.3"
|
|
||||||
},
|
|
||||||
"resolutions": {
|
|
||||||
"@types/react": "17.0.30"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,21 +6,17 @@ import {
|
|||||||
Image,
|
Image,
|
||||||
Button,
|
Button,
|
||||||
Select,
|
Select,
|
||||||
AspectRatio,
|
|
||||||
Text,
|
|
||||||
Card,
|
|
||||||
LoadingOverlay,
|
LoadingOverlay,
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Title,
|
Title,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Apps } from 'tabler-icons-react';
|
import { Apps } from 'tabler-icons-react';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { useConfig } from '../../tools/state';
|
import { useConfig } from '../../tools/state';
|
||||||
import { ServiceTypeList } from '../../tools/types';
|
import { ServiceTypeList } from '../../tools/types';
|
||||||
import { AppShelfItemWrapper } from './AppShelfItemWrapper';
|
|
||||||
|
|
||||||
export function AddItemShelfButton(props: any) {
|
export function AddItemShelfButton(props: any) {
|
||||||
const [opened, setOpened] = useState(false);
|
const [opened, setOpened] = useState(false);
|
||||||
@@ -51,70 +47,16 @@ export function AddItemShelfButton(props: any) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AddItemShelfItem(props: any) {
|
|
||||||
const [opened, setOpened] = useState(false);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal
|
|
||||||
size="xl"
|
|
||||||
radius="md"
|
|
||||||
opened={props.opened || opened}
|
|
||||||
onClose={() => setOpened(false)}
|
|
||||||
title="Add a service"
|
|
||||||
>
|
|
||||||
<AddAppShelfItemForm setOpened={setOpened} />
|
|
||||||
</Modal>
|
|
||||||
<AppShelfItemWrapper>
|
|
||||||
<Card.Section>
|
|
||||||
<Group position="center" mx="lg">
|
|
||||||
<Text
|
|
||||||
// TODO: #1 Remove this hack to get the text to be centered.
|
|
||||||
ml={15}
|
|
||||||
style={{
|
|
||||||
alignSelf: 'center',
|
|
||||||
alignContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
justifyItems: 'center',
|
|
||||||
}}
|
|
||||||
mt="sm"
|
|
||||||
weight={500}
|
|
||||||
>
|
|
||||||
Add a service
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Card.Section>
|
|
||||||
<Card.Section>
|
|
||||||
<AspectRatio ratio={5 / 3} m="xl">
|
|
||||||
<motion.i
|
|
||||||
whileHover={{
|
|
||||||
cursor: 'pointer',
|
|
||||||
scale: 1.1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Apps style={{ cursor: 'pointer' }} onClick={() => setOpened(true)} size={60} />
|
|
||||||
</motion.i>
|
|
||||||
</AspectRatio>
|
|
||||||
</Card.Section>
|
|
||||||
</AppShelfItemWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MatchIcon(name: string, form: any) {
|
function MatchIcon(name: string, form: any) {
|
||||||
fetch(
|
fetch(
|
||||||
`https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/${name
|
`https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/${name
|
||||||
.replace(/\s+/g, '-')
|
.replace(/\s+/g, '-')
|
||||||
.toLowerCase()}.png`
|
.toLowerCase()}.png`
|
||||||
)
|
).then((res) => {
|
||||||
.then((res) => {
|
if (res.ok) {
|
||||||
if (res.status === 200) {
|
form.setFieldValue('icon', res.url);
|
||||||
form.setFieldValue('icon', res.url);
|
}
|
||||||
}
|
});
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// Do nothing
|
|
||||||
});
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -126,9 +68,10 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
|||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
|
id: props.id ?? uuidv4(),
|
||||||
type: props.type ?? 'Other',
|
type: props.type ?? 'Other',
|
||||||
name: props.name ?? '',
|
name: props.name ?? '',
|
||||||
icon: props.icon ?? '',
|
icon: props.icon ?? '/favicon.svg',
|
||||||
url: props.url ?? '',
|
url: props.url ?? '',
|
||||||
apiKey: props.apiKey ?? (undefined as unknown as string),
|
apiKey: props.apiKey ?? (undefined as unknown as string),
|
||||||
},
|
},
|
||||||
@@ -136,15 +79,18 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
|||||||
apiKey: () => null,
|
apiKey: () => null,
|
||||||
// Validate icon with a regex
|
// Validate icon with a regex
|
||||||
icon: (value: string) => {
|
icon: (value: string) => {
|
||||||
if (!value.match(/^https?:\/\/.+\.(png|jpg|jpeg|gif)$/)) {
|
// Regex to match everything that ends with and icon extension
|
||||||
|
if (!value.match(/\.(png|jpg|jpeg|gif|svg)$/)) {
|
||||||
return 'Please enter a valid icon URL';
|
return 'Please enter a valid icon URL';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
// Validate url with a regex http/https
|
// Validate url with a regex http/https
|
||||||
url: (value: string) => {
|
url: (value: string) => {
|
||||||
if (!value.match(/^https?:\/\/.+\/$/)) {
|
try {
|
||||||
return 'Please enter a valid URL (that ends with a /)';
|
const _isValid = new URL(value);
|
||||||
|
} catch (e) {
|
||||||
|
return 'Please enter a valid URL';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -166,11 +112,12 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
|||||||
<form
|
<form
|
||||||
onSubmit={form.onSubmit(() => {
|
onSubmit={form.onSubmit(() => {
|
||||||
// If service already exists, update it.
|
// If service already exists, update it.
|
||||||
if (config.services && config.services.find((s) => s.name === form.values.name)) {
|
if (config.services && config.services.find((s) => s.id === form.values.id)) {
|
||||||
setConfig({
|
setConfig({
|
||||||
...config,
|
...config,
|
||||||
|
// replace the found item by matching ID
|
||||||
services: config.services.map((s) => {
|
services: config.services.map((s) => {
|
||||||
if (s.name === form.values.name) {
|
if (s.id === form.values.id) {
|
||||||
return {
|
return {
|
||||||
...form.values,
|
...form.values,
|
||||||
};
|
};
|
||||||
@@ -213,7 +160,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
|||||||
<TextInput
|
<TextInput
|
||||||
required
|
required
|
||||||
label="Service url"
|
label="Service url"
|
||||||
placeholder="http://localhost:8989"
|
placeholder="http://localhost:7575"
|
||||||
{...form.getInputProps('url')}
|
{...form.getInputProps('url')}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { SimpleGrid } from '@mantine/core';
|
import { SimpleGrid } from '@mantine/core';
|
||||||
import AppShelf, { AppShelfItem } from './AppShelf';
|
import AppShelf from './AppShelf';
|
||||||
|
import { AppShelfItem } from './AppShelfItem';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Item Shelf',
|
title: 'Item Shelf',
|
||||||
|
|||||||
@@ -1,106 +1,79 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { Grid } from '@mantine/core';
|
||||||
import { Text, AspectRatio, Card, Image, Center, Grid, createStyles } from '@mantine/core';
|
import {
|
||||||
|
closestCenter,
|
||||||
|
DndContext,
|
||||||
|
DragOverlay,
|
||||||
|
MouseSensor,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
} from '@dnd-kit/core';
|
||||||
|
import { arrayMove, SortableContext } from '@dnd-kit/sortable';
|
||||||
import { useConfig } from '../../tools/state';
|
import { useConfig } from '../../tools/state';
|
||||||
import { serviceItem } from '../../tools/types';
|
|
||||||
import AppShelfMenu from './AppShelfMenu';
|
|
||||||
import PingComponent from '../modules/ping/PingModule';
|
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => ({
|
import { SortableAppShelfItem, AppShelfItem } from './AppShelfItem';
|
||||||
item: {
|
|
||||||
transition: 'box-shadow 150ms ease, transform 100ms ease',
|
|
||||||
|
|
||||||
'&:hover': {
|
|
||||||
boxShadow: `${theme.shadows.md} !important`,
|
|
||||||
transform: 'scale(1.05)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const AppShelf = (props: any) => {
|
const AppShelf = (props: any) => {
|
||||||
const { config } = useConfig();
|
const [activeId, setActiveId] = useState(null);
|
||||||
|
const { config, setConfig } = useConfig();
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(MouseSensor, {
|
||||||
|
// Require the mouse to move by 10 pixels before activating
|
||||||
|
activationConstraint: {
|
||||||
|
delay: 250,
|
||||||
|
tolerance: 5,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleDragStart(event: any) {
|
||||||
|
const { active } = event;
|
||||||
|
|
||||||
|
setActiveId(active.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragEnd(event: any) {
|
||||||
|
const { active, over } = event;
|
||||||
|
|
||||||
|
if (active.id !== over.id) {
|
||||||
|
const newConfig = { ...config };
|
||||||
|
const activeIndex = newConfig.services.findIndex((e) => e.id === active.id);
|
||||||
|
const overIndex = newConfig.services.findIndex((e) => e.id === over.id);
|
||||||
|
newConfig.services = arrayMove(newConfig.services, activeIndex, overIndex);
|
||||||
|
setConfig(newConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveId(null);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid gutter="xl" align="center">
|
<DndContext
|
||||||
{config.services.map((service) => (
|
sensors={sensors}
|
||||||
<Grid.Col key={service.name} span={6} xl={2} xs={4} sm={3} md={3}>
|
collisionDetection={closestCenter}
|
||||||
<AppShelfItem key={service.name} service={service} />
|
onDragStart={handleDragStart}
|
||||||
</Grid.Col>
|
onDragEnd={handleDragEnd}
|
||||||
))}
|
>
|
||||||
</Grid>
|
<SortableContext items={config.services}>
|
||||||
|
<Grid gutter="xl" align="center">
|
||||||
|
{config.services.map((service) => (
|
||||||
|
<Grid.Col key={service.id} span={6} xl={2} xs={4} sm={3} md={3}>
|
||||||
|
<SortableAppShelfItem service={service} key={service.id} id={service.id} />
|
||||||
|
</Grid.Col>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</SortableContext>
|
||||||
|
<DragOverlay
|
||||||
|
style={{
|
||||||
|
// Add a shadow to the drag overlay
|
||||||
|
boxShadow: '0 0 10px rgba(0, 0, 0, 0.5)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{activeId ? (
|
||||||
|
<AppShelfItem service={config.services.find((e) => e.id === activeId)} id={activeId} />
|
||||||
|
) : null}
|
||||||
|
</DragOverlay>
|
||||||
|
</DndContext>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AppShelfItem(props: any) {
|
|
||||||
const { service }: { service: serviceItem } = props;
|
|
||||||
const [hovering, setHovering] = useState(false);
|
|
||||||
const { classes, theme } = useStyles();
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
animate={{
|
|
||||||
scale: [0.9, 1.06, 1],
|
|
||||||
rotate: [0, 5, 0],
|
|
||||||
}}
|
|
||||||
transition={{ duration: 0.6, type: 'spring', damping: 10, mass: 0.75, stiffness: 100 }}
|
|
||||||
key={service.name}
|
|
||||||
onHoverStart={() => {
|
|
||||||
setHovering(true);
|
|
||||||
}}
|
|
||||||
onHoverEnd={() => {
|
|
||||||
setHovering(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Card withBorder radius="lg" shadow="md" className={classes.item}>
|
|
||||||
<Card.Section>
|
|
||||||
<Text mt="sm" align="center" lineClamp={1} weight={550}>
|
|
||||||
{service.name}
|
|
||||||
</Text>
|
|
||||||
<motion.div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 15,
|
|
||||||
right: 15,
|
|
||||||
alignSelf: 'flex-end',
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
opacity: hovering ? 1 : 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AppShelfMenu service={service} />
|
|
||||||
</motion.div>
|
|
||||||
</Card.Section>
|
|
||||||
<Center>
|
|
||||||
<Card.Section>
|
|
||||||
<AspectRatio
|
|
||||||
ratio={3 / 5}
|
|
||||||
m="xl"
|
|
||||||
style={{
|
|
||||||
width: 150,
|
|
||||||
height: 90,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<motion.i
|
|
||||||
whileHover={{
|
|
||||||
cursor: 'pointer',
|
|
||||||
scale: 1.1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
width={80}
|
|
||||||
height={80}
|
|
||||||
src={service.icon}
|
|
||||||
fit="contain"
|
|
||||||
onClick={() => {
|
|
||||||
window.open(service.url);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</motion.i>
|
|
||||||
</AspectRatio>
|
|
||||||
<PingComponent url={service.url} />
|
|
||||||
</Card.Section>
|
|
||||||
</Center>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AppShelf;
|
export default AppShelf;
|
||||||
|
|||||||
123
src/components/AppShelf/AppShelfItem.tsx
Normal file
123
src/components/AppShelf/AppShelfItem.tsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import {
|
||||||
|
Text,
|
||||||
|
Card,
|
||||||
|
Anchor,
|
||||||
|
AspectRatio,
|
||||||
|
Image,
|
||||||
|
Center,
|
||||||
|
createStyles,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
import { serviceItem } from '../../tools/types';
|
||||||
|
import PingComponent from '../modules/ping/PingModule';
|
||||||
|
import AppShelfMenu from './AppShelfMenu';
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme) => ({
|
||||||
|
item: {
|
||||||
|
transition: 'box-shadow 150ms ease, transform 100ms ease',
|
||||||
|
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow: `${theme.shadows.md} !important`,
|
||||||
|
transform: 'scale(1.05)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export function SortableAppShelfItem(props: any) {
|
||||||
|
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
|
||||||
|
id: props.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
|
||||||
|
<AppShelfItem service={props.service} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppShelfItem(props: any) {
|
||||||
|
const { service }: { service: serviceItem } = props;
|
||||||
|
const [hovering, setHovering] = useState(false);
|
||||||
|
const { classes, theme } = useStyles();
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
animate={{
|
||||||
|
scale: [0.9, 1.06, 1],
|
||||||
|
rotate: [0, 5, 0],
|
||||||
|
}}
|
||||||
|
transition={{ duration: 0.6, type: 'spring', damping: 10, mass: 0.75, stiffness: 100 }}
|
||||||
|
key={service.name}
|
||||||
|
onHoverStart={() => {
|
||||||
|
setHovering(true);
|
||||||
|
}}
|
||||||
|
onHoverEnd={() => {
|
||||||
|
setHovering(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card withBorder radius="lg" shadow="md" className={classes.item}>
|
||||||
|
<Card.Section>
|
||||||
|
<Anchor
|
||||||
|
target="_blank"
|
||||||
|
href={service.url}
|
||||||
|
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}
|
||||||
|
>
|
||||||
|
<Text mt="sm" align="center" lineClamp={1} weight={550}>
|
||||||
|
{service.name}
|
||||||
|
</Text>
|
||||||
|
</Anchor>
|
||||||
|
<motion.div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 15,
|
||||||
|
right: 15,
|
||||||
|
alignSelf: 'flex-end',
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
opacity: hovering ? 1 : 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AppShelfMenu service={service} />
|
||||||
|
</motion.div>
|
||||||
|
</Card.Section>
|
||||||
|
<Center>
|
||||||
|
<Card.Section>
|
||||||
|
<AspectRatio
|
||||||
|
ratio={3 / 5}
|
||||||
|
m="xl"
|
||||||
|
style={{
|
||||||
|
width: 150,
|
||||||
|
height: 90,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<motion.i
|
||||||
|
whileHover={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
scale: 1.1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
width={80}
|
||||||
|
height={80}
|
||||||
|
src={service.icon}
|
||||||
|
fit="contain"
|
||||||
|
onClick={() => {
|
||||||
|
window.open(service.url);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.i>
|
||||||
|
</AspectRatio>
|
||||||
|
<PingComponent url={service.url} />
|
||||||
|
</Card.Section>
|
||||||
|
</Center>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { useMantineTheme, Card } from '@mantine/core';
|
|
||||||
|
|
||||||
export function AppShelfItemWrapper(props: any) {
|
|
||||||
const { children, hovering } = props;
|
|
||||||
const theme = useMantineTheme();
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
style={{
|
|
||||||
boxShadow: hovering ? '0px 0px 3px rgba(0, 0, 0, 0.5)' : '0px 0px 1px rgba(0, 0, 0, 0.5)',
|
|
||||||
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[1],
|
|
||||||
}}
|
|
||||||
radius="md"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -22,6 +22,7 @@ export default function AppShelfMenu(props: any) {
|
|||||||
<AddAppShelfItemForm
|
<AddAppShelfItemForm
|
||||||
setOpened={setOpened}
|
setOpened={setOpened}
|
||||||
name={service.name}
|
name={service.name}
|
||||||
|
id={service.id}
|
||||||
type={service.type}
|
type={service.type}
|
||||||
url={service.url}
|
url={service.url}
|
||||||
icon={service.icon}
|
icon={service.icon}
|
||||||
@@ -54,7 +55,7 @@ export default function AppShelfMenu(props: any) {
|
|||||||
onClick={(e: any) => {
|
onClick={(e: any) => {
|
||||||
setConfig({
|
setConfig({
|
||||||
...config,
|
...config,
|
||||||
services: config.services.filter((s) => s.name !== service.name),
|
services: config.services.filter((s) => s.id !== service.id),
|
||||||
});
|
});
|
||||||
showNotification({
|
showNotification({
|
||||||
autoClose: 5000,
|
autoClose: 5000,
|
||||||
|
|||||||
2
src/components/AppShelf/index.ts
Normal file
2
src/components/AppShelf/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as AppShelf } from './AppShelf';
|
||||||
|
export * from './AppShelfItem';
|
||||||
@@ -7,6 +7,7 @@ import { useRouter } from 'next/router';
|
|||||||
import { setCookies } from 'cookies-next';
|
import { setCookies } from 'cookies-next';
|
||||||
import { useConfig } from '../../tools/state';
|
import { useConfig } from '../../tools/state';
|
||||||
import { Config } from '../../tools/types';
|
import { Config } from '../../tools/types';
|
||||||
|
import { migrateToIdConfig } from '../../tools/migrate';
|
||||||
|
|
||||||
function getIconColor(status: DropzoneStatus, theme: MantineTheme) {
|
function getIconColor(status: DropzoneStatus, theme: MantineTheme) {
|
||||||
return status.accepted
|
return status.accepted
|
||||||
@@ -84,7 +85,8 @@ export default function LoadConfigComponent(props: any) {
|
|||||||
message: undefined,
|
message: undefined,
|
||||||
});
|
});
|
||||||
setCookies('config-name', newConfig.name, { maxAge: 60 * 60 * 24 * 30 });
|
setCookies('config-name', newConfig.name, { maxAge: 60 * 60 * 24 * 30 });
|
||||||
setConfig(newConfig);
|
const migratedConfig = migrateToIdConfig(newConfig);
|
||||||
|
setConfig(migratedConfig);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
accept={['application/json']}
|
accept={['application/json']}
|
||||||
|
|||||||
@@ -5,34 +5,25 @@ import { useConfig } from '../../tools/state';
|
|||||||
export default function ModuleEnabler(props: any) {
|
export default function ModuleEnabler(props: any) {
|
||||||
const { config, setConfig } = useConfig();
|
const { config, setConfig } = useConfig();
|
||||||
const modules = Object.values(Modules).map((module) => module);
|
const modules = Object.values(Modules).map((module) => module);
|
||||||
const enabledModules = config.settings.enabledModules ?? [];
|
|
||||||
modules.filter((module) => enabledModules.includes(module.title));
|
|
||||||
return (
|
return (
|
||||||
<Group direction="column">
|
<Group direction="column">
|
||||||
{modules.map((module) => (
|
{modules.map((module) => (
|
||||||
<Switch
|
<Switch
|
||||||
key={module.title}
|
key={module.title}
|
||||||
size="md"
|
size="md"
|
||||||
checked={enabledModules.includes(module.title)}
|
checked={config.modules?.[module.title]?.enabled ?? false}
|
||||||
label={`Enable ${module.title} module`}
|
label={`Enable ${module.title} module`}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (e.currentTarget.checked) {
|
setConfig({
|
||||||
setConfig({
|
...config,
|
||||||
...config,
|
modules: {
|
||||||
settings: {
|
...config.modules,
|
||||||
...config.settings,
|
[module.title]: {
|
||||||
enabledModules: [...enabledModules, module.title],
|
...config.modules?.[module.title],
|
||||||
|
enabled: e.currentTarget.checked,
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
} else {
|
});
|
||||||
setConfig({
|
|
||||||
...config,
|
|
||||||
settings: {
|
|
||||||
...config.settings,
|
|
||||||
enabledModules: enabledModules.filter((m) => m !== module.title),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Group, Image, Text } from '@mantine/core';
|
import { Group, Image, Text } from '@mantine/core';
|
||||||
|
import { NextLink } from '@mantine/next';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
export function Logo({ style }: any) {
|
export function Logo({ style }: any) {
|
||||||
@@ -11,14 +12,22 @@ export function Logo({ style }: any) {
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Text
|
<NextLink
|
||||||
sx={style}
|
style={{
|
||||||
weight="bold"
|
textDecoration: 'none',
|
||||||
variant="gradient"
|
position: 'relative',
|
||||||
gradient={{ from: 'red', to: 'orange', deg: 145 }}
|
}}
|
||||||
|
href="/"
|
||||||
>
|
>
|
||||||
Homarr
|
<Text
|
||||||
</Text>
|
sx={style}
|
||||||
|
weight="bold"
|
||||||
|
variant="gradient"
|
||||||
|
gradient={{ from: 'red', to: 'orange', deg: 145 }}
|
||||||
|
>
|
||||||
|
Homarr
|
||||||
|
</Text>
|
||||||
|
</NextLink>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,40 +32,42 @@ export default function CalendarComponent(props: any) {
|
|||||||
const radarrService = filtered.filter((service) => service.type === 'Radarr').at(0);
|
const radarrService = filtered.filter((service) => service.type === 'Radarr').at(0);
|
||||||
const nextMonth = new Date(new Date().setMonth(new Date().getMonth() + 2)).toISOString();
|
const nextMonth = new Date(new Date().setMonth(new Date().getMonth() + 2)).toISOString();
|
||||||
if (sonarrService && sonarrService.apiKey) {
|
if (sonarrService && sonarrService.apiKey) {
|
||||||
fetch(
|
const baseUrl = new URL(sonarrService.url).origin;
|
||||||
`${sonarrService?.url}api/calendar?apikey=${sonarrService?.apiKey}&end=${nextMonth}`
|
fetch(`${baseUrl}/api/calendar?apikey=${sonarrService?.apiKey}&end=${nextMonth}`).then(
|
||||||
).then((response) => {
|
(response) => {
|
||||||
response.ok &&
|
response.ok &&
|
||||||
response.json().then((data) => {
|
response.json().then((data) => {
|
||||||
setSonarrMedias(data);
|
setSonarrMedias(data);
|
||||||
showNotification({
|
showNotification({
|
||||||
title: 'Sonarr',
|
title: 'Sonarr',
|
||||||
icon: <Check />,
|
icon: <Check />,
|
||||||
color: 'green',
|
color: 'green',
|
||||||
autoClose: 1500,
|
autoClose: 1500,
|
||||||
radius: 'md',
|
radius: 'md',
|
||||||
message: `Loaded ${data.length} releases`,
|
message: `Loaded ${data.length} releases`,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
if (radarrService && radarrService.apiKey) {
|
if (radarrService && radarrService.apiKey) {
|
||||||
fetch(
|
const baseUrl = new URL(radarrService.url).origin;
|
||||||
`${radarrService?.url}api/v3/calendar?apikey=${radarrService?.apiKey}&end=${nextMonth}`
|
fetch(`${baseUrl}/api/v3/calendar?apikey=${radarrService?.apiKey}&end=${nextMonth}`).then(
|
||||||
).then((response) => {
|
(response) => {
|
||||||
response.ok &&
|
response.ok &&
|
||||||
response.json().then((data) => {
|
response.json().then((data) => {
|
||||||
setRadarrMedias(data);
|
setRadarrMedias(data);
|
||||||
showNotification({
|
showNotification({
|
||||||
title: 'Radarr',
|
title: 'Radarr',
|
||||||
icon: <Check />,
|
icon: <Check />,
|
||||||
color: 'green',
|
color: 'green',
|
||||||
autoClose: 1500,
|
autoClose: 1500,
|
||||||
radius: 'md',
|
radius: 'md',
|
||||||
message: `Loaded ${data.length} releases`,
|
message: `Loaded ${data.length} releases`,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
}, [config.services]);
|
}, [config.services]);
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ function MediaDisplay(props: { media: IMedia }) {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Group direction="column">
|
<Group direction="column">
|
||||||
<Group>
|
<Group noWrap>
|
||||||
<Title order={3}>{media.title}</Title>
|
<Title order={3}>{media.title}</Title>
|
||||||
<Anchor href={`https://www.imdb.com/title/${media.imdbId}`} target="_blank">
|
<Anchor href={`https://www.imdb.com/title/${media.imdbId}`} target="_blank">
|
||||||
<ActionIcon>
|
<ActionIcon>
|
||||||
@@ -48,7 +48,9 @@ function MediaDisplay(props: { media: IMedia }) {
|
|||||||
Season {media.seasonNumber} episode {media.episodeNumber}
|
Season {media.seasonNumber} episode {media.episodeNumber}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<Text align="justify">{media.overview}</Text>
|
<Text lineClamp={12} align="justify">
|
||||||
|
{media.overview}
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
{/*Add the genres at the bottom of the poster*/}
|
{/*Add the genres at the bottom of the poster*/}
|
||||||
<Group>
|
<Group>
|
||||||
|
|||||||
@@ -21,13 +21,8 @@ export const DateModule: IModule = {
|
|||||||
export default function DateComponent(props: any) {
|
export default function DateComponent(props: any) {
|
||||||
const [date, setDate] = useState(new Date());
|
const [date, setDate] = useState(new Date());
|
||||||
const { config } = useConfig();
|
const { config } = useConfig();
|
||||||
const hours = date.getHours();
|
const isFullTime = config?.modules?.[DateModule.title]?.options?.full?.value ?? false;
|
||||||
const minutes = date.getMinutes();
|
const formatString = isFullTime ? 'HH:mm' : 'h:mm A';
|
||||||
const isFullTime =
|
|
||||||
config.settings[`${DateModule.title}.full`] === undefined
|
|
||||||
? true
|
|
||||||
: config.settings[`${DateModule.title}.full`];
|
|
||||||
const formatString = isFullTime ? 'HH:mm' : 'h:mm a';
|
|
||||||
// Change date on minute change
|
// Change date on minute change
|
||||||
// Note: Using 10 000ms instead of 1000ms to chill a little :)
|
// Note: Using 10 000ms instead of 1000ms to chill a little :)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import { IModule } from './modules';
|
|||||||
export function ModuleWrapper(props: any) {
|
export function ModuleWrapper(props: any) {
|
||||||
const { module }: { module: IModule } = props;
|
const { module }: { module: IModule } = props;
|
||||||
const { config, setConfig } = useConfig();
|
const { config, setConfig } = useConfig();
|
||||||
const enabledModules = config.settings.enabledModules ?? [];
|
const enabledModules = config.modules ?? {};
|
||||||
// Remove 'Module' from enabled modules titles
|
// Remove 'Module' from enabled modules titles
|
||||||
const isShown = enabledModules.includes(module.title);
|
const isShown = enabledModules[module.title]?.enabled ?? false;
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
const items: JSX.Element[] = [];
|
const items: JSX.Element[] = [];
|
||||||
if (module.options) {
|
if (module.options) {
|
||||||
@@ -18,25 +18,31 @@ export function ModuleWrapper(props: any) {
|
|||||||
// Loop over all the types with a for each loop
|
// Loop over all the types with a for each loop
|
||||||
types.forEach((type, index) => {
|
types.forEach((type, index) => {
|
||||||
const optionName = `${module.title}.${keys[index]}`;
|
const optionName = `${module.title}.${keys[index]}`;
|
||||||
|
const moduleInConfig = config.modules?.[module.title];
|
||||||
// TODO: Add support for other types
|
// TODO: Add support for other types
|
||||||
if (type === 'boolean') {
|
if (type === 'boolean') {
|
||||||
items.push(
|
items.push(
|
||||||
<Switch
|
<Switch
|
||||||
defaultChecked={
|
defaultChecked={
|
||||||
// Set default checked to the value of the option if it exists
|
// Set default checked to the value of the option if it exists
|
||||||
config.settings[optionName] ??
|
moduleInConfig?.options?.[keys[index]]?.value ?? false
|
||||||
(module.options && module.options[keys[index]].value) ??
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
defaultValue={config.settings[optionName] ?? false}
|
|
||||||
key={keys[index]}
|
key={keys[index]}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
setConfig({
|
setConfig({
|
||||||
...config,
|
...config,
|
||||||
settings: {
|
modules: {
|
||||||
...config.settings,
|
...config.modules,
|
||||||
enabledModules: [...config.settings.enabledModules],
|
[module.title]: {
|
||||||
[optionName]: e.currentTarget.checked,
|
...config.modules[module.title],
|
||||||
|
options: {
|
||||||
|
...config.modules[module.title].options,
|
||||||
|
[keys[index]]: {
|
||||||
|
...config.modules[module.title].options?.[keys[index]],
|
||||||
|
value: e.currentTarget.checked,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -46,7 +52,6 @@ export function ModuleWrapper(props: any) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Sussy baka
|
|
||||||
if (!isShown) {
|
if (!isShown) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ interface Option {
|
|||||||
[x: string]: OptionValues;
|
[x: string]: OptionValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OptionValues {
|
export interface OptionValues {
|
||||||
name: string;
|
name: string;
|
||||||
value: boolean;
|
value: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const service: serviceItem = {
|
const service: serviceItem = {
|
||||||
|
id: '1',
|
||||||
type: 'Other',
|
type: 'Other',
|
||||||
name: 'YouTube',
|
name: 'YouTube',
|
||||||
icon: 'https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/youtube.png',
|
icon: 'https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/youtube.png',
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ export default function PingComponent(props: any) {
|
|||||||
|
|
||||||
const { url }: { url: string } = props;
|
const { url }: { url: string } = props;
|
||||||
const [isOnline, setOnline] = useState<State>('loading');
|
const [isOnline, setOnline] = useState<State>('loading');
|
||||||
|
const exists = config.modules?.[PingModule.title]?.enabled ?? false;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!config.settings.enabledModules.includes('Ping Services')) {
|
if (!exists) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
axios
|
axios
|
||||||
@@ -32,7 +33,7 @@ export default function PingComponent(props: any) {
|
|||||||
setOnline('down');
|
setOnline('down');
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
if (!config.settings.enabledModules.includes('Ping Services')) {
|
if (!exists) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default function SearchBar(props: any) {
|
|||||||
const { config, setConfig } = useConfig();
|
const { config, setConfig } = useConfig();
|
||||||
const [opened, setOpened] = useState(false);
|
const [opened, setOpened] = useState(false);
|
||||||
const [icon, setIcon] = useState(<Search />);
|
const [icon, setIcon] = useState(<Search />);
|
||||||
const queryUrl = config.settings.searchUrl || 'https://www.google.com/search?q=';
|
const queryUrl = config.settings.searchUrl ?? 'https://www.google.com/search?q=';
|
||||||
const textInput = useRef<HTMLInputElement>();
|
const textInput = useRef<HTMLInputElement>();
|
||||||
useHotkeys([['ctrl+K', () => textInput.current && textInput.current.focus()]]);
|
useHotkeys([['ctrl+K', () => textInput.current && textInput.current.focus()]]);
|
||||||
|
|
||||||
@@ -46,7 +46,10 @@ export default function SearchBar(props: any) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// If enabled modules doesn't contain the module, return null
|
// If enabled modules doesn't contain the module, return null
|
||||||
if (!config.settings.enabledModules.includes(SearchModule.title)) {
|
// If module in enabled
|
||||||
|
|
||||||
|
const exists = config.modules?.[SearchModule.title]?.enabled ?? false;
|
||||||
|
if (!exists) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,9 +132,7 @@ export default function WeatherComponent(props: any) {
|
|||||||
const { config } = useConfig();
|
const { config } = useConfig();
|
||||||
const [weather, setWeather] = useState({} as WeatherResponse);
|
const [weather, setWeather] = useState({} as WeatherResponse);
|
||||||
const isFahrenheit: boolean =
|
const isFahrenheit: boolean =
|
||||||
config.settings[`${WeatherModule.title}.freedomunit`] === undefined
|
config?.modules?.[WeatherModule.title]?.options?.freedomunit?.value ?? false;
|
||||||
? false
|
|
||||||
: config.settings[`${WeatherModule.title}.freedomunit`];
|
|
||||||
|
|
||||||
if ('geolocation' in navigator && location.lat === 0 && location.lng === 0) {
|
if ('geolocation' in navigator && location.lat === 0 && location.lng === 0) {
|
||||||
navigator.geolocation.getCurrentPosition((position) => {
|
navigator.geolocation.getCurrentPosition((position) => {
|
||||||
@@ -163,10 +161,10 @@ export default function WeatherComponent(props: any) {
|
|||||||
<Group spacing={0}>
|
<Group spacing={0}>
|
||||||
<WeatherIcon code={weather.current_weather.weathercode} />
|
<WeatherIcon code={weather.current_weather.weathercode} />
|
||||||
<Space mx="sm" />
|
<Space mx="sm" />
|
||||||
<span>{weather.daily.temperature_2m_max[0]}°C</span>
|
<span>{usePerferedUnit(weather.daily.temperature_2m_max[0])}</span>
|
||||||
<ArrowUpRight size={16} style={{ right: 15 }} />
|
<ArrowUpRight size={16} style={{ right: 15 }} />
|
||||||
<Space mx="sm" />
|
<Space mx="sm" />
|
||||||
<span>{weather.daily.temperature_2m_min[0]}°C</span>
|
<span>{usePerferedUnit(weather.daily.temperature_2m_min[0])}</span>
|
||||||
<ArrowDownRight size={16} />
|
<ArrowDownRight size={16} />
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -78,10 +78,9 @@ export default function NothingFoundBackground() {
|
|||||||
<div className={classes.inner}>
|
<div className={classes.inner}>
|
||||||
<Illustration className={classes.image} />
|
<Illustration className={classes.image} />
|
||||||
<div className={classes.content}>
|
<div className={classes.content}>
|
||||||
<Title className={classes.title}>Nothing to see here</Title>
|
<Title className={classes.title}>Config not found</Title>
|
||||||
<Text color="dimmed" size="lg" align="center" className={classes.description}>
|
<Text color="dimmed" size="lg" align="center" className={classes.description}>
|
||||||
Page you are trying to open does not exist. You may have mistyped the address, or the
|
The config you are trying to access does not exist. Please check the URL and try again.
|
||||||
page has been moved to another URL. If you think this is an error contact support.
|
|
||||||
</Text>
|
</Text>
|
||||||
<Group position="center">
|
<Group position="center">
|
||||||
<NextLink href="/">
|
<NextLink href="/">
|
||||||
|
|||||||
@@ -1,8 +1,54 @@
|
|||||||
import { Title } from '@mantine/core';
|
import { GetServerSidePropsContext } from 'next';
|
||||||
import { useRouter } from 'next/router';
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import AppShelf from '../components/AppShelf/AppShelf';
|
||||||
|
import LoadConfigComponent from '../components/Config/LoadConfig';
|
||||||
|
import { Config } from '../tools/types';
|
||||||
|
import { useConfig } from '../tools/state';
|
||||||
|
|
||||||
export default function SlugPage(props: any) {
|
export async function getServerSideProps(
|
||||||
const router = useRouter();
|
context: GetServerSidePropsContext
|
||||||
const { slug } = router.query;
|
): Promise<{ props: { config: Config } }> {
|
||||||
return <Title>ok</Title>;
|
const configByUrl = context.query.slug;
|
||||||
|
const configPath = path.join(process.cwd(), 'data/configs', `${configByUrl}.json`);
|
||||||
|
const configExists = fs.existsSync(configPath);
|
||||||
|
if (!configExists) {
|
||||||
|
// Redirect to 404
|
||||||
|
context.res.writeHead(301, { Location: '/404' });
|
||||||
|
context.res.end();
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
config: {
|
||||||
|
name: 'Default config',
|
||||||
|
services: [],
|
||||||
|
settings: {
|
||||||
|
searchUrl: 'https://www.google.com/search?q=',
|
||||||
|
},
|
||||||
|
modules: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const config = fs.readFileSync(configPath, 'utf8');
|
||||||
|
// Print loaded config
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
config: JSON.parse(config),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HomePage(props: any) {
|
||||||
|
const { config: initialConfig }: { config: Config } = props;
|
||||||
|
const { setConfig } = useConfig();
|
||||||
|
useEffect(() => {
|
||||||
|
setConfig(initialConfig);
|
||||||
|
}, [initialConfig]);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AppShelf />
|
||||||
|
<LoadConfigComponent />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ const stylesServer = createStylesServer();
|
|||||||
export default class _Document extends Document {
|
export default class _Document extends Document {
|
||||||
static async getInitialProps(ctx: DocumentContext) {
|
static async getInitialProps(ctx: DocumentContext) {
|
||||||
const initialProps = await Document.getInitialProps(ctx);
|
const initialProps = await Document.getInitialProps(ctx);
|
||||||
|
|
||||||
// Add your app specific logic here
|
// Add your app specific logic here
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import AppShelf from '../components/AppShelf/AppShelf';
|
|||||||
import LoadConfigComponent from '../components/Config/LoadConfig';
|
import LoadConfigComponent from '../components/Config/LoadConfig';
|
||||||
import { Config } from '../tools/types';
|
import { Config } from '../tools/types';
|
||||||
import { useConfig } from '../tools/state';
|
import { useConfig } from '../tools/state';
|
||||||
|
import { migrateToIdConfig } from '../tools/migrate';
|
||||||
|
|
||||||
export async function getServerSideProps({
|
export async function getServerSideProps({
|
||||||
req,
|
req,
|
||||||
@@ -26,10 +27,9 @@ export async function getServerSideProps({
|
|||||||
name: cookie.toString(),
|
name: cookie.toString(),
|
||||||
services: [],
|
services: [],
|
||||||
settings: {
|
settings: {
|
||||||
enabledModules: [],
|
|
||||||
searchBar: true,
|
|
||||||
searchUrl: 'https://www.google.com/search?q=',
|
searchUrl: 'https://www.google.com/search?q=',
|
||||||
},
|
},
|
||||||
|
modules: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -48,7 +48,8 @@ export default function HomePage(props: any) {
|
|||||||
const { config: initialConfig }: { config: Config } = props;
|
const { config: initialConfig }: { config: Config } = props;
|
||||||
const { config, loadConfig, setConfig, getConfigs } = useConfig();
|
const { config, loadConfig, setConfig, getConfigs } = useConfig();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setConfig(initialConfig);
|
const migratedConfig = migrateToIdConfig(initialConfig);
|
||||||
|
setConfig(migratedConfig);
|
||||||
}, [initialConfig]);
|
}, [initialConfig]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
14
src/tools/migrate.ts
Normal file
14
src/tools/migrate.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { Config } from './types';
|
||||||
|
|
||||||
|
export function migrateToIdConfig(config: Config): Config {
|
||||||
|
// Set the config and add an ID to all the services that don't have one
|
||||||
|
const services = config.services.map((service) => ({
|
||||||
|
...service,
|
||||||
|
id: service.id ?? uuidv4(),
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
services,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -17,10 +17,9 @@ const configContext = createContext<configContextType>({
|
|||||||
name: 'default',
|
name: 'default',
|
||||||
services: [],
|
services: [],
|
||||||
settings: {
|
settings: {
|
||||||
searchBar: true,
|
searchUrl: 'https://google.com/search?q=',
|
||||||
searchUrl: 'https://www.google.com/search?q=',
|
|
||||||
enabledModules: [],
|
|
||||||
},
|
},
|
||||||
|
modules: {},
|
||||||
},
|
},
|
||||||
setConfig: () => {},
|
setConfig: () => {},
|
||||||
loadConfig: async (name: string) => {},
|
loadConfig: async (name: string) => {},
|
||||||
@@ -44,10 +43,9 @@ export function ConfigProvider({ children }: Props) {
|
|||||||
name: 'default',
|
name: 'default',
|
||||||
services: [],
|
services: [],
|
||||||
settings: {
|
settings: {
|
||||||
searchBar: true,
|
|
||||||
searchUrl: 'https://www.google.com/search?q=',
|
searchUrl: 'https://www.google.com/search?q=',
|
||||||
enabledModules: [],
|
|
||||||
},
|
},
|
||||||
|
modules: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadConfig(configName: string) {
|
async function loadConfig(configName: string) {
|
||||||
|
|||||||
@@ -1,37 +1,49 @@
|
|||||||
|
import { OptionValues } from '../components/modules/modules';
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
searchUrl: string;
|
searchUrl: string;
|
||||||
enabledModules: string[];
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
name: string;
|
name: string;
|
||||||
services: serviceItem[];
|
services: serviceItem[];
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
|
modules: {
|
||||||
|
[key: string]: ConfigModule;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConfigModule {
|
||||||
|
title: string;
|
||||||
|
enabled: boolean;
|
||||||
|
options: {
|
||||||
|
[key: string]: OptionValues;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ServiceTypeList = [
|
export const ServiceTypeList = [
|
||||||
'Other',
|
'Other',
|
||||||
'Sonarr',
|
|
||||||
'Radarr',
|
|
||||||
'Lidarr',
|
|
||||||
'qBittorrent',
|
|
||||||
'Plex',
|
|
||||||
'Emby',
|
'Emby',
|
||||||
|
'Lidarr',
|
||||||
|
'Plex',
|
||||||
|
'Radarr',
|
||||||
|
'Sonarr',
|
||||||
|
'qBittorrent',
|
||||||
];
|
];
|
||||||
export type ServiceType =
|
export type ServiceType =
|
||||||
| 'Other'
|
| 'Other'
|
||||||
| 'Sonarr'
|
| 'Emby'
|
||||||
| 'Radarr'
|
|
||||||
| 'Lidarr'
|
| 'Lidarr'
|
||||||
| 'qBittorrent'
|
|
||||||
| 'Plex'
|
| 'Plex'
|
||||||
| 'Emby';
|
| 'Radarr'
|
||||||
|
| 'Sonarr'
|
||||||
|
| 'qBittorrent';
|
||||||
|
|
||||||
export interface serviceItem {
|
export interface serviceItem {
|
||||||
[x: string]: any;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
url: string;
|
url: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
|
apiKey?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user