Compare commits
99 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2586733a98 | ||
|
|
7bc779b296 | ||
|
|
6064dcb6a6 | ||
|
|
7c7b0cc970 | ||
|
|
c182397dd9 | ||
|
|
dc5ee3bdf3 | ||
|
|
c8e1295a4b | ||
|
|
331c55240b | ||
|
|
65037f9b56 | ||
|
|
39853d79ce | ||
|
|
8530550347 | ||
|
|
ba8e9ef63c | ||
|
|
119f2d7e51 | ||
|
|
b0be26300e | ||
|
|
0400188ea7 | ||
|
|
879581224a | ||
|
|
7e5602c881 | ||
|
|
4870ea3e40 | ||
|
|
61c55acd50 | ||
|
|
c45421d27e | ||
|
|
b396d2604f | ||
|
|
28b6dcd1db | ||
|
|
1dd74ea7da | ||
|
|
64923b03d9 | ||
|
|
2ba9d517a8 | ||
|
|
471a9f7407 | ||
|
|
bdf871b476 | ||
|
|
ab860eeea1 | ||
|
|
50d760f3b8 | ||
|
|
73d06e15fb | ||
|
|
49d57024b9 | ||
|
|
31deb5010f | ||
|
|
e86eb7798f | ||
|
|
2896423766 | ||
|
|
696d0c582d | ||
|
|
e94cae620a | ||
|
|
c9c6f2b0c9 | ||
|
|
b8fe799ac6 | ||
|
|
4cb8539143 | ||
|
|
16b86870c4 | ||
|
|
d4ce2a3ed6 | ||
|
|
a474f3e4ee | ||
|
|
9a49fbb747 | ||
|
|
e3d47d78e0 | ||
|
|
d62189f086 | ||
|
|
bb1b3d7d9a | ||
|
|
13aeeefb22 | ||
|
|
8cdc9c3e29 | ||
|
|
3e31a4d38e | ||
|
|
0cb3db6b89 | ||
|
|
b7e8c51b29 | ||
|
|
e60db9f57a | ||
|
|
2c707e86aa | ||
|
|
5c6541e1a7 | ||
|
|
da81783c8e | ||
|
|
6a90a124b3 | ||
|
|
bd6edbbec6 | ||
|
|
53ab06f97e | ||
|
|
6904018585 | ||
|
|
8c14b3ccf9 | ||
|
|
8557820e6e | ||
|
|
3782499da5 | ||
|
|
97ca45964a | ||
|
|
7fa464b38f | ||
|
|
fe5fa99b4a | ||
|
|
9bf8b337f6 | ||
|
|
06caa2ca5e | ||
|
|
1145ee39b6 | ||
|
|
68111616fe | ||
|
|
7662c11bb5 | ||
|
|
1aaa575480 | ||
|
|
3529e46b11 | ||
|
|
006b1a61bf | ||
|
|
f5eb36ff00 | ||
|
|
a97c9b0c0f | ||
|
|
08daeb87bc | ||
|
|
ebc7ba9684 | ||
|
|
8392dcef20 | ||
|
|
20a37b678f | ||
|
|
d3bd894c2a | ||
|
|
e75ff14975 | ||
|
|
ab1e2a32a0 | ||
|
|
22cd5c8b93 | ||
|
|
5c8b1c4fc4 | ||
|
|
a71b50e33f | ||
|
|
d4c1148025 | ||
|
|
0d11244506 | ||
|
|
e786b1e44f | ||
|
|
509873db55 | ||
|
|
c5178ee288 | ||
|
|
4045628166 | ||
|
|
f8b2d64a26 | ||
|
|
62ba99f6cd | ||
|
|
2fad4d06bd | ||
|
|
c9e58e17da | ||
|
|
8e03719a51 | ||
|
|
c4df55060b | ||
|
|
47c636e810 | ||
|
|
38d18fc433 |
@@ -20,6 +20,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'react/react-in-jsx-scope': 'off',
|
'react/react-in-jsx-scope': 'off',
|
||||||
|
'react/no-children-prop': 'off',
|
||||||
"unused-imports/no-unused-imports": "warn",
|
"unused-imports/no-unused-imports": "warn",
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
"@typescript-eslint/no-unused-imports": "off",
|
"@typescript-eslint/no-unused-imports": "off",
|
||||||
|
|||||||
27
.github/release-note.md
vendored
Normal file
27
.github/release-note.md
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
## 🦞 Homarr [v0.0.0](https://github.com/ajnart/homarr/compare/v0.0.0...v0.0.0) (2022-01-01)
|
||||||
|
|
||||||
|
<!-- Small release message -->
|
||||||
|
|
||||||
|
<!-- Bigger announcement marked in bold -->
|
||||||
|
|
||||||
|
### Upgrade Steps
|
||||||
|
*Upgrading without a mounted config? Make sure to download your config from the settings first! You can add it back later by drag and dropping it into your browser.*
|
||||||
|
* `docker pull ghcr.io/ajnart/homarr:latest`
|
||||||
|
* `docker stop [container_id]`
|
||||||
|
* `docker rm [container_id]`
|
||||||
|
* `docker run --name homarr -p 7575:7575 -v /data/docker/homarr:/app/data/configs -d ghcr.io/ajnart/homarr:latest`
|
||||||
|
* *(or use our [docker_compose.yml](https://github.com/ajnart/homarr#-installation))*
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
### UI Changes
|
||||||
|
|
||||||
|
### GitHub Changes
|
||||||
|
|
||||||
|
### Other Changes
|
||||||
|
|
||||||
|
_**Special thanks to our contributors: @ajnart, @c00ldude1oo, @walkxcode, and of course all people using our project.**_
|
||||||
6
.github/workflows/docker.yml
vendored
6
.github/workflows/docker.yml
vendored
@@ -4,8 +4,12 @@ name: Master docker CI
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
|
paths-ignore:
|
||||||
|
- '.github/**'
|
||||||
|
- '**.md'
|
||||||
tags:
|
tags:
|
||||||
- v*
|
- v*
|
||||||
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -110,6 +114,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
context: .
|
context: .
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
22
.github/workflows/docker_dev.yml
vendored
22
.github/workflows/docker_dev.yml
vendored
@@ -6,7 +6,13 @@ name: Development CI
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [dev]
|
branches: [dev]
|
||||||
|
paths-ignore:
|
||||||
|
- '.github/**'
|
||||||
|
- '**.md'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- '.github/**'
|
||||||
|
- '**.md'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
tags:
|
tags:
|
||||||
@@ -25,13 +31,17 @@ jobs:
|
|||||||
yarn_install_and_build:
|
yarn_install_and_build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Setup
|
- name: Setup
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Get yarn cache directory path
|
- name: Get yarn cache directory path
|
||||||
id: yarn-cache-dir-path
|
id: yarn-cache-dir-path
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||||
|
|
||||||
- name: Yarn cache
|
- name: Yarn cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||||
@@ -39,6 +49,7 @@ jobs:
|
|||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||||
restore-keys: ${{ runner.os }}-yarn-
|
restore-keys: ${{ runner.os }}-yarn-
|
||||||
|
|
||||||
- name: Nextjs cache
|
- name: Nextjs cache
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
@@ -50,8 +61,10 @@ jobs:
|
|||||||
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
|
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
|
||||||
# If source files changed but packages didn't, rebuild from a prior cache.
|
# If source files changed but packages didn't, rebuild from a prior cache.
|
||||||
restore-keys: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
|
restore-keys: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
|
||||||
|
|
||||||
- run: yarn install --frozen-lockfile
|
- run: yarn install --frozen-lockfile
|
||||||
- run: yarn build
|
- run: yarn build
|
||||||
|
|
||||||
- name: Cache build output
|
- name: Cache build output
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2
|
||||||
id: restore-build
|
id: restore-build
|
||||||
@@ -72,8 +85,10 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- uses: actions/cache@v2
|
- uses: actions/cache@v2
|
||||||
id: restore-build
|
id: restore-build
|
||||||
with:
|
with:
|
||||||
@@ -85,6 +100,7 @@ jobs:
|
|||||||
./.next/standalone/
|
./.next/standalone/
|
||||||
./packages.json
|
./packages.json
|
||||||
key: ${{ github.sha }}
|
key: ${{ github.sha }}
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
@@ -95,11 +111,15 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
type=ref,event=pr
|
type=ref,event=pr
|
||||||
tpye=raw,value=dev,priority=1
|
tpye=raw,value=dev,priority=1
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
@@ -111,6 +131,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -35,4 +35,5 @@ yarn-error.log*
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
# storybook
|
# storybook
|
||||||
storybook-static
|
storybook-static
|
||||||
|
data/configs
|
||||||
@@ -3,6 +3,7 @@ module.exports = {
|
|||||||
addons: [
|
addons: [
|
||||||
'storybook-dark-mode',
|
'storybook-dark-mode',
|
||||||
'@storybook/addon-links',
|
'@storybook/addon-links',
|
||||||
|
'storybook-addon-mock/register',
|
||||||
'@storybook/addon-essentials',
|
'@storybook/addon-essentials',
|
||||||
{
|
{
|
||||||
name: 'storybook-addon-turbo-build',
|
name: 'storybook-addon-turbo-build',
|
||||||
|
|||||||
10
Dockerfile
10
Dockerfile
@@ -1,19 +1,13 @@
|
|||||||
FROM node:16-alpine
|
FROM node:16-alpine
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
|
||||||
RUN adduser --system --uid 1001 nextjs
|
|
||||||
|
|
||||||
COPY /next.config.js ./
|
COPY /next.config.js ./
|
||||||
COPY /public ./public
|
COPY /public ./public
|
||||||
COPY /package.json ./package.json
|
COPY /package.json ./package.json
|
||||||
|
# Automatically leverage output traces to reduce image size. https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||||
# Automatically leverage output traces to reduce image size
|
|
||||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
|
||||||
COPY /.next/standalone ./
|
COPY /.next/standalone ./
|
||||||
COPY /.next/static ./.next/static
|
COPY /.next/static ./.next/static
|
||||||
|
|
||||||
EXPOSE 7575
|
EXPOSE 7575
|
||||||
ENV PORT 7575
|
ENV PORT 7575
|
||||||
VOLUME /app/data/configs
|
VOLUME /app/data/configs
|
||||||
CMD ["node", "server.js"]
|
CMD ["node", "server.js"]
|
||||||
|
|||||||
118
README.md
118
README.md
@@ -1,5 +1,5 @@
|
|||||||
<h3 align="center">Homarr</h3>
|
<h3 align="center">Homarr</h3>
|
||||||
<br/>
|
<br>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<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>
|
||||||
@@ -9,14 +9,12 @@
|
|||||||
<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">
|
||||||
<a href="">
|
|
||||||
<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/49837342/168315259-b778c816-10fe-44db-bd25-3eea6f31b233.png" />
|
||||||
<a/>
|
|
||||||
</p>
|
</p>
|
||||||
<p align = "center">
|
<p align = "center">
|
||||||
A homepage for <i>your</i> server.
|
A homepage for <i>your</i> server.
|
||||||
<br/>
|
<br/>
|
||||||
<a href = "https://github.com/ajnart/homarr/deployments/activity_log?environment=Production" > <strong> Demo ↗️ </strong> </a> • <a href = "#-installation" > <strong> Install ➡️ </strong> </a>
|
<a href = "https://homarr.netlify.app/" > <strong> Demo ↗️ </strong> </a> • <a href = "#-installation" > <strong> Install ➡️ </strong> </a>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<i>Join the discord!</i>
|
<i>Join the discord!</i>
|
||||||
@@ -30,11 +28,19 @@
|
|||||||
- [📃 Table of Contents](#-table-of-contents)
|
- [📃 Table of Contents](#-table-of-contents)
|
||||||
- [🚀 Getting Started](#-getting-started)
|
- [🚀 Getting Started](#-getting-started)
|
||||||
- [ℹ️ About](#ℹ️-about)
|
- [ℹ️ About](#ℹ️-about)
|
||||||
- [🐛 Known Issues](#-known-issues)
|
- [💥 Known Issues](#-known-issues)
|
||||||
- [⚡ Installation](#-installation)
|
- [⚡ Installation](#-installation)
|
||||||
- [Deploying from Docker Image 🐳](#deploying-from-docker-image-)
|
- [🐳 Deploying from Docker Image](#-deploying-from-docker-image)
|
||||||
- [Building from Source 🛠️](#building-from-source-️)
|
- [🛠️ Building from Source](#%EF%B8%8F-building-from-source)
|
||||||
|
- [🔧 Configuration](#-configuration)
|
||||||
|
- [🧩 Integrations](#--integrations)
|
||||||
|
- [🧑🤝🧑 Multiple Configs](#-multiple-configs)
|
||||||
|
- [🐻 Icons](#-icons)
|
||||||
|
- [📊 Modules](#-modules)
|
||||||
|
- [🔍 Search Bar](#-search-bar)
|
||||||
- [💖 Contributing](#-contributing)
|
- [💖 Contributing](#-contributing)
|
||||||
|
- [🍏 Request Icons](#-request-icons)
|
||||||
|
|
||||||
|
|
||||||
<!-- Getting Started -->
|
<!-- Getting Started -->
|
||||||
# 🚀 Getting Started
|
# 🚀 Getting Started
|
||||||
@@ -45,9 +51,16 @@ Homarr is a simple and lightweight homepage for your server, that helps you easi
|
|||||||
|
|
||||||
**[⤴️ Back to Top](#-table-of-contents)**
|
**[⤴️ Back to Top](#-table-of-contents)**
|
||||||
|
|
||||||
|
## 💥 Known Issues
|
||||||
|
- 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)**
|
||||||
|
|
||||||
## ⚡ Installation
|
## ⚡ Installation
|
||||||
|
|
||||||
### Deploying from Docker Image 🐳
|
### 🐳 Deploying from Docker Image
|
||||||
> Supported architectures: x86-64, ARM, ARM64
|
> Supported architectures: x86-64, ARM, ARM64
|
||||||
|
|
||||||
_Requirements_:
|
_Requirements_:
|
||||||
@@ -78,7 +91,7 @@ services:
|
|||||||
|
|
||||||
***Getting EACCESS errors in the logs? Try running `sudo chmod 775 /directory-you-mounted-to`!***
|
***Getting EACCESS errors in the logs? Try running `sudo chmod 775 /directory-you-mounted-to`!***
|
||||||
|
|
||||||
### Building from Source 🛠️
|
### 🛠️ Building from Source
|
||||||
|
|
||||||
_Requirements_:
|
_Requirements_:
|
||||||
- [Git](https://git-scm.com/downloads)
|
- [Git](https://git-scm.com/downloads)
|
||||||
@@ -93,9 +106,96 @@ _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``*
|
||||||
|
|
||||||
|
## 🔧 Configuration
|
||||||
|
|
||||||
|
### 🧩 Integrations
|
||||||
|
|
||||||
|
Homarr natively integrates with your services. Here is a list of all supported services.
|
||||||
|
|
||||||
|
**Emby**
|
||||||
|
*The Emby integration is still in development.*
|
||||||
|
|
||||||
|
**Lidarr**
|
||||||
|
*The Lidarr integration is still in development.*
|
||||||
|
|
||||||
|
**Sonarr**
|
||||||
|
*Sonarr needs an API key.*<br>
|
||||||
|
Make a new API key in `Advanced > Security > Create new API key`<br>
|
||||||
|
**Current integration:** Upcoming media is displayed in the **Calendar** module.
|
||||||
|
|
||||||
|
**Plex**
|
||||||
|
*The Plex integration is still in development.*
|
||||||
|
|
||||||
|
**Radarr**
|
||||||
|
*Radarr needs an API key.*<br>
|
||||||
|
Make a new API key in `Advanced > Security > Create new API key`<br>
|
||||||
|
**Current integration:** Upcoming media is displayed in the **Calendar** module.
|
||||||
|
|
||||||
|
**qBittorent**
|
||||||
|
*The qBittorent integration is still in development.*
|
||||||
|
|
||||||
|
**[⤴️ Back to Top](#-table-of-contents)**
|
||||||
|
|
||||||
|
### 🧑🤝🧑 Multiple Configs
|
||||||
|
|
||||||
|
Homarr allows the usage of multiple configs. You can add a new config in two ways.
|
||||||
|
|
||||||
|
**Drag-and-Drop**
|
||||||
|
1. Download your config from the Homarr settings.
|
||||||
|
2. Change the name of the `.json` file and the name in the `.json` file to any name you want *(just make sure it's different)*.
|
||||||
|
3. Drag-and-Drop the file into the Homarr tab in your browser.
|
||||||
|
4. Change the config in settings.
|
||||||
|
|
||||||
|
**Using a filebrowser**
|
||||||
|
1. Locate your mounted `default.json` file.
|
||||||
|
2. Duplicate your `default.json` file.
|
||||||
|
3. Change the name of the `.json` file and the name in the `.json` file to any name you want *(just make sure it's different)*.
|
||||||
|
4. Refresh the Homarr tab in your browser.
|
||||||
|
5. Change the config in settings.
|
||||||
|
|
||||||
|
**[⤴️ Back to Top](#-table-of-contents)**
|
||||||
|
|
||||||
|
### 🐻 Icons
|
||||||
|
|
||||||
|
The icons used in Homarr are automatically requested from the [dashboard-icons](https://github.com/walkxhub/dashboard-icons) repo.
|
||||||
|
|
||||||
|
Icons are requested in the following way: <br>
|
||||||
|
`Grab name > Replace ' ' with '-' > .toLower() > https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/{name}.png`
|
||||||
|
|
||||||
|
**[⤴️ Back to Top](#-table-of-contents)**
|
||||||
|
|
||||||
|
### 📊 Modules
|
||||||
|
|
||||||
|
Modules are blocks shown on the sides of the Homarr dashboard that display information. They can be enabled in settings.
|
||||||
|
|
||||||
|
**Clock Module**
|
||||||
|
The Clock Module will display your current time and date.
|
||||||
|
|
||||||
|
**Calendar Module**
|
||||||
|
The Calendar Module uses [integrations](#--integrations-1) to display new content.
|
||||||
|
|
||||||
|
**Weather Module**
|
||||||
|
The Weather Module uses your devices location to display the current, highest, and lowest temperature.
|
||||||
|
|
||||||
|
**[⤴️ Back to Top](#-table-of-contents)**
|
||||||
|
|
||||||
|
### 🔍 Search Bar
|
||||||
|
|
||||||
|
The Search Bar will open any Search Query after the Query URL you've specified in settings.
|
||||||
|
|
||||||
|
*(Eg. `https://www.google.com/search?q=*Your Query will be inserted here*`)*
|
||||||
|
|
||||||
|
**[⤴️ Back to Top](#-table-of-contents)**
|
||||||
|
|
||||||
# 💖 Contributing
|
# 💖 Contributing
|
||||||
**Please read our [Contribution Guidelines](/CONTRIBUTING.md)**
|
**Please read our [Contribution Guidelines](/CONTRIBUTING.md)**
|
||||||
|
|
||||||
All contributions are highly appreciated.
|
All contributions are highly appreciated.
|
||||||
|
|
||||||
|
**[⤴️ Back to Top](#-table-of-contents)**
|
||||||
|
|
||||||
|
## 🍏 Request Icons
|
||||||
|
|
||||||
|
The icons used in Homarr are automatically requested from the [dashboard-icons](https://github.com/walkxhub/dashboard-icons) repo. You can make a icon request by creating an [issue](https://github.com/walkxhub/dashboard-icons/issues/new/choose).
|
||||||
|
|
||||||
**[⤴️ Back to Top](#-table-of-contents)**
|
**[⤴️ Back to Top](#-table-of-contents)**
|
||||||
|
|||||||
@@ -1,12 +1,26 @@
|
|||||||
{
|
{
|
||||||
"name": "config",
|
"name": "config",
|
||||||
"services": [],
|
"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": {
|
"settings": {
|
||||||
"searchBar": true,
|
"searchBar": true,
|
||||||
"searchUrl": "https://duckduckgo.com/?q=",
|
"searchUrl": "Custom",
|
||||||
"enabledModules": [
|
"enabledModules": [
|
||||||
"Date",
|
"Date",
|
||||||
"Calendar"
|
"Calendar",
|
||||||
|
"Weather"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
export const REPO_URL = 'ajnart/homarr';
|
export const REPO_URL = 'ajnart/homarr';
|
||||||
export const CURRENT_VERSION = 'v0.3.0';
|
export const CURRENT_VERSION = 'v0.4.0';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homarr",
|
"name": "homarr",
|
||||||
"version": "0.3.0",
|
"version": "0.4.0",
|
||||||
"private": "false",
|
"private": "false",
|
||||||
"description": "Homarr - A homepage for your server.",
|
"description": "Homarr - A homepage for your server.",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -79,9 +79,13 @@
|
|||||||
"eslint-plugin-unused-imports": "^2.0.0",
|
"eslint-plugin-unused-imports": "^2.0.0",
|
||||||
"jest": "^27.5.1",
|
"jest": "^27.5.1",
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^2.6.2",
|
||||||
|
"storybook-addon-mock": "^2.3.2",
|
||||||
"storybook-addon-turbo-build": "^1.1.0",
|
"storybook-addon-turbo-build": "^1.1.0",
|
||||||
"storybook-dark-mode": "^1.0.9",
|
"storybook-dark-mode": "^1.0.9",
|
||||||
"ts-jest": "^27.1.4",
|
"ts-jest": "^27.1.4",
|
||||||
"typescript": "4.6.3"
|
"typescript": "4.6.3"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"@types/react": "17.0.30"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
LoadingOverlay,
|
LoadingOverlay,
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
Title,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
@@ -28,9 +29,9 @@ export function AddItemShelfButton(props: any) {
|
|||||||
<Modal
|
<Modal
|
||||||
size="xl"
|
size="xl"
|
||||||
radius="md"
|
radius="md"
|
||||||
|
title={<Title order={3}>Add service</Title>}
|
||||||
opened={props.opened || opened}
|
opened={props.opened || opened}
|
||||||
onClose={() => setOpened(false)}
|
onClose={() => setOpened(false)}
|
||||||
title="Add a service"
|
|
||||||
>
|
>
|
||||||
<AddAppShelfItemForm setOpened={setOpened} />
|
<AddAppShelfItemForm setOpened={setOpened} />
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -153,7 +154,14 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Center>
|
<Center>
|
||||||
<Image height={120} width={120} src={form.values.icon} alt="Placeholder" withPlaceholder />
|
<Image
|
||||||
|
height={120}
|
||||||
|
width={120}
|
||||||
|
fit="contain"
|
||||||
|
src={form.values.icon}
|
||||||
|
alt="Placeholder"
|
||||||
|
withPlaceholder
|
||||||
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.onSubmit(() => {
|
onSubmit={form.onSubmit(() => {
|
||||||
|
|||||||
@@ -1,39 +1,46 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Text, AspectRatio, SimpleGrid, Card, Image, useMantineTheme } from '@mantine/core';
|
import { Text, AspectRatio, Card, Image, Center, Grid, createStyles } from '@mantine/core';
|
||||||
import { useConfig } from '../../tools/state';
|
import { useConfig } from '../../tools/state';
|
||||||
import { serviceItem } from '../../tools/types';
|
import { serviceItem } from '../../tools/types';
|
||||||
import AppShelfMenu from './AppShelfMenu';
|
import AppShelfMenu from './AppShelfMenu';
|
||||||
|
import PingComponent from '../modules/ping/PingModule';
|
||||||
|
|
||||||
const AppShelf = () => {
|
const useStyles = createStyles((theme) => ({
|
||||||
|
item: {
|
||||||
|
transition: 'box-shadow 150ms ease, transform 100ms ease',
|
||||||
|
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow: `${theme.shadows.md} !important`,
|
||||||
|
transform: 'scale(1.05)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const AppShelf = (props: any) => {
|
||||||
const { config } = useConfig();
|
const { config } = useConfig();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SimpleGrid
|
<Grid gutter="xl" align="center">
|
||||||
cols={7}
|
|
||||||
spacing="xl"
|
|
||||||
breakpoints={[
|
|
||||||
{ maxWidth: 2400, cols: 6, spacing: 'xl' },
|
|
||||||
{ maxWidth: 1800, cols: 5, spacing: 'xl' },
|
|
||||||
{ maxWidth: 1500, cols: 4, spacing: 'lg' },
|
|
||||||
{ maxWidth: 800, cols: 3, spacing: 'md' },
|
|
||||||
{ maxWidth: 400, cols: 3, spacing: 'sm' },
|
|
||||||
{ maxWidth: 400, cols: 2, spacing: 'sm' },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{config.services.map((service) => (
|
{config.services.map((service) => (
|
||||||
<AppShelfItem key={service.name} service={service} />
|
<Grid.Col key={service.name} span={6} xl={2} xs={4} sm={3} md={3}>
|
||||||
|
<AppShelfItem key={service.name} service={service} />
|
||||||
|
</Grid.Col>
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</Grid>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AppShelfItem(props: any) {
|
export function AppShelfItem(props: any) {
|
||||||
const { service }: { service: serviceItem } = props;
|
const { service }: { service: serviceItem } = props;
|
||||||
const [hovering, setHovering] = useState(false);
|
const [hovering, setHovering] = useState(false);
|
||||||
const theme = useMantineTheme();
|
const { classes, theme } = useStyles();
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<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}
|
key={service.name}
|
||||||
onHoverStart={() => {
|
onHoverStart={() => {
|
||||||
setHovering(true);
|
setHovering(true);
|
||||||
@@ -42,23 +49,16 @@ export function AppShelfItem(props: any) {
|
|||||||
setHovering(false);
|
setHovering(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Card
|
<Card withBorder radius="lg" shadow="md" className={classes.item}>
|
||||||
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"
|
|
||||||
>
|
|
||||||
<Card.Section>
|
<Card.Section>
|
||||||
<Text mt="sm" align="center" lineClamp={1} weight={500}>
|
<Text mt="sm" align="center" lineClamp={1} weight={550}>
|
||||||
{service.name}
|
{service.name}
|
||||||
</Text>
|
</Text>
|
||||||
<motion.div
|
<motion.div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 5,
|
top: 15,
|
||||||
right: 5,
|
right: 15,
|
||||||
alignSelf: 'flex-end',
|
alignSelf: 'flex-end',
|
||||||
}}
|
}}
|
||||||
animate={{
|
animate={{
|
||||||
@@ -68,34 +68,36 @@ export function AppShelfItem(props: any) {
|
|||||||
<AppShelfMenu service={service} />
|
<AppShelfMenu service={service} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</Card.Section>
|
</Card.Section>
|
||||||
<Card.Section>
|
<Center>
|
||||||
<AspectRatio
|
<Card.Section>
|
||||||
ratio={3 / 5}
|
<AspectRatio
|
||||||
m="xl"
|
ratio={3 / 5}
|
||||||
style={{
|
m="xl"
|
||||||
width: 150,
|
style={{
|
||||||
height: 90,
|
width: 150,
|
||||||
}}
|
height: 90,
|
||||||
>
|
|
||||||
<motion.i
|
|
||||||
whileHover={{
|
|
||||||
cursor: 'pointer',
|
|
||||||
scale: 1.1,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Image
|
<motion.i
|
||||||
style={{
|
whileHover={{
|
||||||
maxWidth: 80,
|
cursor: 'pointer',
|
||||||
|
scale: 1.1,
|
||||||
}}
|
}}
|
||||||
fit="contain"
|
>
|
||||||
onClick={() => {
|
<Image
|
||||||
window.open(service.url);
|
width={80}
|
||||||
}}
|
height={80}
|
||||||
src={service.icon}
|
src={service.icon}
|
||||||
/>
|
fit="contain"
|
||||||
</motion.i>
|
onClick={() => {
|
||||||
</AspectRatio>
|
window.open(service.url);
|
||||||
</Card.Section>
|
}}
|
||||||
|
/>
|
||||||
|
</motion.i>
|
||||||
|
</AspectRatio>
|
||||||
|
<PingComponent url={service.url} />
|
||||||
|
</Card.Section>
|
||||||
|
</Center>
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Menu, Modal, Text } from '@mantine/core';
|
import { Menu, Modal, Text, useMantineTheme } from '@mantine/core';
|
||||||
import { showNotification } from '@mantine/notifications';
|
import { showNotification } from '@mantine/notifications';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Check, Edit, Trash } from 'tabler-icons-react';
|
import { Check, Edit, Trash } from 'tabler-icons-react';
|
||||||
@@ -8,12 +8,13 @@ import { AddAppShelfItemForm } from './AddAppShelfItem';
|
|||||||
export default function AppShelfMenu(props: any) {
|
export default function AppShelfMenu(props: any) {
|
||||||
const { service } = props;
|
const { service } = props;
|
||||||
const { config, setConfig } = useConfig();
|
const { config, setConfig } = useConfig();
|
||||||
|
const theme = useMantineTheme();
|
||||||
const [opened, setOpened] = useState(false);
|
const [opened, setOpened] = useState(false);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
size="xl"
|
size="xl"
|
||||||
radius="lg"
|
radius="md"
|
||||||
opened={props.opened || opened}
|
opened={props.opened || opened}
|
||||||
onClose={() => setOpened(false)}
|
onClose={() => setOpened(false)}
|
||||||
title="Modify a service"
|
title="Modify a service"
|
||||||
@@ -28,7 +29,16 @@ export default function AppShelfMenu(props: any) {
|
|||||||
message="Save service"
|
message="Save service"
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Menu position="right">
|
<Menu
|
||||||
|
position="right"
|
||||||
|
radius="md"
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
backgroundColor:
|
||||||
|
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Menu.Label>Settings</Menu.Label>
|
<Menu.Label>Settings</Menu.Label>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createStyles, Switch, Group, useMantineColorScheme } from '@mantine/core';
|
import { createStyles, Switch, Group, useMantineColorScheme, Kbd } from '@mantine/core';
|
||||||
import { Sun, MoonStars } from 'tabler-icons-react';
|
import { Sun, MoonStars } from 'tabler-icons-react';
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => ({
|
const useStyles = createStyles((theme) => ({
|
||||||
@@ -40,6 +40,9 @@ export function ColorSchemeSwitch() {
|
|||||||
<Switch checked={colorScheme === 'dark'} onChange={() => toggleColorScheme()} size="md" />
|
<Switch checked={colorScheme === 'dark'} onChange={() => toggleColorScheme()} size="md" />
|
||||||
</div>
|
</div>
|
||||||
Switch to {colorScheme === 'dark' ? 'light' : 'dark'} mode
|
Switch to {colorScheme === 'dark' ? 'light' : 'dark'} mode
|
||||||
|
<Group spacing={2}>
|
||||||
|
<Kbd>Ctrl</Kbd>+<Kbd>J</Kbd>
|
||||||
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
import { TextInput, Text, Popover, Box } from '@mantine/core';
|
|
||||||
import { useForm } from '@mantine/hooks';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { Search, BrandYoutube, Download } from 'tabler-icons-react';
|
|
||||||
import { useConfig } from '../../tools/state';
|
|
||||||
|
|
||||||
export default function SearchBar(props: any) {
|
|
||||||
const { config, setConfig } = useConfig();
|
|
||||||
const [opened, setOpened] = useState(false);
|
|
||||||
const [icon, setIcon] = useState(<Search />);
|
|
||||||
const querryUrl = config.settings.searchUrl || 'https://www.google.com/search?q=';
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
initialValues: {
|
|
||||||
querry: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (config.settings.searchBar === false) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
mb={"xl"}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<form
|
|
||||||
onChange={() => {
|
|
||||||
// If querry contains !yt or !t add "Searching on YouTube" or "Searching torrent"
|
|
||||||
const querry = form.values.querry.trim();
|
|
||||||
const isYoutube = querry.startsWith('!yt');
|
|
||||||
const isTorrent = querry.startsWith('!t');
|
|
||||||
if (isYoutube) {
|
|
||||||
setIcon(<BrandYoutube size={22} />);
|
|
||||||
} else if (isTorrent) {
|
|
||||||
setIcon(<Download size={22} />);
|
|
||||||
} else {
|
|
||||||
setIcon(<Search size={22} />);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onSubmit={form.onSubmit((values) => {
|
|
||||||
// Find if querry is prefixed by !yt or !t
|
|
||||||
const querry = values.querry.trim();
|
|
||||||
const isYoutube = querry.startsWith('!yt');
|
|
||||||
const isTorrent = querry.startsWith('!t');
|
|
||||||
if (isYoutube) {
|
|
||||||
window.open(`https://www.youtube.com/results?search_query=${querry.substring(3)}`);
|
|
||||||
} else if (isTorrent) {
|
|
||||||
window.open(`https://bitsearch.to/search?q=${querry.substring(3)}`);
|
|
||||||
} else {
|
|
||||||
window.open(`${querryUrl}${values.querry}`);
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Popover
|
|
||||||
opened={opened}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
position="bottom"
|
|
||||||
placement="start"
|
|
||||||
withArrow
|
|
||||||
trapFocus={false}
|
|
||||||
transition="pop-top-left"
|
|
||||||
onFocusCapture={() => setOpened(true)}
|
|
||||||
onBlurCapture={() => setOpened(false)}
|
|
||||||
target={
|
|
||||||
<TextInput
|
|
||||||
variant="filled"
|
|
||||||
color="blue"
|
|
||||||
icon={icon}
|
|
||||||
radius="md"
|
|
||||||
size="md"
|
|
||||||
placeholder="Search the web"
|
|
||||||
{...props}
|
|
||||||
{...form.getInputProps('querry')}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Text>
|
|
||||||
tip: Use the prefixes <b>!yt</b> and <b>!t</b> in front of your query to search on YouTube or for a Torrent respectively.
|
|
||||||
</Text>
|
|
||||||
</Popover>
|
|
||||||
</form>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,19 +2,15 @@ import {
|
|||||||
ActionIcon,
|
ActionIcon,
|
||||||
Group,
|
Group,
|
||||||
Modal,
|
Modal,
|
||||||
Switch,
|
|
||||||
Title,
|
Title,
|
||||||
Text,
|
Text,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
SegmentedControl,
|
SegmentedControl,
|
||||||
Indicator,
|
|
||||||
Alert,
|
|
||||||
TextInput,
|
TextInput,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useColorScheme } from '@mantine/hooks';
|
import { useColorScheme } from '@mantine/hooks';
|
||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { AlertCircle, Settings as SettingsIcon } from 'tabler-icons-react';
|
import { Settings as SettingsIcon } from 'tabler-icons-react';
|
||||||
import { CURRENT_VERSION, REPO_URL } from '../../../data/constants';
|
|
||||||
import { useConfig } from '../../tools/state';
|
import { useConfig } from '../../tools/state';
|
||||||
import { ColorSchemeSwitch } from '../ColorSchemeToggle/ColorSchemeSwitch';
|
import { ColorSchemeSwitch } from '../ColorSchemeToggle/ColorSchemeSwitch';
|
||||||
import ConfigChanger from '../Config/ConfigChanger';
|
import ConfigChanger from '../Config/ConfigChanger';
|
||||||
@@ -40,14 +36,6 @@ function SettingsMenu(props: any) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Group direction="column" grow>
|
<Group direction="column" grow>
|
||||||
<Alert
|
|
||||||
icon={<AlertCircle size={16} />}
|
|
||||||
title="Update available"
|
|
||||||
radius="lg"
|
|
||||||
hidden={current === latest}
|
|
||||||
>
|
|
||||||
Version {latest} is available. Current: {current}
|
|
||||||
</Alert>
|
|
||||||
<Group grow direction="column" spacing={0}>
|
<Group grow direction="column" spacing={0}>
|
||||||
<Text>Search engine</Text>
|
<Text>Search engine</Text>
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
@@ -74,8 +62,8 @@ function SettingsMenu(props: any) {
|
|||||||
/>
|
/>
|
||||||
{searchUrl === 'Custom' && (
|
{searchUrl === 'Custom' && (
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Querry URL"
|
label="Query URL"
|
||||||
placeholder="Custom querry url"
|
placeholder="Custom query url"
|
||||||
value={customSearchUrl}
|
value={customSearchUrl}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setCustomSearchUrl(event.currentTarget.value);
|
setCustomSearchUrl(event.currentTarget.value);
|
||||||
@@ -90,22 +78,6 @@ function SettingsMenu(props: any) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
<Group direction="column">
|
|
||||||
<Switch
|
|
||||||
size="md"
|
|
||||||
onChange={(e) =>
|
|
||||||
setConfig({
|
|
||||||
...config,
|
|
||||||
settings: {
|
|
||||||
...config.settings,
|
|
||||||
searchBar: e.currentTarget.checked,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
checked={config.settings.searchBar}
|
|
||||||
label="Enable search bar"
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
<ModuleEnabler />
|
<ModuleEnabler />
|
||||||
<ColorSchemeSwitch />
|
<ColorSchemeSwitch />
|
||||||
<ConfigChanger />
|
<ConfigChanger />
|
||||||
@@ -125,29 +97,17 @@ function SettingsMenu(props: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SettingsMenuButton(props: any) {
|
export function SettingsMenuButton(props: any) {
|
||||||
const [update, setUpdate] = useState(false);
|
|
||||||
const [opened, setOpened] = useState(false);
|
const [opened, setOpened] = useState(false);
|
||||||
const [latestVersion, setLatestVersion] = useState(CURRENT_VERSION);
|
|
||||||
useEffect(() => {
|
|
||||||
// Fetch Data here when component first mounted
|
|
||||||
fetch(`https://api.github.com/repos/${REPO_URL}/releases/latest`).then((res) => {
|
|
||||||
res.json().then((data) => {
|
|
||||||
setLatestVersion(data.tag_name);
|
|
||||||
if (data.tag_name !== CURRENT_VERSION) {
|
|
||||||
setUpdate(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
size="md"
|
size="xl"
|
||||||
|
radius="md"
|
||||||
title={<Title order={3}>Settings</Title>}
|
title={<Title order={3}>Settings</Title>}
|
||||||
opened={props.opened || opened}
|
opened={props.opened || opened}
|
||||||
onClose={() => setOpened(false)}
|
onClose={() => setOpened(false)}
|
||||||
>
|
>
|
||||||
<SettingsMenu current={CURRENT_VERSION} latest={latestVersion} />
|
<SettingsMenu />
|
||||||
</Modal>
|
</Modal>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="default"
|
variant="default"
|
||||||
@@ -158,14 +118,7 @@ export function SettingsMenuButton(props: any) {
|
|||||||
onClick={() => setOpened(true)}
|
onClick={() => setOpened(true)}
|
||||||
>
|
>
|
||||||
<Tooltip label="Settings">
|
<Tooltip label="Settings">
|
||||||
<Indicator
|
<SettingsIcon />
|
||||||
size={12}
|
|
||||||
disabled={CURRENT_VERSION === latestVersion}
|
|
||||||
offset={-3}
|
|
||||||
position="top-end"
|
|
||||||
>
|
|
||||||
<SettingsIcon />
|
|
||||||
</Indicator>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Aside as MantineAside, Group } from '@mantine/core';
|
import { Aside as MantineAside, Group } from '@mantine/core';
|
||||||
import { CalendarModule } from '../modules/calendar/CalendarModule';
|
import { WeatherModule, DateModule, CalendarModule } from '../modules';
|
||||||
import ModuleWrapper from '../modules/moduleWrapper';
|
import { ModuleWrapper } from '../modules/moduleWrapper';
|
||||||
|
|
||||||
export default function Aside() {
|
export default function Aside(props: any) {
|
||||||
return (
|
return (
|
||||||
<MantineAside
|
<MantineAside
|
||||||
hiddenBreakpoint="md"
|
hiddenBreakpoint="md"
|
||||||
@@ -14,8 +14,10 @@ export default function Aside() {
|
|||||||
base: 'auto',
|
base: 'auto',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Group mt="sm" direction="column">
|
<Group mt="sm" grow direction="column">
|
||||||
<ModuleWrapper module={CalendarModule} />
|
<ModuleWrapper module={CalendarModule} />
|
||||||
|
<ModuleWrapper module={DateModule} />
|
||||||
|
<ModuleWrapper module={WeatherModule} />
|
||||||
</Group>
|
</Group>
|
||||||
</MantineAside>
|
</MantineAside>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
createStyles,
|
createStyles,
|
||||||
Anchor,
|
Anchor,
|
||||||
@@ -6,8 +6,11 @@ import {
|
|||||||
Group,
|
Group,
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Footer as FooterComponent,
|
Footer as FooterComponent,
|
||||||
|
Alert,
|
||||||
|
useMantineTheme,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { BrandGithub } from 'tabler-icons-react';
|
import { AlertCircle, BrandGithub } from 'tabler-icons-react';
|
||||||
|
import { CURRENT_VERSION, REPO_URL } from '../../../data/constants';
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => ({
|
const useStyles = createStyles((theme) => ({
|
||||||
footer: {
|
footer: {
|
||||||
@@ -40,6 +43,8 @@ interface FooterCenteredProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Footer({ links }: FooterCenteredProps) {
|
export function Footer({ links }: FooterCenteredProps) {
|
||||||
|
const [update, setUpdate] = useState(false);
|
||||||
|
const theme = useMantineTheme();
|
||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
const items = links.map((link) => (
|
const items = links.map((link) => (
|
||||||
<Anchor<'a'>
|
<Anchor<'a'>
|
||||||
@@ -54,39 +59,87 @@ export function Footer({ links }: FooterCenteredProps) {
|
|||||||
</Anchor>
|
</Anchor>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
const [latestVersion, setLatestVersion] = useState(CURRENT_VERSION);
|
||||||
|
const [isOpen, setOpen] = useState(true);
|
||||||
|
useEffect(() => {
|
||||||
|
// Fetch Data here when component first mounted
|
||||||
|
fetch(`https://api.github.com/repos/${REPO_URL}/releases/latest`).then((res) => {
|
||||||
|
res.json().then((data) => {
|
||||||
|
setLatestVersion(data.tag_name);
|
||||||
|
if (data.tag_name !== CURRENT_VERSION) {
|
||||||
|
setUpdate(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FooterComponent height="auto" style={{ border: 'none' }}>
|
<FooterComponent
|
||||||
<Group
|
p={5}
|
||||||
sx={{
|
height="auto"
|
||||||
position: 'fixed',
|
style={{
|
||||||
bottom: 0,
|
background: 'none',
|
||||||
right: 15,
|
border: 'none',
|
||||||
}}
|
clear: 'both',
|
||||||
direction="row"
|
position: 'fixed',
|
||||||
align="center"
|
bottom: '0',
|
||||||
mb={15}
|
left: '0',
|
||||||
>
|
}}
|
||||||
<Group className={classes.links}>{items}</Group>
|
>
|
||||||
<Group spacing="xs" position="right" noWrap>
|
<Group position="apart" direction="row" style={{ alignItems: 'end' }} mr="xs" mb="xs">
|
||||||
<ActionIcon<'a'> component="a" href="https://github.com/ajnart/homarr" size="lg">
|
<Group position="left">
|
||||||
<BrandGithub size={18} />
|
<Alert
|
||||||
</ActionIcon>
|
// onClick open latest release page
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
icon={<AlertCircle size={16} />}
|
||||||
|
title={`Updated version: ${latestVersion} is available. Current version: ${CURRENT_VERSION}`}
|
||||||
|
withCloseButton
|
||||||
|
radius="lg"
|
||||||
|
hidden={CURRENT_VERSION === latestVersion || !isOpen}
|
||||||
|
variant="outline"
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
|
||||||
|
},
|
||||||
|
|
||||||
|
closeButton: {
|
||||||
|
marginLeft: '5px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
children={undefined}
|
||||||
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
<Text
|
<Group position="right">
|
||||||
style={{
|
<Group spacing={0}>
|
||||||
fontSize: '0.90rem',
|
<ActionIcon<'a'> component="a" href="https://github.com/ajnart/homarr" size="lg">
|
||||||
textAlign: 'center',
|
<BrandGithub size={18} />
|
||||||
color: '#a0aec0',
|
</ActionIcon>
|
||||||
}}
|
<Text
|
||||||
>
|
style={{
|
||||||
Made with ❤️ by @
|
position: 'relative',
|
||||||
<Anchor
|
fontSize: '0.90rem',
|
||||||
href="https://github.com/ajnart"
|
color: 'gray',
|
||||||
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}
|
}}
|
||||||
|
>
|
||||||
|
{CURRENT_VERSION}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: '0.90rem',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#a0aec0',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
ajnart
|
Made with ❤️ by @
|
||||||
</Anchor>
|
<Anchor
|
||||||
</Text>
|
href="https://github.com/ajnart"
|
||||||
|
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}
|
||||||
|
>
|
||||||
|
ajnart
|
||||||
|
</Anchor>
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
</FooterComponent>
|
</FooterComponent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,117 +1,35 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createStyles, Header as Head, Group, Drawer, Center } from '@mantine/core';
|
import { createStyles, Header as Head, Group, Box } from '@mantine/core';
|
||||||
import { useBooleanToggle } from '@mantine/hooks';
|
|
||||||
import { NextLink } from '@mantine/next';
|
|
||||||
import { Logo } from './Logo';
|
import { Logo } from './Logo';
|
||||||
import CalendarComponent from '../modules/calendar/CalendarModule';
|
import SearchBar from '../modules/search/SearchModule';
|
||||||
import { SettingsMenuButton } from '../Settings/SettingsMenu';
|
|
||||||
import { AddItemShelfButton } from '../AppShelf/AddAppShelfItem';
|
import { AddItemShelfButton } from '../AppShelf/AddAppShelfItem';
|
||||||
|
import { SettingsMenuButton } from '../Settings/SettingsMenu';
|
||||||
|
|
||||||
const HEADER_HEIGHT = 60;
|
const HEADER_HEIGHT = 60;
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => ({
|
const useStyles = createStyles((theme) => ({
|
||||||
root: {
|
hide: {
|
||||||
position: 'relative',
|
[theme.fn.smallerThan('xs')]: {
|
||||||
zIndex: 1,
|
|
||||||
},
|
|
||||||
|
|
||||||
dropdown: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: HEADER_HEIGHT,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
zIndex: 0,
|
|
||||||
borderTopRightRadius: 0,
|
|
||||||
borderTopLeftRadius: 0,
|
|
||||||
borderTopWidth: 0,
|
|
||||||
overflow: 'hidden',
|
|
||||||
|
|
||||||
[theme.fn.largerThan('md')]: {
|
|
||||||
display: 'none',
|
display: 'none',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
header: {
|
|
||||||
display: 'flex',
|
|
||||||
height: '100%',
|
|
||||||
},
|
|
||||||
|
|
||||||
links: {
|
|
||||||
[theme.fn.smallerThan('md')]: {
|
|
||||||
display: 'none',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
burger: {
|
|
||||||
[theme.fn.largerThan('md')]: {
|
|
||||||
display: 'none',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
link: {
|
|
||||||
display: 'block',
|
|
||||||
lineHeight: 1,
|
|
||||||
padding: '8px 12px',
|
|
||||||
borderRadius: theme.radius.sm,
|
|
||||||
textDecoration: 'none',
|
|
||||||
color: theme.colorScheme === 'dark' ? theme.colors.dark[0] : theme.colors.gray[7],
|
|
||||||
fontSize: theme.fontSizes.sm,
|
|
||||||
fontWeight: 500,
|
|
||||||
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0],
|
|
||||||
},
|
|
||||||
|
|
||||||
[theme.fn.smallerThan('sm')]: {
|
|
||||||
borderRadius: 0,
|
|
||||||
padding: theme.spacing.md,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
linkActive: {
|
|
||||||
'&, &:hover': {
|
|
||||||
backgroundColor:
|
|
||||||
theme.colorScheme === 'dark'
|
|
||||||
? theme.fn.rgba(theme.colors[theme.primaryColor][9], 0.25)
|
|
||||||
: theme.colors[theme.primaryColor][0],
|
|
||||||
color: theme.colors[theme.primaryColor][theme.colorScheme === 'dark' ? 3 : 7],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface HeaderResponsiveProps {
|
export function Header(props: any) {
|
||||||
links: { link: string; label: string }[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Header({ links }: HeaderResponsiveProps) {
|
|
||||||
const [opened, toggleOpened] = useBooleanToggle(false);
|
|
||||||
const { classes, cx } = useStyles();
|
const { classes, cx } = useStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Head height={HEADER_HEIGHT}>
|
<Head height="auto">
|
||||||
<Group direction="row" align="center" position="apart" className={classes.header} mx="xl">
|
<Group m="xs" position="apart">
|
||||||
<NextLink style={{ textDecoration: 'none' }} href="/">
|
<Box className={classes.hide}>
|
||||||
<Logo style={{ fontSize: 22 }} />
|
<Logo style={{ fontSize: 22 }} />
|
||||||
</NextLink>
|
</Box>
|
||||||
<Group>
|
<Group noWrap>
|
||||||
|
<SearchBar />
|
||||||
<SettingsMenuButton />
|
<SettingsMenuButton />
|
||||||
<AddItemShelfButton />
|
<AddItemShelfButton />
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Drawer
|
|
||||||
opened={opened}
|
|
||||||
overlayOpacity={0.55}
|
|
||||||
overlayBlur={3}
|
|
||||||
onClose={() => toggleOpened()}
|
|
||||||
position="right"
|
|
||||||
>
|
|
||||||
{opened ?? (
|
|
||||||
<Center>
|
|
||||||
<CalendarComponent />
|
|
||||||
</Center>
|
|
||||||
)}
|
|
||||||
</Drawer>
|
|
||||||
</Head>
|
</Head>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,24 @@
|
|||||||
import { AppShell, Center, createStyles } from '@mantine/core';
|
import { AppShell, createStyles } from '@mantine/core';
|
||||||
import { Header } from './Header';
|
import { Header } from './Header';
|
||||||
import { Footer } from './Footer';
|
import { Footer } from './Footer';
|
||||||
import Aside from './Aside';
|
import Aside from './Aside';
|
||||||
import Navbar from './Navbar';
|
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => ({
|
const useStyles = createStyles((theme) => ({
|
||||||
main: {
|
main: {},
|
||||||
[theme.fn.largerThan('md')]: {
|
|
||||||
maxWidth: 1500,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export default function Layout({ children, style }: any) {
|
export default function Layout({ children, style }: any) {
|
||||||
const { classes, cx } = useStyles();
|
const { classes, cx } = useStyles();
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<AppShell aside={<Aside />} header={<Header />} footer={<Footer links={[]} />}>
|
||||||
navbar={<Navbar />}
|
<main
|
||||||
aside={<Aside />}
|
className={cx(classes.main)}
|
||||||
header={<Header links={[]} />}
|
style={{
|
||||||
footer={<Footer links={[]} />}
|
...style,
|
||||||
>
|
}}
|
||||||
<Center>
|
>
|
||||||
<main
|
{children}
|
||||||
className={cx(classes.main)}
|
</main>
|
||||||
style={{
|
|
||||||
...style,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</main>
|
|
||||||
</Center>
|
|
||||||
</AppShell>
|
</AppShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { Group, Image, Text } from '@mantine/core';
|
import { Group, Image, Text } from '@mantine/core';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { CURRENT_VERSION } from '../../../data/constants';
|
|
||||||
|
|
||||||
export function Logo({ style }: any) {
|
export function Logo({ style }: any) {
|
||||||
return (
|
return (
|
||||||
<Group>
|
<Group spacing="xs">
|
||||||
<Image
|
<Image
|
||||||
width={50}
|
width={50}
|
||||||
src="/imgs/logo.png"
|
src="/imgs/logo.png"
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
left: 15,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text
|
||||||
@@ -21,20 +19,6 @@ export function Logo({ style }: any) {
|
|||||||
>
|
>
|
||||||
Homarr
|
Homarr
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
position: 'relative',
|
|
||||||
left: -14,
|
|
||||||
bottom: -2,
|
|
||||||
color: 'gray',
|
|
||||||
fontStyle: 'inherit',
|
|
||||||
fontSize: 'inherit',
|
|
||||||
alignSelf: 'center',
|
|
||||||
alignContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{CURRENT_VERSION}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Group, Navbar as MantineNavbar } from '@mantine/core';
|
import { Group, Navbar as MantineNavbar } from '@mantine/core';
|
||||||
import { DateModule } from '../modules/date/DateModule';
|
import { WeatherModule, DateModule } from '../modules';
|
||||||
import ModuleWrapper from '../modules/moduleWrapper';
|
import { ModuleWrapper } from '../modules/moduleWrapper';
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
return (
|
return (
|
||||||
@@ -14,8 +14,10 @@ export default function Navbar() {
|
|||||||
base: 'auto',
|
base: 'auto',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Group mt="sm" direction="column">
|
<Group mt="sm" direction="column" align="center">
|
||||||
<ModuleWrapper module={DateModule} />
|
<ModuleWrapper module={DateModule} />
|
||||||
|
<ModuleWrapper module={WeatherModule} />
|
||||||
|
<ModuleWrapper module={WeatherModule} />
|
||||||
</Group>
|
</Group>
|
||||||
</MantineNavbar>
|
</MantineNavbar>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable react/no-children-prop */
|
/* eslint-disable react/no-children-prop */
|
||||||
import { Popover, Box, ScrollArea, Divider, Indicator } from '@mantine/core';
|
import { Popover, Box, ScrollArea, Divider, Indicator, useMantineTheme } from '@mantine/core';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Calendar } from '@mantine/dates';
|
import { Calendar } from '@mantine/dates';
|
||||||
import { showNotification } from '@mantine/notifications';
|
import { showNotification } from '@mantine/notifications';
|
||||||
@@ -93,6 +93,7 @@ function DayComponent(props: any) {
|
|||||||
radarrmedias,
|
radarrmedias,
|
||||||
}: { renderdate: Date; sonarrmedias: []; radarrmedias: [] } = props;
|
}: { renderdate: Date; sonarrmedias: []; radarrmedias: [] } = props;
|
||||||
const [opened, setOpened] = useState(false);
|
const [opened, setOpened] = useState(false);
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
|
||||||
const day = renderdate.getDate();
|
const day = renderdate.getDate();
|
||||||
// Itterate over the medias and filter the ones that are on the same day
|
// Itterate over the medias and filter the ones that are on the same day
|
||||||
@@ -126,8 +127,7 @@ function DayComponent(props: any) {
|
|||||||
width={700}
|
width={700}
|
||||||
onClose={() => setOpened(false)}
|
onClose={() => setOpened(false)}
|
||||||
opened={opened}
|
opened={opened}
|
||||||
// TODO: Fix this !! WTF ?
|
target={day}
|
||||||
target={` ${day}`}
|
|
||||||
>
|
>
|
||||||
<ScrollArea style={{ height: 400 }}>
|
<ScrollArea style={{ height: 400 }}>
|
||||||
{sonarrFiltered.map((media: any, index: number) => (
|
{sonarrFiltered.map((media: any, index: number) => (
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Group, Text, Title } from '@mantine/core';
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Clock } from 'tabler-icons-react';
|
import { Clock } from 'tabler-icons-react';
|
||||||
|
import { useConfig } from '../../../tools/state';
|
||||||
import { IModule } from '../modules';
|
import { IModule } from '../modules';
|
||||||
|
|
||||||
export const DateModule: IModule = {
|
export const DateModule: IModule = {
|
||||||
@@ -9,33 +10,36 @@ export const DateModule: IModule = {
|
|||||||
description: 'Show the current time and date in a card',
|
description: 'Show the current time and date in a card',
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
component: DateComponent,
|
component: DateComponent,
|
||||||
|
options: {
|
||||||
|
full: {
|
||||||
|
name: 'Display full time (24-hour)',
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
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 hours = date.getHours();
|
const hours = date.getHours();
|
||||||
const minutes = date.getMinutes();
|
const minutes = date.getMinutes();
|
||||||
|
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(() => {
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
setDate(new Date());
|
setDate(new Date());
|
||||||
}, 10000);
|
}, 1000 * 60);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group p="sm" direction="column">
|
<Group p="sm" direction="column">
|
||||||
<Title>
|
<Title>{dayjs(date).format(formatString)}</Title>
|
||||||
{hours < 10 ? `0${hours}` : hours}:{minutes < 10 ? `0${minutes}` : minutes}
|
<Text size="xl">{dayjs(date).format('dddd, MMMM D')}</Text>
|
||||||
</Title>
|
|
||||||
<Text size="xl">
|
|
||||||
{
|
|
||||||
// Use dayjs to format the date
|
|
||||||
// https://day.js.org/en/getting-started/installation/
|
|
||||||
dayjs(date).format('dddd, MMMM D')
|
|
||||||
}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,5 @@
|
|||||||
export * from './date';
|
export * from './date';
|
||||||
export * from './calendar';
|
export * from './calendar';
|
||||||
|
export * from './search';
|
||||||
|
export * from './ping';
|
||||||
|
export * from './weather';
|
||||||
|
|||||||
@@ -1,28 +1,82 @@
|
|||||||
import { Card, useMantineTheme } from '@mantine/core';
|
import { Card, Menu, Switch, useMantineTheme } from '@mantine/core';
|
||||||
import { useConfig } from '../../tools/state';
|
import { useConfig } from '../../tools/state';
|
||||||
import { IModule } from './modules';
|
import { IModule } from './modules';
|
||||||
|
|
||||||
export default function ModuleWrapper(props: any) {
|
export function ModuleWrapper(props: any) {
|
||||||
const { module }: { module: IModule } = props;
|
const { module }: { module: IModule } = props;
|
||||||
const { config } = useConfig();
|
const { config, setConfig } = useConfig();
|
||||||
const enabledModules = config.settings.enabledModules ?? [];
|
const enabledModules = config.settings.enabledModules ?? [];
|
||||||
// Remove 'Module' from enabled modules titles
|
// Remove 'Module' from enabled modules titles
|
||||||
const isShown = enabledModules.includes(module.title);
|
const isShown = enabledModules.includes(module.title);
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
|
const items: JSX.Element[] = [];
|
||||||
|
if (module.options) {
|
||||||
|
const keys = Object.keys(module.options);
|
||||||
|
const values = Object.values(module.options);
|
||||||
|
// Get the value and the name of the option
|
||||||
|
const types = values.map((v) => typeof v.value);
|
||||||
|
// Loop over all the types with a for each loop
|
||||||
|
types.forEach((type, index) => {
|
||||||
|
const optionName = `${module.title}.${keys[index]}`;
|
||||||
|
// TODO: Add support for other types
|
||||||
|
if (type === 'boolean') {
|
||||||
|
items.push(
|
||||||
|
<Switch
|
||||||
|
defaultChecked={
|
||||||
|
// Set default checked to the value of the option if it exists
|
||||||
|
config.settings[optionName] ??
|
||||||
|
(module.options && module.options[keys[index]].value) ??
|
||||||
|
false
|
||||||
|
}
|
||||||
|
defaultValue={config.settings[optionName] ?? false}
|
||||||
|
key={keys[index]}
|
||||||
|
onClick={(e) => {
|
||||||
|
setConfig({
|
||||||
|
...config,
|
||||||
|
settings: {
|
||||||
|
...config.settings,
|
||||||
|
enabledModules: [...config.settings.enabledModules],
|
||||||
|
[optionName]: e.currentTarget.checked,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
label={values[index].name}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Sussy baka
|
||||||
if (!isShown) {
|
if (!isShown) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card hidden={!isShown} mx="sm" withBorder radius="lg" shadow="sm">
|
||||||
hidden={!isShown}
|
{module.options && (
|
||||||
mx="sm"
|
<Menu
|
||||||
radius="lg"
|
size="md"
|
||||||
shadow="sm"
|
shadow="xl"
|
||||||
style={{
|
closeOnItemClick={false}
|
||||||
// Make background color of the card depend on the theme
|
radius="md"
|
||||||
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[5] : 'white',
|
position="left"
|
||||||
}}
|
styles={{
|
||||||
>
|
root: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 15,
|
||||||
|
right: 15,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
backgroundColor:
|
||||||
|
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menu.Label>Settings</Menu.Label>
|
||||||
|
{items.map((item) => (
|
||||||
|
<Menu.Item key={item.key}>{item}</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
<module.component />
|
<module.component />
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,5 +7,14 @@ export interface IModule {
|
|||||||
description: string;
|
description: string;
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
component: React.ComponentType;
|
component: React.ComponentType;
|
||||||
props?: any;
|
options?: Option;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Option {
|
||||||
|
[x: string]: OptionValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OptionValues {
|
||||||
|
name: string;
|
||||||
|
value: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
15
src/components/modules/ping/PingModule.story.tsx
Normal file
15
src/components/modules/ping/PingModule.story.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { serviceItem } from '../../../tools/types';
|
||||||
|
import PingComponent from './PingModule';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Modules/Search bar',
|
||||||
|
};
|
||||||
|
|
||||||
|
const service: serviceItem = {
|
||||||
|
type: 'Other',
|
||||||
|
name: 'YouTube',
|
||||||
|
icon: 'https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/youtube.png',
|
||||||
|
url: 'https://youtube.com/',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Default = (args: any) => <PingComponent service={service} />;
|
||||||
59
src/components/modules/ping/PingModule.tsx
Normal file
59
src/components/modules/ping/PingModule.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { Indicator, Tooltip } from '@mantine/core';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Plug } from 'tabler-icons-react';
|
||||||
|
import { useConfig } from '../../../tools/state';
|
||||||
|
import { IModule } from '../modules';
|
||||||
|
|
||||||
|
export const PingModule: IModule = {
|
||||||
|
title: 'Ping Services',
|
||||||
|
description: 'Pings your services and shows their status as an indicator',
|
||||||
|
icon: Plug,
|
||||||
|
component: PingComponent,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PingComponent(props: any) {
|
||||||
|
type State = 'loading' | 'down' | 'online';
|
||||||
|
const { config } = useConfig();
|
||||||
|
|
||||||
|
const { url }: { url: string } = props;
|
||||||
|
const [isOnline, setOnline] = useState<State>('loading');
|
||||||
|
useEffect(() => {
|
||||||
|
if (!config.settings.enabledModules.includes('Ping Services')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
axios
|
||||||
|
.get('/api/modules/ping', { params: { url } })
|
||||||
|
.then(() => {
|
||||||
|
setOnline('online');
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setOnline('down');
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
if (!config.settings.enabledModules.includes('Ping Services')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
radius="lg"
|
||||||
|
style={{ position: 'absolute', bottom: 20, right: 20 }}
|
||||||
|
label={isOnline === 'loading' ? 'Loading...' : isOnline === 'online' ? 'Online' : 'Offline'}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
animate={{
|
||||||
|
scale: isOnline === 'online' ? [1, 0.8, 1] : 1,
|
||||||
|
}}
|
||||||
|
transition={{ repeat: Infinity, duration: 2.5, ease: 'easeInOut' }}
|
||||||
|
>
|
||||||
|
<Indicator
|
||||||
|
size={13}
|
||||||
|
color={isOnline === 'online' ? 'green' : isOnline === 'down' ? 'red' : 'yellow'}
|
||||||
|
>
|
||||||
|
{null}
|
||||||
|
</Indicator>
|
||||||
|
</motion.div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
src/components/modules/ping/index.ts
Normal file
1
src/components/modules/ping/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { PingModule } from './PingModule';
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import SearchBar from './SearchBar';
|
import SearchBar from './SearchModule';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Search bar',
|
title: 'Search bar',
|
||||||
118
src/components/modules/search/SearchModule.tsx
Normal file
118
src/components/modules/search/SearchModule.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { TextInput, Kbd, createStyles, Text, Popover } from '@mantine/core';
|
||||||
|
import { useForm, useHotkeys } from '@mantine/hooks';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { Search, BrandYoutube, Download } from 'tabler-icons-react';
|
||||||
|
import { useConfig } from '../../../tools/state';
|
||||||
|
import { IModule } from '../modules';
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme) => ({
|
||||||
|
hide: {
|
||||||
|
[theme.fn.smallerThan('sm')]: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const SearchModule: IModule = {
|
||||||
|
title: 'Search Bar',
|
||||||
|
description: 'Show the current time and date in a card',
|
||||||
|
icon: Search,
|
||||||
|
component: SearchBar,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SearchBar(props: any) {
|
||||||
|
const { config, setConfig } = useConfig();
|
||||||
|
const [opened, setOpened] = useState(false);
|
||||||
|
const [icon, setIcon] = useState(<Search />);
|
||||||
|
const queryUrl = config.settings.searchUrl || 'https://www.google.com/search?q=';
|
||||||
|
const textInput = useRef<HTMLInputElement>();
|
||||||
|
useHotkeys([['ctrl+K', () => textInput.current && textInput.current.focus()]]);
|
||||||
|
|
||||||
|
const { classes, cx } = useStyles();
|
||||||
|
const rightSection = (
|
||||||
|
<div className={classes.hide}>
|
||||||
|
<Kbd>Ctrl</Kbd>
|
||||||
|
<span style={{ margin: '0 5px' }}>+</span>
|
||||||
|
<Kbd>K</Kbd>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
query: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// If enabled modules doesn't contain the module, return null
|
||||||
|
if (!config.settings.enabledModules.includes(SearchModule.title)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onChange={() => {
|
||||||
|
// If query contains !yt or !t add "Searching on YouTube" or "Searching torrent"
|
||||||
|
const query = form.values.query.trim();
|
||||||
|
const isYoutube = query.startsWith('!yt');
|
||||||
|
const isTorrent = query.startsWith('!t');
|
||||||
|
if (isYoutube) {
|
||||||
|
setIcon(<BrandYoutube size={22} />);
|
||||||
|
} else if (isTorrent) {
|
||||||
|
setIcon(<Download size={22} />);
|
||||||
|
} else {
|
||||||
|
setIcon(<Search size={22} />);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onSubmit={form.onSubmit((values) => {
|
||||||
|
const query = values.query.trim();
|
||||||
|
const isYoutube = query.startsWith('!yt');
|
||||||
|
const isTorrent = query.startsWith('!t');
|
||||||
|
form.setValues({ query: '' });
|
||||||
|
setTimeout(() => {
|
||||||
|
if (isYoutube) {
|
||||||
|
window.open(`https://www.youtube.com/results?search_query=${query.substring(3)}`);
|
||||||
|
} else if (isTorrent) {
|
||||||
|
window.open(`https://bitsearch.to/search?q=${query.substring(3)}`);
|
||||||
|
} else {
|
||||||
|
window.open(`${queryUrl}${values.query}`);
|
||||||
|
}
|
||||||
|
}, 20);
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Popover
|
||||||
|
opened={opened}
|
||||||
|
position="bottom"
|
||||||
|
placement="start"
|
||||||
|
width={260}
|
||||||
|
withArrow
|
||||||
|
radius="md"
|
||||||
|
trapFocus={false}
|
||||||
|
transition="pop-bottom-right"
|
||||||
|
onFocusCapture={() => setOpened(true)}
|
||||||
|
onBlurCapture={() => setOpened(false)}
|
||||||
|
target={
|
||||||
|
<TextInput
|
||||||
|
variant="filled"
|
||||||
|
icon={icon}
|
||||||
|
ref={textInput}
|
||||||
|
rightSectionWidth={90}
|
||||||
|
rightSection={rightSection}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
styles={{ rightSection: { pointerEvents: 'none' } }}
|
||||||
|
placeholder="Search the web..."
|
||||||
|
{...props}
|
||||||
|
{...form.getInputProps('query')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text>
|
||||||
|
tip: Use the prefixes <b>!yt</b> and <b>!t</b> in front of your query to search on YouTube
|
||||||
|
or for a Torrent respectively.
|
||||||
|
</Text>
|
||||||
|
</Popover>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
src/components/modules/search/index.ts
Normal file
1
src/components/modules/search/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { SearchModule } from './SearchModule';
|
||||||
41
src/components/modules/weather/WeatherInterface.ts
Normal file
41
src/components/modules/weather/WeatherInterface.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// To parse this data:
|
||||||
|
//
|
||||||
|
// import { Convert, WeatherResponse } from "./file";
|
||||||
|
//
|
||||||
|
// const weatherResponse = Convert.toWeatherResponse(json);
|
||||||
|
//
|
||||||
|
// These functions will throw an error if the JSON doesn't
|
||||||
|
// match the expected interface, even if the JSON is valid.
|
||||||
|
|
||||||
|
export interface WeatherResponse {
|
||||||
|
current_weather: CurrentWeather;
|
||||||
|
utc_offset_seconds: number;
|
||||||
|
latitude: number;
|
||||||
|
elevation: number;
|
||||||
|
longitude: number;
|
||||||
|
generationtime_ms: number;
|
||||||
|
daily_units: DailyUnits;
|
||||||
|
daily: Daily;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CurrentWeather {
|
||||||
|
winddirection: number;
|
||||||
|
windspeed: number;
|
||||||
|
time: string;
|
||||||
|
weathercode: number;
|
||||||
|
temperature: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Daily {
|
||||||
|
temperature_2m_max: number[];
|
||||||
|
time: Date[];
|
||||||
|
temperature_2m_min: number[];
|
||||||
|
weathercode: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DailyUnits {
|
||||||
|
temperature_2m_max: string;
|
||||||
|
temperature_2m_min: string;
|
||||||
|
time: string;
|
||||||
|
weathercode: string;
|
||||||
|
}
|
||||||
174
src/components/modules/weather/WeatherModule.tsx
Normal file
174
src/components/modules/weather/WeatherModule.tsx
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import { Group, Space, Title, Tooltip } from '@mantine/core';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
ArrowDownRight,
|
||||||
|
ArrowUpRight,
|
||||||
|
Cloud,
|
||||||
|
CloudFog,
|
||||||
|
CloudRain,
|
||||||
|
CloudSnow,
|
||||||
|
CloudStorm,
|
||||||
|
QuestionMark,
|
||||||
|
Snowflake,
|
||||||
|
Sun,
|
||||||
|
} from 'tabler-icons-react';
|
||||||
|
import { useConfig } from '../../../tools/state';
|
||||||
|
import { IModule } from '../modules';
|
||||||
|
import { WeatherResponse } from './WeatherInterface';
|
||||||
|
|
||||||
|
export const WeatherModule: IModule = {
|
||||||
|
title: 'Weather (beta)',
|
||||||
|
description: 'Look up the current weather in your location',
|
||||||
|
icon: Sun,
|
||||||
|
component: WeatherComponent,
|
||||||
|
options: {
|
||||||
|
freedomunit: {
|
||||||
|
name: 'Display in Fahrenheit',
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 0 Clear sky
|
||||||
|
// 1, 2, 3 Mainly clear, partly cloudy, and overcast
|
||||||
|
// 45, 48 Fog and depositing rime fog
|
||||||
|
// 51, 53, 55 Drizzle: Light, moderate, and dense intensity
|
||||||
|
// 56, 57 Freezing Drizzle: Light and dense intensity
|
||||||
|
// 61, 63, 65 Rain: Slight, moderate and heavy intensity
|
||||||
|
// 66, 67 Freezing Rain: Light and heavy intensity
|
||||||
|
// 71, 73, 75 Snow fall: Slight, moderate, and heavy intensity
|
||||||
|
// 77 Snow grains
|
||||||
|
// 80, 81, 82 Rain showers: Slight, moderate, and violent
|
||||||
|
// 85, 86Snow showers slight and heavy
|
||||||
|
// 95 *Thunderstorm: Slight or moderate
|
||||||
|
// 96, 99 *Thunderstorm with slight and heavy hail
|
||||||
|
export function WeatherIcon(props: any) {
|
||||||
|
const { code } = props;
|
||||||
|
let data: { icon: any; name: string };
|
||||||
|
switch (code) {
|
||||||
|
case 0: {
|
||||||
|
data = { icon: Sun, name: 'Clear' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
|
case 2:
|
||||||
|
case 3: {
|
||||||
|
data = { icon: Cloud, name: 'Mainly clear' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 45:
|
||||||
|
case 48: {
|
||||||
|
data = { icon: CloudFog, name: 'Fog' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 51:
|
||||||
|
case 53:
|
||||||
|
case 55: {
|
||||||
|
data = { icon: Cloud, name: 'Drizzle' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 56:
|
||||||
|
case 57: {
|
||||||
|
data = { icon: Snowflake, name: 'Freezing drizzle' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 61:
|
||||||
|
case 63:
|
||||||
|
case 65: {
|
||||||
|
data = { icon: CloudRain, name: 'Rain' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 66:
|
||||||
|
case 67: {
|
||||||
|
data = { icon: CloudRain, name: 'Freezing rain' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 71:
|
||||||
|
case 73:
|
||||||
|
case 75: {
|
||||||
|
data = { icon: CloudSnow, name: 'Snow fall' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 77: {
|
||||||
|
data = { icon: CloudSnow, name: 'Snow grains' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 80:
|
||||||
|
case 81:
|
||||||
|
case 82: {
|
||||||
|
data = { icon: CloudRain, name: 'Rain showers' };
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 85:
|
||||||
|
case 86: {
|
||||||
|
data = { icon: CloudSnow, name: 'Snow showers' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 95: {
|
||||||
|
data = { icon: CloudStorm, name: 'Thunderstorm' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 96:
|
||||||
|
case 99: {
|
||||||
|
data = { icon: CloudStorm, name: 'Thunderstorm with hail' };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
data = { icon: QuestionMark, name: 'Unknown' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Tooltip label={data.name}>
|
||||||
|
<data.icon size={50} />
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WeatherComponent(props: any) {
|
||||||
|
// Get location from browser
|
||||||
|
const [location, setLocation] = useState({ lat: 0, lng: 0 });
|
||||||
|
const { config } = useConfig();
|
||||||
|
const [weather, setWeather] = useState({} as WeatherResponse);
|
||||||
|
const isFahrenheit: boolean =
|
||||||
|
config.settings[`${WeatherModule.title}.freedomunit`] === undefined
|
||||||
|
? false
|
||||||
|
: config.settings[`${WeatherModule.title}.freedomunit`];
|
||||||
|
|
||||||
|
if ('geolocation' in navigator && location.lat === 0 && location.lng === 0) {
|
||||||
|
navigator.geolocation.getCurrentPosition((position) => {
|
||||||
|
setLocation({ lat: position.coords.latitude, lng: position.coords.longitude });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
axios
|
||||||
|
.get(
|
||||||
|
`https://api.open-meteo.com/v1/forecast?latitude=${location.lat}&longitude=${location.lng}&daily=weathercode,temperature_2m_max,temperature_2m_min¤t_weather=true&timezone=Europe%2FLondon`
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
setWeather(res.data);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
if (!weather.current_weather) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function usePerferedUnit(value: number): string {
|
||||||
|
return isFahrenheit ? `${(value * (9 / 5)).toFixed(1)}°F` : `${value.toFixed(1)}°C`;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Group position="left" direction="column">
|
||||||
|
<Title>{usePerferedUnit(weather.current_weather.temperature)}</Title>
|
||||||
|
<Group spacing={0}>
|
||||||
|
<WeatherIcon code={weather.current_weather.weathercode} />
|
||||||
|
<Space mx="sm" />
|
||||||
|
<span>{weather.daily.temperature_2m_max[0]}°C</span>
|
||||||
|
<ArrowUpRight size={16} style={{ right: 15 }} />
|
||||||
|
<Space mx="sm" />
|
||||||
|
<span>{weather.daily.temperature_2m_min[0]}°C</span>
|
||||||
|
<ArrowDownRight size={16} />
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
src/components/modules/weather/index.ts
Normal file
1
src/components/modules/weather/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { WeatherModule } from './WeatherModule';
|
||||||
95
src/pages/404.tsx
Normal file
95
src/pages/404.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
createStyles,
|
||||||
|
Container,
|
||||||
|
Title,
|
||||||
|
Text,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
useMantineTheme,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { NextLink } from '@mantine/next';
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
paddingTop: 80,
|
||||||
|
paddingBottom: 80,
|
||||||
|
},
|
||||||
|
|
||||||
|
inner: {
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
|
||||||
|
image: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
left: 0,
|
||||||
|
zIndex: 0,
|
||||||
|
opacity: 0.75,
|
||||||
|
},
|
||||||
|
|
||||||
|
content: {
|
||||||
|
paddingTop: 220,
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 1,
|
||||||
|
|
||||||
|
[theme.fn.smallerThan('sm')]: {
|
||||||
|
paddingTop: 120,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
title: {
|
||||||
|
fontFamily: `Greycliff CF, ${theme.fontFamily}`,
|
||||||
|
textAlign: 'center',
|
||||||
|
fontWeight: 900,
|
||||||
|
fontSize: 38,
|
||||||
|
|
||||||
|
[theme.fn.smallerThan('sm')]: {
|
||||||
|
fontSize: 32,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
description: {
|
||||||
|
maxWidth: 540,
|
||||||
|
margin: 'auto',
|
||||||
|
marginTop: theme.spacing.xl,
|
||||||
|
marginBottom: theme.spacing.xl * 1.5,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export function Illustration(props: React.ComponentPropsWithoutRef<'svg'>) {
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 362 145" {...props}>
|
||||||
|
<path
|
||||||
|
fill={theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0]}
|
||||||
|
d="M62.6 142c-2.133 0-3.2-1.067-3.2-3.2V118h-56c-2 0-3-1-3-3V92.8c0-1.333.4-2.733 1.2-4.2L58.2 4c.8-1.333 2.067-2 3.8-2h28c2 0 3 1 3 3v85.4h11.2c.933 0 1.733.333 2.4 1 .667.533 1 1.267 1 2.2v21.2c0 .933-.333 1.733-1 2.4-.667.533-1.467.8-2.4.8H93v20.8c0 2.133-1.067 3.2-3.2 3.2H62.6zM33 90.4h26.4V51.2L33 90.4zM181.67 144.6c-7.333 0-14.333-1.333-21-4-6.666-2.667-12.866-6.733-18.6-12.2-5.733-5.467-10.266-13-13.6-22.6-3.333-9.6-5-20.667-5-33.2 0-12.533 1.667-23.6 5-33.2 3.334-9.6 7.867-17.133 13.6-22.6 5.734-5.467 11.934-9.533 18.6-12.2 6.667-2.8 13.667-4.2 21-4.2 7.467 0 14.534 1.4 21.2 4.2 6.667 2.667 12.8 6.733 18.4 12.2 5.734 5.467 10.267 13 13.6 22.6 3.334 9.6 5 20.667 5 33.2 0 12.533-1.666 23.6-5 33.2-3.333 9.6-7.866 17.133-13.6 22.6-5.6 5.467-11.733 9.533-18.4 12.2-6.666 2.667-13.733 4-21.2 4zm0-31c9.067 0 15.6-3.733 19.6-11.2 4.134-7.6 6.2-17.533 6.2-29.8s-2.066-22.2-6.2-29.8c-4.133-7.6-10.666-11.4-19.6-11.4-8.933 0-15.466 3.8-19.6 11.4-4 7.6-6 17.533-6 29.8s2 22.2 6 29.8c4.134 7.467 10.667 11.2 19.6 11.2zM316.116 142c-2.134 0-3.2-1.067-3.2-3.2V118h-56c-2 0-3-1-3-3V92.8c0-1.333.4-2.733 1.2-4.2l56.6-84.6c.8-1.333 2.066-2 3.8-2h28c2 0 3 1 3 3v85.4h11.2c.933 0 1.733.333 2.4 1 .666.533 1 1.267 1 2.2v21.2c0 .933-.334 1.733-1 2.4-.667.533-1.467.8-2.4.8h-11.2v20.8c0 2.133-1.067 3.2-3.2 3.2h-27.2zm-29.6-51.6h26.4V51.2l-26.4 39.2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NothingFoundBackground() {
|
||||||
|
const { classes } = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className={classes.root}>
|
||||||
|
<div className={classes.inner}>
|
||||||
|
<Illustration className={classes.image} />
|
||||||
|
<div className={classes.content}>
|
||||||
|
<Title className={classes.title}>Nothing to see here</Title>
|
||||||
|
<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
|
||||||
|
page has been moved to another URL. If you think this is an error contact support.
|
||||||
|
</Text>
|
||||||
|
<Group position="center">
|
||||||
|
<NextLink href="/">
|
||||||
|
<Button size="md">Take me back to home page</Button>
|
||||||
|
</NextLink>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
src/pages/[slug].tsx
Normal file
8
src/pages/[slug].tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Title } from '@mantine/core';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
|
export default function SlugPage(props: any) {
|
||||||
|
const router = useRouter();
|
||||||
|
const { slug } = router.query;
|
||||||
|
return <Title>ok</Title>;
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ export default function App(props: AppProps & { colorScheme: ColorScheme }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Homarr - A homepage for your server!</title>
|
<title>Homarr 🦞</title>
|
||||||
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
|
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
|
||||||
<link rel="shortcut icon" href="/favicon.svg" />
|
<link rel="shortcut icon" href="/favicon.svg" />
|
||||||
</Head>
|
</Head>
|
||||||
|
|||||||
29
src/pages/api/modules/ping.ts
Normal file
29
src/pages/api/modules/ping.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
|
||||||
|
async function Get(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
// Parse req.body as a ServiceItem
|
||||||
|
const { url } = req.query;
|
||||||
|
await axios
|
||||||
|
.get(url as string)
|
||||||
|
.then((response) => {
|
||||||
|
res.status(200).json(response.data);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
res.status(500).json(error);
|
||||||
|
});
|
||||||
|
// // Make a request to the URL
|
||||||
|
// const response = await axios.get(url);
|
||||||
|
// // Return the response
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
// Filter out if the reuqest is a POST or a GET
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
return Get(req, res);
|
||||||
|
}
|
||||||
|
return res.status(405).json({
|
||||||
|
statusCode: 405,
|
||||||
|
message: 'Method not allowed',
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Group } from '@mantine/core';
|
|
||||||
import { getCookie, setCookies } from 'cookies-next';
|
import { getCookie, setCookies } from 'cookies-next';
|
||||||
import { GetServerSidePropsContext } from 'next';
|
import { GetServerSidePropsContext } from 'next';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@@ -6,7 +5,6 @@ import fs from 'fs';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import AppShelf from '../components/AppShelf/AppShelf';
|
import AppShelf from '../components/AppShelf/AppShelf';
|
||||||
import LoadConfigComponent from '../components/Config/LoadConfig';
|
import LoadConfigComponent from '../components/Config/LoadConfig';
|
||||||
import SearchBar from '../components/SearchBar/SearchBar';
|
|
||||||
import { Config } from '../tools/types';
|
import { Config } from '../tools/types';
|
||||||
import { useConfig } from '../tools/state';
|
import { useConfig } from '../tools/state';
|
||||||
|
|
||||||
@@ -54,10 +52,7 @@ export default function HomePage(props: any) {
|
|||||||
}, [initialConfig]);
|
}, [initialConfig]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SearchBar />
|
<AppShelf />
|
||||||
<Group align="start" position="apart" noWrap>
|
|
||||||
<AppShelf />
|
|
||||||
</Group>
|
|
||||||
<LoadConfigComponent />
|
<LoadConfigComponent />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export interface Settings {
|
export interface Settings {
|
||||||
searchUrl: string;
|
searchUrl: string;
|
||||||
searchBar: boolean;
|
|
||||||
enabledModules: string[];
|
enabledModules: string[];
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user