Compare commits
88 Commits
Author | SHA1 | Date | |
---|---|---|---|
707fc0e7ba | |||
|
f8d0761d0c | ||
|
c701075dfa | ||
6297655a34 | |||
e207e58f6a | |||
af9037991b | |||
0b8ea779f3 | |||
c30533045a | |||
|
ff0b5150f1 | ||
|
5a85322c92 | ||
553076011b | |||
837446bc33 | |||
8ada6cca70 | |||
e028716b18 | |||
7bab0dbdd9 | |||
4a31e1e146 | |||
1f12c43da8 | |||
188bcb4f21 | |||
6b17544aa0 | |||
ae8d62958b | |||
729d1b5309 | |||
1fc60ffe4f | |||
2662b17049 | |||
|
d1b29caaa6 | ||
|
e75945851c | ||
|
13071ae3d1 | ||
|
5b727eee02 | ||
|
37dfd2a132 | ||
|
9a14de007e | ||
|
ed8b17e0df | ||
|
c368290e32 | ||
|
00b069fc6f | ||
|
40d3e8de76 | ||
|
1017cc9864 | ||
|
b04e718367 | ||
|
ab8136bab7 | ||
488e4bbe7f | |||
62ec6fa099 | |||
|
67fd101a38 | ||
|
0e045b4c55 | ||
|
e608701404 | ||
|
2b5f88db18 | ||
|
b8c81389dc | ||
|
5b05842512 | ||
|
80f6a13140 | ||
|
f2eacfcba1 | ||
|
a02961b70b | ||
|
9d0ec9e348 | ||
|
ffe3404a2a | ||
|
bcf0e1bec2 | ||
|
f70fc3ecae | ||
|
e9afa4d7dd | ||
|
fbe9338fd3 | ||
|
83665e4f48 | ||
|
8e5ee54a78 | ||
|
71cf63eb3b | ||
|
6777bc347b | ||
|
2644101276 | ||
|
607fb898f8 | ||
|
2e7eb41f8c | ||
|
db738288fa | ||
|
7e5ad02248 | ||
|
118d3e5ac0 | ||
|
687a9e4086 | ||
|
4f04feb2da | ||
|
68c36d6c54 | ||
|
e4537f134b | ||
|
239ef1688d | ||
|
154e6efe80 | ||
|
d05b8d3bf0 | ||
|
0ae40f78f8 | ||
|
6de53c49b3 | ||
|
ab40c4e007 | ||
|
1f92e1746d | ||
|
8ae1fe8a4e | ||
|
fd9237eb52 | ||
|
4bfcc5bc95 | ||
|
d3da4cfe93 | ||
|
d1cc18761f | ||
|
a503c5743e | ||
|
a9aed9f9e2 | ||
|
7ef65940ee | ||
|
d1b9dea287 | ||
|
ae73d7a5a0 | ||
|
25b6367aa1 | ||
|
b102c9b2b3 | ||
|
da6e676d6e | ||
|
796a16c8da |
@ -2,4 +2,5 @@ assets/*
|
||||
dockerfile
|
||||
*.md
|
||||
.git
|
||||
screenshot.png
|
||||
screenshot.png
|
||||
node_modules
|
||||
|
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -12,7 +12,7 @@ Fixes # (issue)
|
||||
|
||||
## Checklist:
|
||||
|
||||
- [ ] I read & comply with the [contributing guidelines](https://github.com/bastienwirtz/homer/blob/master/CONTRIBUTING.md)
|
||||
- [ ] I've read & comply with the [contributing guidelines](https://github.com/bastienwirtz/homer/blob/master/CONTRIBUTING.md)
|
||||
- [ ] I have tested my code for new features & regressions on both mobile & desktop devices, using the latest version of major browsers.
|
||||
- [ ] I have made corresponding changes the documentation (README.md).
|
||||
- [ ] I've check my modifications for any breaking change, especially in the `config.yml` file
|
||||
- [ ] I've checked my modifications for any breaking changes, especially in the `config.yml` file
|
||||
|
3
.github/workflows/main.yml
vendored
@ -3,8 +3,7 @@ name: Upload Release Asset
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- *
|
||||
tags: [v*]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
4
.gitignore
vendored
@ -21,4 +21,6 @@ yarn-error.log*
|
||||
*.sw?
|
||||
|
||||
# App configuration
|
||||
public/config.yml
|
||||
config.yml
|
||||
|
||||
.drone.yml
|
@ -6,10 +6,10 @@ First off, thank you for considering contributing to Homer!
|
||||
|
||||
### Project philosophy
|
||||
|
||||
Homer is meant to be a light and very simple dashboard that keeps all your usefull utilities at hands. The few features implemented in Homer focus on
|
||||
Homer is meant to be a light and very simple dashboard that keeps all your useful utilities at hands. The few features implemented in Homer focus on
|
||||
UX and usability. If you are looking for a full featured dashboard, there is tons of great stuff out there like https://heimdall.site/, https://github.com/rmountjoy92/DashMachine or https://organizr.app/.
|
||||
|
||||
- Configuration is stored in a simple config file, avoiding the need for a backend/database while making possible to use versionning or [config template](https://docs.ansible.com/ansible/latest/user_guide/playbooks_templating.html).
|
||||
- Configuration is stored in a simple config file, avoiding the need for a backend/database while making possible to use versioning or [config template](https://docs.ansible.com/ansible/latest/user_guide/playbooks_templating.html).
|
||||
- Only modern browsers are supported, feel free to use any JS features without any polyfill as soon as the latest version of the major browsers supports them.
|
||||
|
||||
### Roadmap
|
||||
@ -21,7 +21,7 @@ Feel free to open an issue if you have any question.
|
||||
|
||||
### Code of conduct and guidelines
|
||||
|
||||
First of all, we expect everyone (contributors and maintainers alike) to respect the [Code of conduct](https://github.com/bastienwirtz/homer/blob/master/CODE_OF_CONDUCT.md). It is not a recomandation, it is mandatory.
|
||||
First of all, we expect everyone (contributors and maintainers alike) to respect the [Code of conduct](https://github.com/bastienwirtz/homer/blob/master/CODE_OF_CONDUCT.md). It is not a recommendation, it is mandatory.
|
||||
|
||||
For all contributions, please respect the following guidelines:
|
||||
|
||||
|
@ -19,12 +19,12 @@ ENV UID 911
|
||||
ENV PORT 8080
|
||||
|
||||
RUN addgroup -S ${GROUP} -g ${GID} && adduser -D -S -u ${UID} ${USER} ${GROUP} && \
|
||||
apk add -U darkhttpd
|
||||
apk add -U --no-cache su-exec darkhttpd
|
||||
|
||||
COPY --from=build-stage --chown=${USER}:${GROUP} /app/dist /www/
|
||||
COPY --chown=${USER}:${GROUP} entrypoint.sh /entrypoint.sh
|
||||
COPY --from=build-stage --chown=${USER}:${GROUP} /app/dist/assets /www/default-assets
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
USER ${USER}
|
||||
EXPOSE ${PORT}
|
||||
VOLUME [ "/www/config.yml", "/www/assets" ]
|
||||
VOLUME /www/assets
|
||||
ENTRYPOINT ["/bin/sh", "/entrypoint.sh"]
|
||||
|
@ -28,13 +28,13 @@ ENV UID 911
|
||||
ENV PORT 8080
|
||||
|
||||
RUN addgroup -S ${GROUP} -g ${GID} && adduser -D -S -u ${UID} ${USER} ${GROUP} && \
|
||||
apk add -U darkhttpd && \
|
||||
apk add -U --no-cache darkhttpd su-exec && \
|
||||
rm /usr/bin/qemu-arm-static
|
||||
|
||||
COPY --from=build-stage --chown=${USER}:${GROUP} /app/dist /www/
|
||||
COPY --chown=${USER}:${GROUP} entrypoint.sh /entrypoint.sh
|
||||
COPY --from=build-stage --chown=${USER}:${GROUP} /app/dist/assets /www/default-assets
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
USER ${USER}
|
||||
EXPOSE ${PORT}
|
||||
VOLUME [ "/www/config.yml", "/www/assets" ]
|
||||
VOLUME /www/assets
|
||||
ENTRYPOINT ["/bin/sh", "/entrypoint.sh"]
|
||||
|
@ -28,13 +28,13 @@ ENV UID 911
|
||||
ENV PORT 8080
|
||||
|
||||
RUN addgroup -S ${GROUP} -g ${GID} && adduser -D -S -u ${UID} ${USER} ${GROUP} && \
|
||||
apk add -U darkhttpd && \
|
||||
apk add -U --no-cache darkhttpd su-exec && \
|
||||
rm /usr/bin/qemu-aarch64-static
|
||||
|
||||
COPY --from=build-stage --chown=${USER}:${GROUP} /app/dist /www/
|
||||
COPY --chown=${USER}:${GROUP} entrypoint.sh /entrypoint.sh
|
||||
COPY --from=build-stage --chown=${USER}:${GROUP} /app/dist/assets /www/default-assets
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
USER ${USER}
|
||||
EXPOSE ${PORT}
|
||||
VOLUME [ "/www/config.yml", "/www/assets" ]
|
||||
VOLUME /www/assets
|
||||
ENTRYPOINT ["/bin/sh", "/entrypoint.sh"]
|
||||
|
91
README.md
@ -1,15 +1,43 @@
|
||||
# Homer
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](http://makeapullrequest.com)
|
||||
[](https://gitter.im/homer-dashboard/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
[](https://github.com/bastienwirtz/homer/releases/latest/download/homer.zip)
|
||||
[](https://github.com/awesome-selfhosted/awesome-selfhosted)
|
||||
<h1 align="center">
|
||||
<img
|
||||
width="180"
|
||||
alt="Homer's donut"
|
||||
src="https://raw.githubusercontent.com//bastienwirtz/homer/main/public/logo.png">
|
||||
<br/>
|
||||
Homer
|
||||
</h1>
|
||||
|
||||
A dead simple static **HOM**epage for your serv**ER** to keep your s
|
||||
ervices on hand, from a simple `yaml` configuration file.
|
||||
<h4 align="center">
|
||||
A dead simple static <strong>HOM</strong>epage for your serv<strong>ER</strong> to keep your services on hand, from a simple `yaml` configuration file.
|
||||
</h4>
|
||||
|
||||
## [Live demo](https://homer-demo.netlify.app) • [Chat](https://gitter.im/homer-dashboard/community)
|
||||

|
||||
<p align="center">
|
||||
<strong>
|
||||
<a href="https://homer-demo.netlify.app">Demo</a>
|
||||
•
|
||||
<a href="https://gitter.im/homer-dashboard/community">Chat</a>
|
||||
•
|
||||
<a href="#getting-started">Getting started</a>
|
||||
</strong>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://opensource.org/licenses/Apache-2.0"><img
|
||||
alt="License: Apache 2"
|
||||
src="https://img.shields.io/badge/License-Apache%202.0-blue.svg"></a>
|
||||
<a href="https://gitter.im/homer-dashboard/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge"><img
|
||||
alt="Gitter chat"
|
||||
src="https://badges.gitter.im/homer-dashboard/community.svg"></a>
|
||||
<a href="https://github.com/bastienwirtz/homer/releases/latest/download/homer.zip"><img
|
||||
alt="Download homer static build"
|
||||
src="https://img.shields.io/badge/Download-homer.zip-orange"></a>
|
||||
<a href="https://github.com/awesome-selfhosted/awesome-selfhosted"><img
|
||||
alt="Awesome"
|
||||
src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.github.com/bastienwirtz/homer/main/docs/screenshot.png" width="100%">
|
||||
</p>
|
||||
|
||||
## Table of Contents
|
||||
- [Features](#features)
|
||||
@ -17,7 +45,7 @@ ervices on hand, from a simple `yaml` configuration file.
|
||||
- [Configuration](docs/configuration.md)
|
||||
- [Tips & tricks](docs/tips-and-tricks.md)
|
||||
- [Roadmap](#roadmap)
|
||||
- [Developement](docs/developement.md)
|
||||
- [Development](docs/development.md)
|
||||
|
||||
|
||||
## Features
|
||||
@ -38,31 +66,54 @@ ervices on hand, from a simple `yaml` configuration file.
|
||||
|
||||
Homer is a full static html/js dashboard, generated from the source in `/src` using webpack. It's meant to be served by an HTTP server, **it will not work if you open dist/index.html directly over file:// protocol**.
|
||||
|
||||
For more information about the `config.yml` file see [configuration](docs/configuration.md) the section.
|
||||
See [documentation](docs/configuration.md) for information about the configuration (`assets/config.yml`) options.
|
||||
|
||||
### Using docker
|
||||
|
||||
To launch container :
|
||||
|
||||
```sh
|
||||
docker run -p 8080:8080 -v /your/local/config.yml:/www/config.yml -v /your/local/assets/:/www/assets b4bz/homer:latest
|
||||
docker run -p 8080:8080 -v /your/local/assets/:/www/assets b4bz/homer:latest
|
||||
```
|
||||
|
||||
As a bind mount is used here, docker will not copy the initial content of the `assets` directory to the mounted directory.
|
||||
You can initialise your assets directory with the content provided in this repository
|
||||
```sh
|
||||
cp -r /public/assets/* /your/local/assets/
|
||||
Default assets will be automatically installed in the `/www/assets` directory. Use `UID` and/or `GID` env var to change the assets owner (`docker run -e "UID=1000" -e "GID=1000" [...]`).
|
||||
|
||||
### Using docker-compose
|
||||
|
||||
The `docker-compose.yml` file must be edited to match your needs.
|
||||
Set the port and volume (equivalent to -p and -v arguments) :
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- /your/local/assets/:/www/assets
|
||||
ports:
|
||||
- 8080:8080
|
||||
```
|
||||
|
||||
**Alternatively** if you just want to provide images/icons without customizing the other files (app manifest & pwa icons), you can mount a custom directory in the `www` directory and use it in your `config.yml` for icons path.
|
||||
To launch container :
|
||||
|
||||
```sh
|
||||
cd /path/to/docker-compose.yml
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Default assets will be automatically installed in the `/www/assets` directory. Use `UID` and/or `GID` env var to change the assets owner, also in `docker-compose.yml` :
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- UID=1000
|
||||
- GID=1000
|
||||
```
|
||||
|
||||
### Using the release tarball (prebuilt, ready to use)
|
||||
|
||||
Download and extract the latest the latest release (`homer.zip`) from the [release page](https://github.com/bastienwirtz/homer/releases), rename the `config.yml.dist` file to `config.yml`, and put it behind a webserver.
|
||||
Download and extract the latest the latest release (`homer.zip`) from the [release page](https://github.com/bastienwirtz/homer/releases), rename the `assets/config.yml.dist` file to `assets/config.yml`, and put it behind a webserver.
|
||||
|
||||
```sh
|
||||
wget https://github.com/bastienwirtz/homer/releases/latest/download/homer.zip
|
||||
unzip homer.zip
|
||||
cd homer
|
||||
cp config.yml.dist config.yml
|
||||
cp assets/config.yml.dist assets/config.yml
|
||||
npx serve # or python -m http.server 8010 or apache, nginx ...
|
||||
```
|
||||
|
||||
|
14
docker-compose.yml
Normal file
@ -0,0 +1,14 @@
|
||||
---
|
||||
version: "2"
|
||||
services:
|
||||
homer:
|
||||
image: b4bz/homer
|
||||
container_name: homer
|
||||
volumes:
|
||||
- /your/local/assets/:/www/assets
|
||||
ports:
|
||||
- 8080:8080
|
||||
#environment:
|
||||
# - UID=1000
|
||||
# - GID=1000
|
||||
restart: unless-stopped
|
@ -1,6 +1,6 @@
|
||||
## Configuration
|
||||
|
||||
Title, icons, links, colors, and services can be configured in the `config.yml` file (located in project root directory once built, or in the `public/` directory in developement mode), using [yaml](http://yaml.org/) format.
|
||||
Title, icons, links, colors, and services can be configured in the `config.yml` file (located in `/assets` directory once built, or in the `public/assets` directory in development mode), using [yaml](http://yaml.org/) format.
|
||||
|
||||
```yaml
|
||||
---
|
||||
@ -13,7 +13,8 @@ Title, icons, links, colors, and services can be configured in the `config.yml`
|
||||
|
||||
title: "App dashboard"
|
||||
subtitle: "Homer"
|
||||
logo: "assets/homer.png"
|
||||
# documentTitle: "Welcome" # Customize the browser tab text
|
||||
logo: "assets/logo.png"
|
||||
# Alternatively a fa icon can be provided:
|
||||
# icon: "fas fa-skull-crossbones"
|
||||
|
||||
@ -21,10 +22,17 @@ header: true # Set to false to hide the header
|
||||
footer: '<p>Created with <span class="has-text-danger">❤️</span> with <a href="https://bulma.io/">bulma</a>, <a href="https://vuejs.org/">vuejs</a> & <a href="https://fontawesome.com/">font awesome</a> // Fork me on <a href="https://github.com/bastienwirtz/homer"><i class="fab fa-github-alt"></i></a></p>' # set false if you want to hide it.
|
||||
|
||||
columns: "3" # "auto" or number (must be a factor of 12: 1, 2, 3, 4, 6, 12)
|
||||
vlayout: true # default to the vertical layout
|
||||
connectivityCheck: true # whether you want to display a message when the apps are not accessible anymore (VPN disconnected for example)
|
||||
|
||||
# Optional theming
|
||||
theme: default # 'default' or one of the theme available in 'src/assets/themes'.
|
||||
theme_use_dark: false # true or false, useful for overriding browser default in new sessions
|
||||
|
||||
# Optional custom stylesheet
|
||||
# Will load custom CSS files. Especially useful for custom icon sets.
|
||||
# stylesheet:
|
||||
# - "assets/custom.css"
|
||||
|
||||
# Here is the exaustive list of customization parameters
|
||||
# However all value are optional and will fallback to default if not set.
|
||||
@ -42,6 +50,7 @@ colors:
|
||||
text-subtitle: "#424242"
|
||||
card-shadow: rgba(0, 0, 0, 0.1)
|
||||
link-hover: "#363636"
|
||||
background-image: "assets/your/light/bg.png"
|
||||
dark:
|
||||
highlight-primary: "#3367d6"
|
||||
highlight-secondary: "#4285f4"
|
||||
@ -54,12 +63,14 @@ colors:
|
||||
text-subtitle: "#f5f5f5"
|
||||
card-shadow: rgba(0, 0, 0, 0.4)
|
||||
link-hover: "#ffdd57"
|
||||
background-image: "assets/your/dark/bg.png"
|
||||
|
||||
# Optional message
|
||||
message:
|
||||
# url: "https://<my-api-endpoint>" # Can fetch information from an endpoint to override value below.
|
||||
style: "is-warning"
|
||||
title: "Optional message!"
|
||||
icon: "fa fa-exclamation-triangle"
|
||||
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
|
||||
# Optional navbar
|
||||
@ -78,7 +89,7 @@ links:
|
||||
# Leave only a "items" key if not using group (group name, icon & tagstyle are optional, section separation will not be displayed).
|
||||
services:
|
||||
- name: "Application"
|
||||
icon: "fa fa-code-fork"
|
||||
icon: "fas fa-code-branch"
|
||||
items:
|
||||
- name: "Awesome app"
|
||||
logo: "assets/tools/sample.png"
|
||||
@ -88,6 +99,8 @@ services:
|
||||
tag: "app"
|
||||
url: "https://www.reddit.com/r/selfhosted/"
|
||||
target: "_blank" # optional html tag target attribute
|
||||
info: "https://github.com/bastienwirtz/homer/tree/main/docs" # optional link to documentation
|
||||
infotarget: "_blank" # same as target, but for icon link
|
||||
- name: "Another one"
|
||||
logo: "assets/tools/sample2.png"
|
||||
subtitle: "Another application"
|
||||
@ -98,12 +111,15 @@ services:
|
||||
- name: "Other group"
|
||||
icon: "fas fa-heartbeat"
|
||||
items:
|
||||
- name: "Another app"
|
||||
- name: "Pi-hole"
|
||||
logo: "assets/tools/sample.png"
|
||||
subtitle: "Another example"
|
||||
subtitle: "Network-wide Ad Blocking"
|
||||
tag: "other"
|
||||
url: "https://www.reddit.com/r/selfhosted/"
|
||||
target: "_blank" # optionnal html a tag target attribute
|
||||
url: "http://192.168.0.151/admin"
|
||||
type: "PiHole" # optional, loads a specific component that provides extra features. MUST MATCH a file name (without file extension) available in `src/components/services`
|
||||
target: "_blank" # optional html a tag target attribute
|
||||
# class: "green" # optional custom CSS class for card, useful with custom stylesheet
|
||||
# background: red # optional color for card to set color directly without custom stylesheet
|
||||
```
|
||||
|
||||
If you choose to fetch message information from an endpoint, the output format should be:
|
||||
@ -129,3 +145,11 @@ Homer uses [bulma CSS](https://bulma.io/), which provides a [modifiers syntax](h
|
||||
- `is-danger` (red)
|
||||
|
||||
You can read the [bulma modifiers page](https://bulma.io/documentation/modifiers/syntax/) for other options regarding size, style, or state.
|
||||
|
||||
### PWA Icons
|
||||
|
||||
In order to easily generate all required icon preset for the PWA to work, a tool like [vue-pwa-asset-generator](https://www.npmjs.com/package/vue-pwa-asset-generator) can be used:
|
||||
|
||||
```bash
|
||||
npx vue-pwa-asset-generator -a {your_512x512_source_png} -o {your_output_folder}
|
||||
```
|
||||
|
@ -1,4 +1,4 @@
|
||||
## Developement
|
||||
## Development
|
||||
|
||||
```sh
|
||||
# Using yarn (recommended)
|
||||
@ -13,7 +13,7 @@ npm run serve
|
||||
### Themes
|
||||
|
||||
Themes are meant to be simple customization (written in [scss](https://sass-lang.com/documentation/syntax)).
|
||||
To addd a new theme, just add a file in the theme directory, and put all style in the `body #app.theme-<name>` scope. Then import it in the main style file.
|
||||
To add a new theme, just add a file in the theme directory, and put all style in the `body #app.theme-<name>` scope. Then import it in the main style file.
|
||||
|
||||
```scss
|
||||
// `src/assets/themes/my-awesome-theme.scss`
|
BIN
docs/screenshot.png
Normal file
After Width: | Height: | Size: 50 KiB |
@ -5,9 +5,9 @@ Here is a collection of neat tips and tricks that Homer users have come up with!
|
||||
## Use Homer as a custom "new tab" page
|
||||
#### `by @vosdev`
|
||||
|
||||
This [extension](https://addons.mozilla.org/firefox/addon/custom-new-tab-page) allows you to have your homer dashboard in your new tab page, while leaving focus on the address bar meaning you can still type right away if you want to search or go to a page that is not on your homer dash.
|
||||
These extensions for [Firefox](https://addons.mozilla.org/firefox/addon/custom-new-tab-page) and [Chrome & Friends](https://chrome.google.com/webstore/detail/new-tab-changer/occbjkhimchkolibngmcefpjlbknggfh) allow you to have your homer dashboard in your new tab page, while leaving focus on the address bar meaning you can still type right away if you want to search or go to a page that is not on your homer dash.
|
||||
|
||||
The extension loads Homer in an iframe on your new tab page, meaning you have to add `target: '_top'` to each of your items.
|
||||
The Firefox extension loads Homer in an iframe on your new tab page, meaning you have to add `target: '_top'` to each of your items.
|
||||
|
||||
```yaml
|
||||
- name: "Reddit"
|
||||
@ -24,7 +24,7 @@ The extension loads Homer in an iframe on your new tab page, meaning you have to
|
||||
## YAML Anchors
|
||||
#### `by @JamiePhonic`
|
||||
|
||||
Since Homer is configured using YAML, it supports all of YAML's helpful fetaures, such as anchoring!
|
||||
Since Homer is configured using YAML, it supports all of YAML's helpful features, such as anchoring!
|
||||
|
||||
For example, you can define tags and tag styles for each "item" in a service.
|
||||
Using Anchoring, you can define all your tags and their styles once like this: (for example)
|
||||
@ -66,15 +66,15 @@ Then when Homer reads your config, it will substitute your anchors automatically
|
||||
target: "_blank" # optional html tag target attribute
|
||||
```
|
||||
|
||||
The end result is that if you want to update the name or style of any perticular tag, just update it once, in the tags section!
|
||||
The end result is that if you want to update the name or style of any particular tag, just update it once, in the tags section!
|
||||
Great if you have a lot of services or a lot of tags!
|
||||
|
||||
## Remotely edit your config with Code Server
|
||||
#### `by @JamiePhonic`
|
||||
|
||||
Homer doesn't yet provide a way to edit your configuration from inside Homer itself, but that doesnt mean it cant be done!
|
||||
Homer doesn't yet provide a way to edit your configuration from inside Homer itself, but that doesn't mean it cant be done!
|
||||
|
||||
You can setup and use [Code-Server](https://github.com/cdr/code-server) to edit your config.yml file from anywhere!
|
||||
You can setup and use [Code-Server](https://github.com/cdr/code-server) to edit your `config.yml` file from anywhere!
|
||||
|
||||
If you're running Homer in docker, you can setup a Code-Server container and pass your homer config directory into it.
|
||||
Simply pass your homer config directory as and extra -v parameter to your code-server container:
|
||||
@ -123,4 +123,4 @@ So, using [Node-Red](https://nodered.org/docs/getting-started/) and a quick flow
|
||||
|
||||
To get started, simply import [this flow](https://flows.nodered.org/flow/4b6406c9a684c26ace0430dd1826e95d) into your Node-Red instance and change the RSS feed in the "Get News RSS Feed" node to one of your choosing!
|
||||
|
||||
So far, the flow has been tested with BBC News and Sky News, however it should be easy to modify the flow to work with other RSS feeds if they dont work out of the box!
|
||||
So far, the flow has been tested with BBC News and Sky News, however it should be easy to modify the flow to work with other RSS feeds if they don't work out of the box!
|
||||
|
@ -1,6 +1,15 @@
|
||||
#!/bin/sh
|
||||
|
||||
yes n | cp -i /www/config.yml.dist /www/config.yml
|
||||
while true; do echo n; done | cp -Ri /app/dist/www/assets /www/assets 2>/dev/null
|
||||
# Ensure default assets are present.
|
||||
while true; do echo n; done | cp -Ri /www/default-assets/* /www/assets/ &> /dev/null
|
||||
|
||||
darkhttpd /www/ --no-listing --port $PORT
|
||||
# Ensure compatibility with previous version (config.yml was in the root directory)
|
||||
if [ -f "/www/config.yml" ]; then
|
||||
yes n | cp -i /www/config.yml /www/assets/ &> /dev/null
|
||||
fi
|
||||
|
||||
# Install default config if no one is available.
|
||||
yes n | cp -i /www/default-assets/config.yml.dist /www/assets/config.yml &> /dev/null
|
||||
|
||||
chown -R $UID:$GID /www/assets
|
||||
exec su-exec $UID:$GID darkhttpd /www/ --no-listing --port "$PORT"
|
||||
|
@ -1,7 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker manifest push --purge b4bz/homer:latest
|
||||
docker manifest create b4bz/homer:latest b4bz/homer:latest-amd64 b4bz/homer:latest-arm32v7 b4bz/homer:latest-arm64v8
|
||||
docker manifest annotate b4bz/homer:latest b4bz/homer:latest-arm32v7 --os linux --arch arm
|
||||
docker manifest annotate b4bz/homer:latest b4bz/homer:latest-arm64v8 --os linux --arch arm64 --variant v8
|
||||
docker manifest push --purge b4bz/homer:latest
|
||||
IFS='-' read -r TAG string <<< "$DOCKER_TAG"
|
||||
|
||||
docker manifest create b4bz/homer:$TAG b4bz/homer:$TAG-amd64 b4bz/homer:$TAG-arm32v7 b4bz/homer:$TAG-arm64v8
|
||||
docker manifest annotate b4bz/homer:$TAG b4bz/homer:$TAG-arm32v7 --os linux --arch arm
|
||||
docker manifest annotate b4bz/homer:$TAG b4bz/homer:$TAG-arm64v8 --os linux --arch arm64 --variant v8
|
||||
docker manifest push --purge b4bz/homer:$TAG
|
||||
|
30
package.json
@ -8,28 +8,28 @@
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.13.0",
|
||||
"bulma": "^0.8.2",
|
||||
"@fortawesome/fontawesome-free": "^5.15.1",
|
||||
"bulma": "^0.9.1",
|
||||
"core-js": "^3.6.4",
|
||||
"js-yaml": "^3.14.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"register-service-worker": "^1.7.1",
|
||||
"vue": "^2.6.11"
|
||||
"vue": "^2.6.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.4.1",
|
||||
"@vue/cli-plugin-eslint": "~4.4.1",
|
||||
"@vue/cli-plugin-pwa": "~4.4.1",
|
||||
"@vue/cli-service": "~4.4.1",
|
||||
"@vue/cli-plugin-babel": "~4.5.8",
|
||||
"@vue/cli-plugin-eslint": "~4.5.8",
|
||||
"@vue/cli-plugin-pwa": "~4.5.8",
|
||||
"@vue/cli-service": "~4.5.8",
|
||||
"@vue/eslint-config-prettier": "^6.0.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^7.2.0",
|
||||
"eslint-plugin-prettier": "^3.1.1",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"prettier": "^2.0.5",
|
||||
"raw-loader": "^4.0.1",
|
||||
"sass": "^1.26.8",
|
||||
"sass-loader": "^8.0.2",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
"eslint": "^7.11.0",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"eslint-plugin-vue": "^7.1.0",
|
||||
"prettier": "^2.1.2",
|
||||
"raw-loader": "^4.0.2",
|
||||
"sass": "^1.27.0",
|
||||
"sass-loader": "^10.0.4",
|
||||
"vue-template-compiler": "^2.6.12"
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ logo: "logo.png"
|
||||
header: true
|
||||
footer: '<p>Created with <span class="has-text-danger">❤️</span> with <a href="https://bulma.io/">bulma</a>, <a href="https://vuejs.org/">vuejs</a> & <a href="https://fontawesome.com/">font awesome</a> // Fork me on <a href="https://github.com/bastienwirtz/homer"><i class="fab fa-github-alt"></i></a></p>' # set false if you want to hide it.
|
||||
|
||||
# Optionnal theme customization
|
||||
# Optional theme customization
|
||||
theme: default
|
||||
colors:
|
||||
light:
|
||||
@ -42,7 +42,8 @@ colors:
|
||||
message:
|
||||
#url: https://b4bz.io
|
||||
style: "is-dark" # See https://bulma.io/documentation/components/message/#colors for styling options.
|
||||
title: "👋 Demo !"
|
||||
title: "Demo !"
|
||||
icon: "fa fa-grin"
|
||||
content: "This is a dummy homepage demo. <br /> Find more information on <a href='https://github.com/bastienwirtz/homer'>github.com/bastienwirtz/homer</a>"
|
||||
|
||||
# Optional navbar
|
||||
@ -51,7 +52,7 @@ links:
|
||||
- name: "Contribute"
|
||||
icon: "fab fa-github"
|
||||
url: "https://github.com/bastienwirtz/homer"
|
||||
target: "_blank" # optionnal html a tag target attribute
|
||||
target: "_blank" # optional html a tag target attribute
|
||||
- name: "Wiki"
|
||||
icon: "fas fa-book"
|
||||
url: "https://www.wikipedia.org/"
|
||||
@ -68,7 +69,7 @@ services:
|
||||
subtitle: "Bookmark example"
|
||||
tag: "app"
|
||||
url: "https://www.reddit.com/r/selfhosted/"
|
||||
target: "_blank" # optionnal html a tag target attribute
|
||||
target: "_blank" # optional html a tag target attribute
|
||||
- name: "Another one"
|
||||
logo: "assets/tools/sample2.png"
|
||||
subtitle: "Another application"
|
@ -9,7 +9,7 @@ logo: false
|
||||
|
||||
header: true
|
||||
|
||||
# Optionnal theme customization
|
||||
# Optional theme customization
|
||||
theme: sui
|
||||
colors:
|
||||
light:
|
8
public/assets/custom.css.sample
Normal file
@ -0,0 +1,8 @@
|
||||
@charset "UTF-8";
|
||||
|
||||
/* Custom card colors */
|
||||
/* Use with `class:` property of services in config.yml */
|
||||
body #app .card.green {
|
||||
background-color: #006600;
|
||||
color: #00ff00;
|
||||
}
|
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 425 B After Width: | Height: | Size: 790 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 2.3 KiB |
BIN
public/assets/icons/icon-any.png
Normal file
After Width: | Height: | Size: 75 KiB |
1
public/assets/icons/icon-any.svg
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
public/assets/icons/icon-maskable.png
Normal file
After Width: | Height: | Size: 124 KiB |
Before Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 6.7 KiB |
@ -1,79 +1,42 @@
|
||||
{
|
||||
"name": "Dashboard",
|
||||
"short_name": "homer",
|
||||
"name": "Homer Dashboard",
|
||||
"short_name": "Homer",
|
||||
"theme_color": "#3367D6",
|
||||
"start_url": "../",
|
||||
"icons": [
|
||||
{
|
||||
"src": "./assets/icons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./assets/icons/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./assets/icons/android-chrome-maskable-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "./assets/icons/android-chrome-maskable-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "./assets/icons/apple-touch-icon-60x60.png",
|
||||
"sizes": "60x60",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./assets/icons/apple-touch-icon-76x76.png",
|
||||
"sizes": "76x76",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./assets/icons/apple-touch-icon-120x120.png",
|
||||
"sizes": "120x120",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./assets/icons/apple-touch-icon-152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./assets/icons/apple-touch-icon-180x180.png",
|
||||
"sizes": "180x180",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./assets/icons/apple-touch-icon.png",
|
||||
"sizes": "180x180",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./assets/icons/favicon-16x16.png",
|
||||
"src": "./icons/favicon-16x16.png",
|
||||
"sizes": "16x16",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./assets/icons/favicon-32x32.png",
|
||||
"src": "./icons/favicon-32x32.png",
|
||||
"sizes": "32x32",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./assets/icons/msapplication-icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
"src": "./icons/icon-any.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "./assets/icons/mstile-150x150.png",
|
||||
"sizes": "150x150",
|
||||
"type": "image/png"
|
||||
"src": "./icons/icon-any.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-maskable.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "./icons/safari-pinned-tab.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "monochrome"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 48 KiB |
BIN
public/logo.png
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 36 KiB |
BIN
screenshot.png
Before Width: | Height: | Size: 51 KiB |
107
src/App.vue
@ -26,11 +26,12 @@
|
||||
<Navbar
|
||||
:open="showMenu"
|
||||
:links="config.links"
|
||||
@navbar:toggle="showMenu = !showMenu"
|
||||
@navbar-toggle="showMenu = !showMenu"
|
||||
>
|
||||
<DarkMode @updated="isDark = $event" />
|
||||
<DarkMode :isDark="this.isDark" @updated="isDark = $event" />
|
||||
|
||||
<SettingToggle
|
||||
<LayoutToggle
|
||||
:vlayout="this.vlayout"
|
||||
@updated="vlayout = $event"
|
||||
name="vlayout"
|
||||
icon="fa-list"
|
||||
@ -40,9 +41,10 @@
|
||||
<SearchInput
|
||||
class="navbar-item is-inline-block-mobile"
|
||||
@input="filterServices"
|
||||
@search:focus="showMenu = true"
|
||||
@search:open="navigateToFirstService"
|
||||
@search:cancel="filterServices"
|
||||
:value="filter"
|
||||
@search-focus="showMenu = true"
|
||||
@search-open="navigateToFirstService"
|
||||
@search-cancel="filterServices"
|
||||
/>
|
||||
</Navbar>
|
||||
</div>
|
||||
@ -51,7 +53,7 @@
|
||||
<div v-cloak class="container">
|
||||
<ConnectivityChecker
|
||||
v-if="config.connectivityCheck"
|
||||
@network:status-update="offline = $event"
|
||||
@network-status-update="offline = $event"
|
||||
/>
|
||||
<div v-if="!offline">
|
||||
<!-- Optional messages -->
|
||||
@ -61,13 +63,14 @@
|
||||
<div v-if="!vlayout || filter" class="columns is-multiline">
|
||||
<template v-for="group in services">
|
||||
<h2 v-if="group.name" class="column is-full group-title">
|
||||
<i v-if="group.icon" :class="group.icon"></i>
|
||||
<i v-if="group.icon" :class="['fa-fw', group.icon]"></i>
|
||||
{{ group.name }}
|
||||
</h2>
|
||||
<Service
|
||||
v-for="item in group.items"
|
||||
:key="item.name"
|
||||
v-bind:item="item"
|
||||
@filter="filterTag"
|
||||
:class="['column', `is-${12 / config.columns}`]"
|
||||
/>
|
||||
</template>
|
||||
@ -84,12 +87,13 @@
|
||||
:key="group.name"
|
||||
>
|
||||
<h2 v-if="group.name" class="group-title">
|
||||
<i v-if="group.icon" :class="group.icon"></i>
|
||||
<i v-if="group.icon" :class="['fa-fw', group.icon]"></i>
|
||||
{{ group.name }}
|
||||
</h2>
|
||||
<Service
|
||||
v-for="item in group.items"
|
||||
v-bind:item="item"
|
||||
@filter="filterTag"
|
||||
:key="item.url"
|
||||
/>
|
||||
</div>
|
||||
@ -119,7 +123,7 @@ import ConnectivityChecker from "./components/ConnectivityChecker.vue";
|
||||
import Service from "./components/Service.vue";
|
||||
import Message from "./components/Message.vue";
|
||||
import SearchInput from "./components/SearchInput.vue";
|
||||
import SettingToggle from "./components/SettingToggle.vue";
|
||||
import LayoutToggle from "./components/LayoutToggle.vue";
|
||||
import DarkMode from "./components/DarkMode.vue";
|
||||
import DynamicTheme from "./components/DynamicTheme.vue";
|
||||
|
||||
@ -128,38 +132,60 @@ import defaultConfig from "./assets/defaults.yml";
|
||||
export default {
|
||||
name: "App",
|
||||
components: {
|
||||
Navbar,
|
||||
ConnectivityChecker,
|
||||
Service,
|
||||
Message,
|
||||
SearchInput,
|
||||
SettingToggle,
|
||||
DarkMode,
|
||||
DynamicTheme,
|
||||
Message,
|
||||
Navbar,
|
||||
SearchInput,
|
||||
Service,
|
||||
LayoutToggle,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
config: null,
|
||||
services: null,
|
||||
offline: false,
|
||||
filter: "",
|
||||
vlayout: true,
|
||||
isDark: null,
|
||||
offline: false,
|
||||
services: null,
|
||||
showMenu: false,
|
||||
vlayout: null,
|
||||
};
|
||||
},
|
||||
created: async function () {
|
||||
const defaults = jsyaml.load(defaultConfig);
|
||||
let config = await this.getConfig();
|
||||
let config;
|
||||
try {
|
||||
config = await this.getConfig();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
config = this.handleErrors("⚠️ Error loading configuration", error);
|
||||
}
|
||||
this.config = merge(defaults, config);
|
||||
this.services = this.config.services;
|
||||
document.title = `${this.config.title} | ${this.config.subtitle}`;
|
||||
this.isDark = this.config.theme_use_dark;
|
||||
this.vlayout = this.config.vlayout;
|
||||
document.title =
|
||||
this.config.documentTitle ||
|
||||
`${this.config.title} | ${this.config.subtitle}`;
|
||||
if (this.config.stylesheet) {
|
||||
let stylesheet = "";
|
||||
for (const file of this.config.stylesheet) {
|
||||
stylesheet += `@import "${file}";`;
|
||||
}
|
||||
this.createStylesheet(stylesheet);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getConfig: function (path = "config.yml") {
|
||||
getConfig: function (path = "assets/config.yml") {
|
||||
return fetch(path).then((response) => {
|
||||
if (response.redirected) {
|
||||
// This allows to work with authentication proxies.
|
||||
window.location.href = response.url;
|
||||
return;
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw Error(response.statusText);
|
||||
throw Error(`${response.statusText}: ${response.body}`);
|
||||
}
|
||||
|
||||
const that = this;
|
||||
@ -173,9 +199,6 @@ export default {
|
||||
return that.getConfig(config.externalConfig);
|
||||
}
|
||||
return config;
|
||||
})
|
||||
.catch((error) => {
|
||||
return this.handleErrors("⚠️ Error loading configuration", error);
|
||||
});
|
||||
});
|
||||
},
|
||||
@ -193,6 +216,35 @@ export default {
|
||||
console.warning("fail to open service");
|
||||
}
|
||||
},
|
||||
filterTag: function (filter) {
|
||||
this.showMenu = true;
|
||||
this.$nextTick(() => {
|
||||
document.getElementById("searchBox").focus();
|
||||
});
|
||||
this.filter = filter;
|
||||
|
||||
if (!filter) {
|
||||
this.services = this.config.services;
|
||||
return;
|
||||
}
|
||||
|
||||
const searchResultItems = [];
|
||||
for (const group of this.config.services) {
|
||||
for (const item of group.items) {
|
||||
if (this.matchesFilter(item)) {
|
||||
searchResultItems.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.services = [
|
||||
{
|
||||
name: filter,
|
||||
icon: "fas fa-search",
|
||||
items: searchResultItems,
|
||||
},
|
||||
];
|
||||
},
|
||||
filterServices: function (filter) {
|
||||
this.filter = filter;
|
||||
|
||||
@ -227,6 +279,11 @@ export default {
|
||||
},
|
||||
};
|
||||
},
|
||||
createStylesheet: function (css) {
|
||||
let style = document.createElement("style");
|
||||
style.appendChild(document.createTextNode(css));
|
||||
document.head.appendChild(style);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -13,17 +13,20 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
html {
|
||||
html, body, body #app {
|
||||
height: 100%;
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Raleway", sans-serif;
|
||||
height: 100%;
|
||||
|
||||
#app {
|
||||
height: auto;
|
||||
min-height: 100%;
|
||||
background-color: var(--background);
|
||||
background-image: var(--background-image);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
color: var(--text);
|
||||
transition: background-color cubic-bezier(0.165, 0.84, 0.44, 1) 300ms;
|
||||
|
||||
@ -46,6 +49,17 @@ body {
|
||||
&:hover {
|
||||
background-color: var(--card-background);
|
||||
}
|
||||
|
||||
.linkoverlay {
|
||||
position:absolute;
|
||||
left:0;
|
||||
top:0;
|
||||
bottom:0;
|
||||
right:0;
|
||||
}
|
||||
.thirty-five {
|
||||
font-size: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
@ -167,6 +181,7 @@ body {
|
||||
|
||||
.title {
|
||||
font-size: 1.1em;
|
||||
line-height: 1.2em;
|
||||
@include ellipsis();
|
||||
}
|
||||
|
||||
@ -192,17 +207,27 @@ body {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.media-left {
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
.media-content {
|
||||
overflow: hidden;
|
||||
text-overflow: inherit;
|
||||
}
|
||||
.infolink {
|
||||
font-family: "Font Awesome 5 Free";
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tag {
|
||||
color: var(--highlight-secondary);
|
||||
background-color: var(--highlight-secondary);
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
bottom: 1rem;
|
||||
right: -0.2rem;
|
||||
width: 3px;
|
||||
overflow: hidden;
|
||||
|
@ -24,6 +24,7 @@ colors:
|
||||
text-subtitle: "#424242"
|
||||
card-shadow: rgba(0, 0, 0, 0.1)
|
||||
link-hover: "#363636"
|
||||
background-image: ""
|
||||
dark:
|
||||
highlight-primary: "#3367d6"
|
||||
highlight-secondary: "#4285f4"
|
||||
@ -36,6 +37,7 @@ colors:
|
||||
text-subtitle: "#f5f5f5"
|
||||
card-shadow: rgba(0, 0, 0, 0.4)
|
||||
link-hover: "#ffdd57"
|
||||
background-image: ""
|
||||
|
||||
message: ~
|
||||
links: []
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* SUI theme
|
||||
* Inpired by the great https://github.com/jeroenpardon/sui start page
|
||||
* Inspired by the great https://github.com/jeroenpardon/sui start page
|
||||
* Author: @bastienwirtz
|
||||
*/
|
||||
body #app.theme-sui {
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div v-if="offline" class="offline-message">
|
||||
<i class="far fa-dizzy"></i>
|
||||
<h1>
|
||||
You're offline bro.
|
||||
You're offline friend.
|
||||
<span @click="checkOffline"> <i class="fas fa-redo-alt"></i></span>
|
||||
</h1>
|
||||
</div>
|
||||
@ -44,7 +44,7 @@ export default {
|
||||
that.offline = true;
|
||||
})
|
||||
.finally(function () {
|
||||
that.$emit("network:status-update", that.offline);
|
||||
that.$emit("network-status-update", that.offline);
|
||||
});
|
||||
},
|
||||
},
|
||||
|
@ -4,30 +4,30 @@
|
||||
aria-label="Toggle dark mode"
|
||||
class="navbar-item is-inline-block-mobile"
|
||||
>
|
||||
<i class="fas fa-adjust"></i>
|
||||
<i class="fas fa-fw fa-adjust"></i>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Darkmode",
|
||||
data: function () {
|
||||
return {
|
||||
isDark: null,
|
||||
};
|
||||
props: {
|
||||
isDark: Boolean,
|
||||
},
|
||||
created: function () {
|
||||
this.isDark =
|
||||
let isDark =
|
||||
"overrideDark" in localStorage
|
||||
? JSON.parse(localStorage.overrideDark)
|
||||
: matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
this.$emit("updated", this.isDark);
|
||||
: this.isDark === null
|
||||
? matchMedia("(prefers-color-scheme: dark)").matches
|
||||
: this.isDark;
|
||||
this.$emit("updated", isDark);
|
||||
},
|
||||
methods: {
|
||||
toggleTheme: function () {
|
||||
this.isDark = !this.isDark;
|
||||
localStorage.overrideDark = this.isDark;
|
||||
this.$emit("updated", this.isDark);
|
||||
let isDark = !this.isDark;
|
||||
localStorage.overrideDark = isDark;
|
||||
this.$emit("updated", isDark);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -1,17 +1,15 @@
|
||||
<template>
|
||||
<DynamicStyle>
|
||||
/* light / dark theme switch based on system pref if available */ body #app
|
||||
{
|
||||
:root, body #app.is-light {
|
||||
{{ getVars(themes.light) }}
|
||||
} @media (prefers-color-scheme: light), (prefers-color-scheme:
|
||||
no-preference) { body #app {
|
||||
no-preference) { :root, body #app {
|
||||
{{ getVars(themes.light) }}
|
||||
} } @media (prefers-color-scheme: dark) { body #app { } } /* light / dark
|
||||
theme override base on user choice. */ body #app.is-dark {
|
||||
} } body #app.is-dark {
|
||||
{{ getVars(themes.dark) }}
|
||||
} body #app.is-light {
|
||||
{{ getVars(themes.light) }}
|
||||
}
|
||||
} @media (prefers-color-scheme: dark) { :root, body #app {
|
||||
{{ getVars(themes.dark) }}
|
||||
} }
|
||||
</DynamicStyle>
|
||||
</template>
|
||||
|
||||
@ -25,7 +23,13 @@ export default {
|
||||
getVars: function (theme) {
|
||||
let vars = [];
|
||||
for (const themeVars in theme) {
|
||||
vars.push(`--${themeVars}: ${theme[themeVars]}`);
|
||||
let value = `${theme[themeVars]}`;
|
||||
if (!value) {
|
||||
value = "initial";
|
||||
} else if (themeVars == "background-image") {
|
||||
value = `url(${theme[themeVars]})`;
|
||||
}
|
||||
vars.push(`--${themeVars}: ${value}`);
|
||||
}
|
||||
return vars.join(";");
|
||||
},
|
||||
|
43
src/components/LayoutToggle.vue
Normal file
@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<a v-on:click="toggleLayout()" class="navbar-item is-inline-block-mobile">
|
||||
<span>
|
||||
<i :class="['fas', 'fa-fw', vlayout ? icon : secondaryIcon]"></i>
|
||||
</span>
|
||||
<slot></slot>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "LayoutToggle",
|
||||
props: {
|
||||
name: String,
|
||||
icon: String,
|
||||
iconAlt: String,
|
||||
vlayout: Boolean,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
secondaryIcon: null,
|
||||
};
|
||||
},
|
||||
created: function () {
|
||||
this.secondaryIcon = this.iconAlt || this.icon;
|
||||
let vlayout;
|
||||
if (this.name in localStorage) {
|
||||
vlayout = JSON.parse(localStorage[this.name]);
|
||||
} else {
|
||||
vlayout = this.vlayout === null ? true : this.vlayout;
|
||||
}
|
||||
|
||||
this.$emit("updated", vlayout);
|
||||
},
|
||||
methods: {
|
||||
toggleLayout: function () {
|
||||
let vlayout = !this.vlayout;
|
||||
localStorage[this.name] = vlayout;
|
||||
this.$emit("updated", vlayout);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
@ -1,9 +1,16 @@
|
||||
<template>
|
||||
<article v-if="item" class="message" :class="item.style">
|
||||
<div v-if="item.title" class="message-header">
|
||||
<p>{{ item.title }}</p>
|
||||
<article v-if="show" class="message" :class="message.style">
|
||||
<div v-if="message.title || message.icon" class="message-header">
|
||||
<p>
|
||||
<i v-if="message.icon" :class="`fa-fw ${message.icon}`"></i>
|
||||
{{ message.title }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="item.content" class="message-body" v-html="item.content"></div>
|
||||
<div
|
||||
v-if="message.content"
|
||||
class="message-body"
|
||||
v-html="message.content"
|
||||
></div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
@ -13,19 +20,25 @@ export default {
|
||||
props: {
|
||||
item: Object,
|
||||
},
|
||||
created: function () {
|
||||
data: function () {
|
||||
return {
|
||||
show: false,
|
||||
message: {},
|
||||
};
|
||||
},
|
||||
created: async function () {
|
||||
// Look for a new message if an endpoint is provided.
|
||||
let that = this;
|
||||
this.message = Object.assign({}, this.item);
|
||||
if (this.item && this.item.url) {
|
||||
this.getMessage(this.item.url).then(function (message) {
|
||||
// keep the original config value if no value is provided by the endpoint
|
||||
for (const prop of ["title", "style", "content"]) {
|
||||
if (prop in message && message[prop] !== null) {
|
||||
that.item[prop] = message[prop];
|
||||
}
|
||||
const fetchedMessage = await this.getMessage(this.item.url);
|
||||
// keep the original config value if no value is provided by the endpoint
|
||||
for (const prop of ["title", "style", "content"]) {
|
||||
if (prop in fetchedMessage && fetchedMessage[prop] !== null) {
|
||||
this.message[prop] = fetchedMessage[prop];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
this.show = this.message.title || this.message.content;
|
||||
},
|
||||
methods: {
|
||||
getMessage: function (url) {
|
||||
|
@ -9,7 +9,7 @@
|
||||
aria-expanded="false"
|
||||
class="navbar-burger"
|
||||
:class="{ 'is-active': showMenu }"
|
||||
v-on:click="$emit('navbar:toggle')"
|
||||
v-on:click="$emit('navbar-toggle')"
|
||||
>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
@ -21,15 +21,14 @@
|
||||
<a
|
||||
class="navbar-item"
|
||||
rel="noreferrer"
|
||||
v-for="link in links"
|
||||
:key="link.url"
|
||||
v-for="(link, key) in links"
|
||||
:key="key"
|
||||
:href="link.url"
|
||||
:target="link.target"
|
||||
>
|
||||
<i
|
||||
v-if="link.icon"
|
||||
style="margin-right: 6px;"
|
||||
:class="link.icon"
|
||||
:class="['fa-fw', link.icon, { 'mr-2': link.name }]"
|
||||
></i>
|
||||
{{ link.name }}
|
||||
</a>
|
||||
|
@ -2,12 +2,13 @@
|
||||
<div class="search-bar">
|
||||
<label for="search" class="search-label"></label>
|
||||
<input
|
||||
id="searchBox"
|
||||
type="text"
|
||||
ref="search"
|
||||
:value="value"
|
||||
@input="$emit('input', $event.target.value.toLowerCase())"
|
||||
@keyup.enter.exact="$emit('search:open')"
|
||||
@keyup.alt.enter="$emit('search:open', '_blank')"
|
||||
@keyup.enter.exact="$emit('search-open')"
|
||||
@keyup.alt.enter="$emit('search-open', '_blank')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@ -20,7 +21,7 @@ export default {
|
||||
this._keyListener = function (event) {
|
||||
if (event.key === "/") {
|
||||
event.preventDefault();
|
||||
this.$emit("search:focus");
|
||||
this.$emit("search-focus");
|
||||
this.$nextTick(() => {
|
||||
this.$refs.search.focus();
|
||||
});
|
||||
@ -28,7 +29,7 @@ export default {
|
||||
if (event.key === "Escape") {
|
||||
this.$refs.search.value = "";
|
||||
this.$refs.search.blur();
|
||||
this.$emit("search:cancel");
|
||||
this.$emit("search-cancel");
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", this._keyListener.bind(this));
|
||||
|
@ -1,40 +1,26 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="card">
|
||||
<a :href="item.url" :target="item.target" rel="noreferrer">
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<div v-if="item.logo" class="media-left">
|
||||
<figure class="image is-48x48">
|
||||
<img :src="item.logo" :alt="`${item.name} logo`" />
|
||||
</figure>
|
||||
</div>
|
||||
<div v-if="item.icon" class="media-left">
|
||||
<figure class="image is-48x48">
|
||||
<i style="font-size: 35px;" :class="item.icon"></i>
|
||||
</figure>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<p class="title is-4">{{ item.name }}</p>
|
||||
<p class="subtitle is-6">{{ item.subtitle }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tag" :class="item.tagstyle" v-if="item.tag">
|
||||
<strong class="tag-text">#{{ item.tag }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<component v-on="$listeners" v-bind:is="component" :item="item"></component>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Generic from "./services/Generic.vue";
|
||||
|
||||
export default {
|
||||
name: "Service",
|
||||
components: {
|
||||
Generic,
|
||||
},
|
||||
props: {
|
||||
item: Object,
|
||||
},
|
||||
computed: {
|
||||
component() {
|
||||
const type = this.item.type || "Generic";
|
||||
if (type == "Generic") {
|
||||
return Generic;
|
||||
}
|
||||
return () => import(`./services/${type}.vue`);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
@ -1,40 +0,0 @@
|
||||
<template>
|
||||
<a v-on:click="toggleSetting()" class="navbar-item is-inline-block-mobile">
|
||||
<span><i :class="['fas', value ? icon : iconAlt]"></i></span>
|
||||
<slot></slot>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SettingToggle",
|
||||
props: {
|
||||
name: String,
|
||||
icon: String,
|
||||
iconAlt: String,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
value: true,
|
||||
};
|
||||
},
|
||||
created: function () {
|
||||
if (!this.iconAlt) {
|
||||
this.iconAlt = this.icon;
|
||||
}
|
||||
|
||||
if (this.name in localStorage) {
|
||||
this.value = JSON.parse(localStorage[this.name]);
|
||||
}
|
||||
|
||||
this.$emit("updated", this.value);
|
||||
},
|
||||
methods: {
|
||||
toggleSetting: function () {
|
||||
this.value = !this.value;
|
||||
localStorage[this.name] = this.value;
|
||||
this.$emit("updated", this.value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
84
src/components/services/Generic.vue
Normal file
@ -0,0 +1,84 @@
|
||||
<script>
|
||||
export default {};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
||||
<script>
|
||||
export default {};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="card"
|
||||
:style="`background-color:${item.background};`"
|
||||
:class="item.class"
|
||||
>
|
||||
<a
|
||||
:href="item.url"
|
||||
class="linkoverlay"
|
||||
:target="item.target"
|
||||
rel="noreferrer"
|
||||
></a>
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<div v-if="item.logo" class="media-left">
|
||||
<figure class="image is-48x48">
|
||||
<img :src="item.logo" :alt="`${item.name} logo`" />
|
||||
</figure>
|
||||
</div>
|
||||
<div v-if="item.icon" class="media-left">
|
||||
<figure class="image is-48x48">
|
||||
<i class="thirty-five" :class="['fa-fw', item.icon]"></i>
|
||||
</figure>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<p class="title is-4">{{ item.name }}</p>
|
||||
<p class="subtitle is-6">{{ item.subtitle }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
v-if="item.info"
|
||||
:href="item.info"
|
||||
:target="item.infotarget"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<div class="infolink">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
</div>
|
||||
</a>
|
||||
<div
|
||||
v-on:click="filterTag()"
|
||||
class="tag is-clickable"
|
||||
:class="item.tagstyle"
|
||||
v-if="item.tag"
|
||||
>
|
||||
<strong class="tag-text">#{{ item.tag }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Generic",
|
||||
props: {
|
||||
item: Object,
|
||||
},
|
||||
methods: {
|
||||
filterTag: function () {
|
||||
this.$emit("filter", this.item.tag.toLowerCase());
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.media-left img {
|
||||
max-height: 100%;
|
||||
}
|
||||
</style>
|
96
src/components/services/PiHole.vue
Normal file
@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="card" :class="item.class">
|
||||
<a :href="item.url" :target="item.target" rel="noreferrer">
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<div v-if="item.logo" class="media-left">
|
||||
<figure class="image is-48x48">
|
||||
<img :src="item.logo" :alt="`${item.name} logo`" />
|
||||
</figure>
|
||||
</div>
|
||||
<div v-if="item.icon" class="media-left">
|
||||
<figure class="image is-48x48">
|
||||
<i class="thirty-five" :class="['fa-fw', item.icon]"></i>
|
||||
</figure>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<p class="title is-4">{{ item.name }}</p>
|
||||
<p class="subtitle is-6">{{ item.subtitle }}</p>
|
||||
</div>
|
||||
<div v-if="status" class="status" :class="status.status">
|
||||
{{ status.status }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-on:click="filterTag()"
|
||||
class="tag is-clickable"
|
||||
:class="item.tagstyle"
|
||||
v-if="item.tag"
|
||||
>
|
||||
<strong class="tag-text">#{{ item.tag }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "PiHole",
|
||||
props: {
|
||||
item: Object,
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
status: null,
|
||||
};
|
||||
},
|
||||
created: function () {
|
||||
this.fetchStatus();
|
||||
},
|
||||
methods: {
|
||||
fetchStatus: async function () {
|
||||
this.status = await fetch(`${this.item.url}/api.php`).then((response) =>
|
||||
response.json()
|
||||
);
|
||||
},
|
||||
filterTag: function () {
|
||||
this.$emit("filter", this.item.tag.toLowerCase());
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.media-left img {
|
||||
max-height: 100%;
|
||||
}
|
||||
.status {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-title);
|
||||
|
||||
&.enabled:before {
|
||||
background-color: #94e185;
|
||||
border-color: #78d965;
|
||||
box-shadow: 0px 0px 4px 1px #94e185;
|
||||
}
|
||||
|
||||
&.disabled:before {
|
||||
background-color: #c9404d;
|
||||
border-color: #c42c3b;
|
||||
box-shadow: 0px 0px 4px 1px #c9404d;
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: " ";
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
margin-right: 10px;
|
||||
border: 1px solid #000;
|
||||
border-radius: 7px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,3 +1,5 @@
|
||||
const manifestOptions = require("./public/assets/manifest.json");
|
||||
|
||||
module.exports = {
|
||||
chainWebpack: (config) => {
|
||||
config.module
|
||||
@ -12,89 +14,15 @@ module.exports = {
|
||||
manifestPath: "assets/manifest.json",
|
||||
appleMobileWebAppStatusBarStyle: "black",
|
||||
appleMobileWebAppCapable: "yes",
|
||||
name: "Homer Dashboard",
|
||||
short_name: "Homer",
|
||||
theme_color: "#3367D6",
|
||||
icons: [
|
||||
{
|
||||
src: "./assets/icons/android-chrome-192x192.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "./assets/icons/android-chrome-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "./assets/icons/android-chrome-maskable-192x192.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png",
|
||||
purpose: "maskable",
|
||||
},
|
||||
{
|
||||
src: "./assets/icons/android-chrome-maskable-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
purpose: "maskable",
|
||||
},
|
||||
{
|
||||
src: "./assets/icons/apple-touch-icon-60x60.png",
|
||||
sizes: "60x60",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "./assets/icons/apple-touch-icon-76x76.png",
|
||||
sizes: "76x76",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "./assets/icons/apple-touch-icon-120x120.png",
|
||||
sizes: "120x120",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "./assets/icons/apple-touch-icon-152x152.png",
|
||||
sizes: "152x152",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "./assets/icons/apple-touch-icon-180x180.png",
|
||||
sizes: "180x180",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "./assets/icons/apple-touch-icon.png",
|
||||
sizes: "180x180",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "./assets/icons/favicon-16x16.png",
|
||||
sizes: "16x16",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "./assets/icons/favicon-32x32.png",
|
||||
sizes: "32x32",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "./assets/icons/msapplication-icon-144x144.png",
|
||||
sizes: "144x144",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "./assets/icons/mstile-150x150.png",
|
||||
sizes: "150x150",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
name: manifestOptions.name,
|
||||
themeColor: manifestOptions.theme_color,
|
||||
manifestOptions,
|
||||
iconPaths: {
|
||||
favicon32: "assets/icons/favicon-32x32.png",
|
||||
favicon16: "assets/icons/favicon-16x16.png",
|
||||
appleTouchIcon: "assets/icons/apple-touch-icon-152x152.png",
|
||||
appleTouchIcon: "assets/icons/icon-maskable.png",
|
||||
maskIcon: "assets/icons/safari-pinned-tab.svg",
|
||||
msTileImage: "assets/icons/msapplication-icon-144x144.png",
|
||||
msTileImage: "assets/icons/icon-any.png",
|
||||
},
|
||||
},
|
||||
};
|
||||
|