Compare commits

...

40 Commits

Author SHA1 Message Date
Ross Mountjoy
9ecb749601 removed border-bottom from sidenav collection items. 2020-05-30 12:00:15 -04:00
Ross Mountjoy
6b362950f7 dark mode toggle on sidenav works now. it changes the user's theme setting. 2020-05-30 11:47:56 -04:00
Ross Mountjoy
634d21ac2d ctrl + s now submits the config editor form. 2020-05-30 10:05:40 -04:00
Ross Mountjoy
38f7c18e2f manually adding #152 to develop 2020-05-30 09:39:38 -04:00
Ross Mountjoy
29f5054913 fixed issue with plex platform 2020-05-30 09:27:50 -04:00
sportivaman
d85dff85fe
Merge pull request #168 from saponace/feat-new-apps-templates
Add apps templates for Cyberchef, Kibana, Filebrowser, Authelia, YoutubeDL-Material and Dozzle
2020-05-30 09:25:48 -04:00
Ross Mountjoy
29b9fe3d22 (MANUALLY ADDING PR FROM reedhaffner 0.6 version) Add Plex as a platform 2020-05-30 09:24:37 -04:00
Ross Mountjoy
3a4cd45313 (MANUALLY ADDING PR FROM reedhaffner ) Add available fields in Deluge platform 2020-05-30 09:17:18 -04:00
Ross Mountjoy
52de05cfd4 (MANUALLY ADDING PR FROM Thlb 0.6) Add Docker platform and template app 2020-05-30 09:12:56 -04:00
Ross Mountjoy
5240f6210c (MANUALLY ADDING PR FROM Thlb 0.6 version) Add Lidarr platforms 2020-05-30 08:55:09 -04:00
Ross Mountjoy
19ac14ed0e (MANUALLY ADDING PR FROM Thlb 0.6 version) Add Sonarr,Radarr,Tautulli,Healthchecks platforms 2020-05-30 08:25:16 -04:00
Remi Somdecoste-Lespoune
d125645bcc Add Dozzle template 2020-05-28 01:30:21 +02:00
Remi Somdecoste-Lespoune
1999511fdc Add YoutubeDL-Material template 2020-05-27 15:49:56 +02:00
Remi Somdecoste-Lespoune
4ad069a93a Add Authelia template 2020-05-27 15:43:14 +02:00
Remi Somdecoste-Lespoune
f9a883b563 Add Filebrowser template 2020-05-27 15:39:05 +02:00
Remi Somdecoste-Lespoune
7f6ae09077 Add Kibana template 2020-05-27 15:34:25 +02:00
Remi Somdecoste-Lespoune
a779c2e7dd Add Cyberchef template 2020-05-27 15:25:27 +02:00
Ross Mountjoy
9243692217 - fixed issue where expanding cards were breaking grid 2020-05-21 15:19:23 -04:00
Ross Mountjoy
401ddbe5c6 - fixed searchbar covering sidenav 2020-05-21 13:10:18 -04:00
Ross Mountjoy
962bfa780d - disabled expanding cards on mobile devices 2020-05-21 12:05:18 -04:00
Ross Mountjoy
4b5b7a9464 - actually fixed that last issue. 2020-05-21 11:44:07 -04:00
Ross Mountjoy
a8e15ac437 - fixed error causing collections to display over the tag selector dropdown 2020-05-21 11:38:57 -04:00
Ross Mountjoy
f5c246788d - fixed error causing wiki form to show on config editor 2020-05-21 11:16:43 -04:00
Ross Mountjoy
88c1f5fb15 - started the wiki system
- improved collections
- implemented auto expand on hover
2020-05-21 11:10:31 -04:00
Ross Mountjoy
daa4fe6c8f - fixes #163 2020-05-11 06:14:44 -04:00
Ross Mountjoy
7a5309fcf7 - fixed error in dockerfile 2020-05-10 09:00:13 -04:00
Ross Mountjoy
c2e06ff7a1 - fixed error in dockerfile 2020-05-09 12:54:19 -04:00
Ross Mountjoy
c521b41765 - fixed issue with 'info' tab of settings editor not appearing on desktop size
- fixed 'tags' gui section if no tags exist in config
- fixed issue with tag selector visibility
2020-05-08 13:12:13 -04:00
Ross Mountjoy
e63aa75c22 trying to fix docker's annoying git issues. 2020-05-08 12:19:23 -04:00
Ross Mountjoy
c00f5bbf8b trying to fix docker's annoying git issues. 2020-05-08 12:14:03 -04:00
Ross Mountjoy
81d3f96f3a trying to fix docker's annoying git issues. 2020-05-08 12:11:13 -04:00
Ross Mountjoy
0b2bb955cf tracking codemirror 2020-05-08 11:47:50 -04:00
Ross Mountjoy
0b37598508 - fixed problem causing failure to start if no tags or action providers
- replaced 'no cards go to settings' with a tap target
2020-05-08 10:23:15 -04:00
Ross Mountjoy
62191b21b8 ##### Updated to version 0.6!
> Version 0.6 brings DashMachine one big step forward to being a finished product by adding a gui to edit the various settings in the config.ini.

**Changelog**
- improvements to /home including 'pinned' cards, multi-select tag filtering, 'action providers' allowing you to do web searches from the searchbar
- rebuilt sidenav with list view, mirroring filter/search/collapse state of the homepage
- /settings and /home now on same route
- dynamic reloading of settings (no more page reloads)
- dedicated config.ini editor slide-out
- settings editor slide-out
- card editor slide-out
- better access group control
- dedicated documentation pages
- improved documentation
- new system for automatically generating documentation for platforms
- ability to load custom platforms
- added an 'on_starup' method for platforms allowing for registering api routes. (example coming soon)
2020-05-07 09:27:18 -04:00
sportivaman
dc1863ffc7
Merge pull request #124 from rmountjoy92/master
merge 56b170b with develop
2020-04-12 14:00:28 -04:00
sportivaman
aaf8913de3
Merge pull request #114 from marcjmiller/fixRunPyForRemoteExecution
updated run.py to fix behavior when run remotely (i.e. init.d scripts)
2020-04-12 13:57:13 -04:00
sportivaman
7f29f3f0d1
Merge pull request #119 from rxmii4269/develop
added support for sabnzbd
2020-04-12 13:08:13 -04:00
Romaine Murray
f2cf017be9
Merge pull request #1 from rxmii4269/master
Added Support for sabnzbd
2020-04-08 22:28:28 -05:00
romaine murray
9846f3d1b8 added support for sabnzbd 2020-04-08 10:50:33 -05:00
Marc Miller
753126a39c updated run.py to fix behavior when run remotely (i.e. init.d scripts) 2020-04-07 23:37:19 -04:00
721 changed files with 115192 additions and 2456 deletions

5
.gitignore vendored
View File

@ -14,7 +14,6 @@ dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
@ -117,7 +116,9 @@ scheduler.db
scheduler.db
.idea/
.vscode/
dashmachine/user_data/
dashmachine/static/images/icons
dashmachine/static/images/backgrounds
dashmachine/static/images/backgrounds
dashmachine/platform/custom_*

View File

@ -1,16 +1,8 @@
# DashMachine
### Another web application bookmark dashboard, with fun features.
##### Another web application bookmark dashboard, with fun features.
## Before Installing
Please read the latest update post: https://www.reddit.com/r/DashMachine/comments/fqk8gl/version_05/
![screenshot](https://raw.githubusercontent.com/rmountjoy92/DashMachine/master/screenshot1.png)
![screenshot](https://raw.githubusercontent.com/rmountjoy92/DashMachine/master/screenshot2.png)
![screenshot](https://raw.githubusercontent.com/rmountjoy92/DashMachine/master/screenshot3.png)
![screenshot](https://raw.githubusercontent.com/rmountjoy92/DashMachine/master/screenshot4.png)
### Demo
* [Go to live demo](#)
### Features
* creates a dashboard to view web pages
@ -27,83 +19,27 @@ Please read the latest update post: https://www.reddit.com/r/DashMachine/comment
* multiple users, access groups, access settings
* tagging system
## Installation
### Docker
```
docker create \
--name=dashmachine \
-p 5000:5000 \
-v path/to/data:/dashmachine/dashmachine/user_data \
--restart unless-stopped \
rmountjoy/dashmachine:latest
```
To run in a subfolder, use a CONTEXT_PATH environment variable. For example, to run at localhost:5000/dash:
```
docker create \
--name=dashmachine \
-p 5000:5000 \
-e CONTEXT_PATH=/dash
-v path/to/data:/dashmachine/dashmachine/user_data \
--restart unless-stopped \
rmountjoy/dashmachine:latest
```
### Synology
Check out this awesome guide: https://nashosted.com/manage-your-self-hosted-applications-using-dashmachine/
### Python
Instructions are for linux.
```
virtualenv --python=python3 DashMachineEnv
cd DashMachineEnv && source bin/activate
git clone https://github.com/rmountjoy92/DashMachine.git
cd DashMachine && pip install -r requirements.txt
python3 run.py
```
Then open a web browser and go to localhost:5000
## Default user/password
```
User: admin
Password: admin
```
### Want to contribute?
* Please submit your pull request on the develop branch, not master.
* Please use the [pull request template](https://github.com/rmountjoy92/DashMachine/blob/master/pull_request_template.md)
* See this link for [how to create a pull request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request)
## Updating
For python, use git. For docker, just pull the latest image and recreate the container.
## Configuration
The user data folder is located at DashMachine/dashmachine/user_data. This is where the config.ini, custom backgrounds/icons, and the database file live. A reference for what can go into the config.ini file can be found on the settings page of the dashmachine by clicking the info icon next to 'Config'.
### Subreddit
* [Go to subreddit](https://www.reddit.com/r/DashMachine)
### Note
If you change the config.ini file, you either have to restart the container (or python script) or click the 'save' button in the config section of settings for the config to be applied. Pictures added to the backgrounds/icons folders are available immediately.
### Want to buy me a coffee?
* [Librepay](https://liberapay.com/rmountjoy/donate)
* [Bountysource](https://www.bountysource.com/teams/dashmachine-app)
## Want to contribute?
Please use the pull request template at:
https://github.com/rmountjoy92/DashMachine/blob/master/pull_request_template.md
### Want a feature to be added faster?
* [Open a bounty](https://www.bountysource.com/)
* [Bountysource faq](https://github.com/bountysource/core/wiki/Frequently-Asked-Questions)
See this link for how to create a pull request:
https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request
## Subreddit
https://www.reddit.com/r/DashMachine
## Want to buy me a coffee?
recurring:
<a href="https://liberapay.com/rmountjoy/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a>
recurring or one-time:
https://www.bountysource.com/teams/dashmachine-app
## Want a feature to be added faster?
Open a bounty on https://www.bountysource.com/
Bountysource faq: https://github.com/bountysource/core/wiki/Frequently-Asked-Questions
## Tech used
### Tech used
* Flask
* SQLalchemy w/ SQLite
* Jinja2
* Materialize css
* JavaScript/jQuery/jQueryUI
## FAQs
1. application does not work in iframe
see https://github.com/rmountjoy92/DashMachine/issues/6

View File

@ -1,5 +1,6 @@
#!/usr/bin/env python3
import os
import uuid
from flask import Flask
from flask_caching import Cache
from flask_sqlalchemy import SQLAlchemy
@ -11,13 +12,23 @@ from dashmachine.paths import user_data_folder
if not os.path.isdir(user_data_folder):
os.mkdir(user_data_folder)
secret_file = os.path.join(user_data_folder, ".secret")
if not os.path.isfile(secret_file):
with open(secret_file, "w") as new_file:
new_file.write(uuid.uuid4().hex)
with open(secret_file, "r") as secret_file:
secret_key = secret_file.read().encode("utf-8")
if len(secret_key) < 32:
secret_key = uuid.uuid4().hex
context_path = os.getenv("CONTEXT_PATH", "")
app = Flask(__name__, static_url_path=context_path + "/static")
cache = Cache(app, config={"CACHE_TYPE": "simple"})
api = Api(app)
app.config["AVATARS_IDENTICON_BG"] = (255, 255, 255)
app.config["SECRET_KEY"] = "66532a62c4048f976e22a39638b6f10e"
app.config["SECRET_KEY"] = secret_key
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///user_data/site.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 0
@ -31,12 +42,14 @@ from dashmachine.main.routes import main
from dashmachine.user_system.routes import user_system
from dashmachine.error_pages.routes import error_pages
from dashmachine.settings_system.routes import settings_system
from dashmachine.docs_system.routes import docs_system
from dashmachine import sources
app.register_blueprint(main, url_prefix=context_path)
app.register_blueprint(user_system, url_prefix=context_path)
app.register_blueprint(error_pages, url_prefix=context_path)
app.register_blueprint(settings_system, url_prefix=context_path)
app.register_blueprint(docs_system, url_prefix=context_path)
from dashmachine.rest_api.resources import *

View File

View File

@ -0,0 +1,524 @@
import os
import importlib
from dashmachine.paths import platform_folder
from dashmachine.main.models import DataSources
doc_toc_string = """
<a name="top"></a>
### Platforms
{% for doc_dict in doc_dicts %}
> - [{{ doc_dict['name'] }}](#{{ doc_dict['name'] }})
{% endfor %}
"""
base_md_string = """
{% if doc_dict['author'] %}
[ Go to top](#top)
<br>
{% endif %}
### {{ doc_dict['name'] }}
{{ doc_dict['description'] }}
{%+ if doc_dict['author'] -%}
- Author: [{{ doc_dict['author'] }}]({{ doc_dict['author_url'] }})
{% endif %}
{%+ if doc_dict['version'] -%}
- Version: {{ doc_dict['version'] }}
{% endif %}
{%+ if doc_dict['returns'] -%}
- Returns: {{ doc_dict['returns'] }}
{% endif %}
{%+ if doc_dict['returns_json_keys'] -%}
- Available template variables: {% for key in doc_dict['returns_json_keys'] %}"{{key}}", {% endfor %}
{% endif %}
##### Default config
```ini
{{ doc_dict['variables'][0]['variable'] }}
{%+ for variable in doc_dict['variables'][1:] -%}
{{ variable['variable'] }} = {{ variable['default']|safe }}
{%+ endfor -%}
```
| Variable | Description | Default | Options |
|----------------------------|-------------------------------|---------------------------|---------------------------|
{%+ for variable in doc_dict['variables'] -%}
| {{ variable['variable'] }} | {{ variable['description'] }} | {{ variable['default']|replace("|","&#124;")|safe }} | {{ variable['options'] }} |
{% endfor %}
{%+ for variable in doc_dict['variables'] -%}
{% if variable['variables']|length > 0 %}
<br>
>##### {{ variable['variable'] }}
>```ini
>{{ variable['variable'] }} = { {%- for subvariable in variable['variables'] -%} "{{ subvariable['variable'] }}": "{{ subvariable['default'] }}",{%- endfor -%} }
>```
>| Variable | Description | Default | Options |
>|-------------------------------|----------------------------------|------------------------------|------------------------------|
{%+ for subvariable in variable['variables'] -%}
>| {{ subvariable['variable'] }} | {{ subvariable['description'] }} | {{ subvariable['default']|safe }} | {{ subvariable['options'] }} |
{% endfor %}
{% endif %}
{% endfor %}
{% if doc_dict['example']|length > 0 %}
##### Example
{{ doc_dict['example']|safe }}
{% endif %}
<div class="divider"></div>
"""
settings_doc_dict = {
"name": "Settings",
"description": "This is the configuration entry for DashMachine's settings. DashMachine will not work if this is missing. As for all config entries, [Settings] can only appear once in the config. If you change the config.ini file, you either have to restart the container (or python script) or click the save button in the config section of settings for the config to be applied.",
"variables": [
{
"variable": "[Settings]",
"description": "Config section name.",
"default": "",
"options": ".ini header",
},
{
"variable": "theme",
"description": "UI theme",
"default": "light",
"options": "light, dark",
},
{
"variable": "accent",
"description": "UI accent color",
"default": "orange",
"options": "orange, red, pink, purple, deepPurple, indigo, blue, lightBlue,cyan, teal, green, lightGreen, lime, yellow, amber, deepOrange, brown, grey, blueGrey",
},
{
"variable": "background",
"description": "Background image for the UI",
"default": "None",
"options": "/static/images/backgrounds/yourpicture.png, external link to image, None, random",
},
{
"variable": "roles",
"description": "User roles for access groups.",
"default": "admin,user,public_user",
"options": "comma separated string, Note: admin, user, public_user roles are required and will be added automatically if omitted.",
},
{
"variable": "custom_app_title",
"description": "Change the title of the app for browser tabs",
"default": "Dashmachine",
"options": "string",
},
{
"variable": "tags_expanded",
"description": "Set to False to have your tags collapsed by default",
"default": "True",
"options": "True, False",
},
{
"variable": "tags",
"description": "Set custom options for your tags",
"default": '{"name": "","icon": "","sort_pos": "",}',
"options": "comma separated json dicts",
"variables": [
{
"variable": "name",
"description": "The name of the tag",
"default": "",
"options": "string",
},
{
"variable": "icon",
"description": "The icon for the tag",
"default": "",
"options": "Use material design icons: https://material.io/resources/icons",
},
{
"variable": "sort_pos",
"description": "The sort position for the tag",
"default": "",
"options": "number",
},
],
},
{
"variable": "action_providers",
"description": "Set custom actions for the search bars. In the search bar, press '!' followed by your configured macro to run the action. A common action would be running a search on a search provider.",
"default": '{"name": "Google","macro": "g","action": "https://www.google.com/search?q={{ value }}"}',
"options": "comma separated json dicts",
"variables": [
{
"variable": "name",
"description": "The name of the action",
"default": "Google",
"options": "string",
},
{
"variable": "macro",
"description": "A key or set of keys that you will type after '!'",
"default": "g",
"options": "string",
},
{
"variable": "action",
"description": "jinja template url with the value of the search bar available as 'value'.",
"default": "https://www.google.com/search?q={{ value }}",
"options": "jinja template",
},
],
},
],
}
access_groups_doc_dict = {
"name": "Access Groups",
"description": "You can create access groups to control what user roles can access parts of the ui. Access groups are just a collection of roles, and each user has an attribute 'role'. Each application can have an access group, if the user's role is not in the group, the app will be hidden.",
"variables": [
{
"variable": "[name]",
"description": "Name for access group.",
"default": "",
"options": ".ini header",
},
{
"variable": "roles",
"description": "A comma separated list of user roles allowed to view apps in this access group",
"default": "admin",
"options": "Roles defined in your config.",
},
{
"variable": "can_access_home",
"description": "Control if this user is allowed to access /home",
"default": "True",
"options": "True,False",
},
{
"variable": "can_access_user_settings",
"description": "Control if this user is allowed to access their user settings",
"default": "True",
"options": "True,False",
},
{
"variable": "can_access_main_settings",
"description": "Control if this user is allowed to access the global dashmachine settings.",
"default": "False",
"options": "True,False",
},
{
"variable": "can_access_card_editor",
"description": "Control if this user is allowed to access the card editor",
"default": "False",
"options": "True,False",
},
{
"variable": "can_access_raw_config",
"description": "Control if this user is allowed to access the config.ini editor",
"default": "False",
"options": "True,False",
},
{
"variable": "can_access_docs",
"description": "Control if this user is allowed to access the documentation",
"default": "False",
"options": "True,False",
},
{
"variable": "can_see_sidenav",
"description": "Control if this user is allowed to see the sidenav",
"default": "True",
"options": "True,False",
},
{
"variable": "can_edit_users",
"description": "Control if this user is allowed edit other users and their settings",
"default": "False",
"options": "True,False",
},
{
"variable": "can_edit_images",
"description": "Control if this user is allowed edit images in settings",
"default": "False",
"options": "True,False",
},
],
}
user_settings_doc_dict = {
"name": "Users",
"description": "Each user requires a config entry, and there must be at least one user in the config (otherwise the default user is added). Each user has a username, a role for configuring access groups, and a password. By default there is one user, named 'admin', with role 'admin' and password 'admin'. To change this user's name, password or role, just modify the config entry's variables and press save. To add a new user, add another user config entry UNDER all existing user config entries. A user with role 'admin' must appear first in the config. Do not change the order of users in the config once they have been defined, otherwise their passwords will not match the next time the config is applied. When users are removed from the config, they are deleted and their cached password is also deleted when the config is applied.",
"variables": [
{
"variable": "[username]",
"description": "The user's name for logging in.",
"default": "admin",
"options": ".ini header",
},
{
"variable": "role",
"description": "The user's role. This is used for access groups and controlling who can view /home and /settings. There must be at least one 'admin' user, and it must be defined first in the config. Otherwise, the first user will be set to admin.",
"default": "admin",
"options": "string",
},
{
"variable": "password",
"description": "Add a password to this variable to change the password for this user. The password will be hashed, cached and removed from the config. When adding a new user, specify the password, otherwise 'admin' will be used.",
"default": "",
"options": "string",
},
{
"variable": "confirm_password",
"description": "When adding a new user or changing an existing user's password you must confirm the password in this variable",
"default": "",
"options": "string",
},
{
"variable": "theme",
"description": "Override the theme from Settings for this user",
"default": "",
"options": "same as Settings",
},
{
"variable": "accent",
"description": "Override the accent from Settings for this user",
"default": "",
"options": "same as Settings",
},
{
"variable": "background",
"description": "Override the background from Settings for this user",
"default": "",
"options": "same as Settings",
},
{
"variable": "tags_expanded",
"description": "Override the tags_expanded from Settings for this user",
"default": "",
"options": "same as Settings",
},
],
}
apps_doc_dict = {
"name": "App",
"description": "These entries are the standard card type for displaying apps on your dashboard and sidenav.",
"variables": [
{
"variable": "[name]",
"description": "The name of your app. ",
"default": "",
"options": ".ini header",
},
{
"variable": "prefix",
"description": "The prefix for the app's url. ",
"default": "https://",
"options": "web prefix, e.g. http:// or https://",
},
{
"variable": "url",
"description": "The url for your app.",
"default": "",
"options": "web url, e.g. myapp.com",
},
{
"variable": "open_in",
"description": "open the app in the current tab, an iframe or a new tab",
"default": "this_tab",
"options": "web url, e.g. myapp.com ",
},
{
"variable": "icon",
"description": "Icon for the dashboard.",
"default": "static/images/apps/default.png",
"options": "/static/images/icons/yourpicture.png, external link to image",
},
{
"variable": "sidebar_icon",
"description": "Icon for the sidenav.",
"default": "static/images/apps/default.png",
"options": "/static/images/icons/yourpicture.png, external link to image",
},
{
"variable": "description",
"description": "A short description for the app.",
"default": "",
"options": "HTML",
},
{
"variable": "data_sources",
"description": "Data sources to be included on the app's card.*Note: you must have a data source set up in the config above this application entry.",
"default": "",
"options": "comma separated string",
},
{
"variable": "tags",
"description": "Optionally specify tags for organization on /home",
"default": "",
"options": "comma separated string",
},
{
"variable": "groups",
"description": "Optionally specify the access groups that can see this app.",
"default": "",
"options": "comma separated string",
},
],
}
collections_doc_dict = {
"name": "Collection",
"description": "These entries provide a card on the dashboard containing a list of links.",
"variables": [
{
"variable": "[name]",
"description": "Name for the collection",
"default": "",
"options": ".ini header",
},
{
"variable": "type",
"description": "This tells DashMachine what type of card this is.",
"default": "collection",
"options": "collection",
"disabled": "True",
},
{
"variable": "icon",
"description": "The material design icon class for the collection.",
"default": "collections_bookmark",
"options": "https://material.io/resources/icons",
},
{
"variable": "tags",
"description": "Optionally specify tags for organization on /home",
"default": "",
"options": "comma separated string",
},
{
"variable": "groups",
"description": "Optionally specify the access groups that can see this app.",
"default": "",
"options": "comma separated string",
},
{
"variable": "urls",
"description": "The urls to include in your collection.",
"default": '{"url": "https://google.com", "icon": "static/images/apps/default.png", "name": "Google", "open_in": "new_tab"},{"url": "https://duckduckgo.com", "icon": "static/images/apps/default.png", "name": "DuckDuckGo", "open_in": "this_tab"}',
"options": "comma separated json dicts",
"variables": [
{
"variable": "url",
"description": "The url for the collection item",
"default": "https://google.com",
"options": "web url",
},
{
"variable": "icon",
"description": "The icon for the collection item",
"default": "static/images/apps/default.png",
"options": "/static/images/icons/yourpicture.png, external link to image",
},
{
"variable": "open_in",
"description": "Which mode to open the link in",
"default": "this_tab",
"options": "this_tab,new_tab",
},
],
},
],
}
custom_card_example = """
```ini
[variable_name]
platform = weather
woeid = 2514815
[custom_card_name]
type = custom
data_sources = variable_name
```
"""
custom_card_doc_dict = {
"name": "Custom Card",
"description": "These entries provide an empty card on the dashboard to be populated by a data source. This allows the data source to populate the entire card.",
"example": custom_card_example,
"variables": [
{
"variable": "[name]",
"description": "Name for the custom card",
"default": "",
"options": ".ini header",
},
{
"variable": "type",
"description": "This tells DashMachine what type of card this is.",
"default": "custom",
"options": "custom",
"disabled": "True",
},
{
"variable": "data_sources",
"description": "What data sources to display on the card.",
"default": "",
"options": "comma separated string",
},
{
"variable": "tags",
"description": "Optionally specify tags for organization on /home",
"default": "",
"options": "comma separated string",
},
{
"variable": "groups",
"description": "Optionally specify the access groups that can see this app.",
"default": "",
"options": "comma separated string",
},
],
}
def data_sources_doc_dicts(platform_name=None, get_all=False):
if platform_name:
platform_names = [platform_name]
elif get_all:
platform_names = [
file.replace(".py", "") for file in os.listdir(platform_folder)
]
platform_names.remove("__pycache__")
platform_names.remove("__init__")
data_source_dicts = []
for name in platform_names:
module = importlib.import_module(f"dashmachine.platform.{name}", ".")
platform = module.Platform()
if getattr(platform, "docs", None):
docs = platform.docs()
data_source_dicts.append(docs)
return data_source_dicts
def configured_data_sources_doc_dicts(data_source_id=None, get_all=False):
if data_source_id:
data_sources = [DataSources.query.filter_by(id=data_source_id).first()]
elif get_all:
data_sources = DataSources.query.all()
data_source_dicts = []
for data_source in data_sources:
data_source_dict = {
"id": data_source.id,
"name": data_source.name,
"platform": data_source.platform,
}
module = importlib.import_module(
f"dashmachine.platform.{data_source.platform}", "."
)
platform = module.Platform()
docs = platform.docs()
for key, value in docs.items():
data_source_dict[key] = value
data_source_dicts.append(data_source_dict)
if data_source_id:
return data_source_dicts[0]
return data_source_dicts

View File

@ -0,0 +1,239 @@
### Creating Platforms
DashMachine platforms are python plugins loaded by Python's `importlib` module. When they are loaded by DM, they are initialized with their `Platform` class. The official platforms for dashmachine are located at `DashMachine/dashmachine/platform` and are a great resource for learning how they work.
> **NOTE:** When you modify or add one of the files in `DashMachine/dashmachine/platform`, changes will be wiped out by updates. If you want to experiment with your own platforms, put them in `DashMachine/dashmachine/user_data/platform` to persist updates.
<div class="divider"></div>
##### A minimal platform
Let's start by making a super simple platform. It's just going to return 'Hello World' to the card's data source container. First add a file called `hello_world.py` in `DashMachine/dashmachine/user_data/platform` with the contents:
```python
class Platform:
def docs(self):
# This is the function DM calls to get the metadata about your plaform.
# This is what DM uses to generate the documentation and options in the gui forms.
# It is very important that this information is correct.
documentation = {
"name": "hello_world",
"author": "RMountjoy",
"author_url": "https://github.com/rmountjoy92",
"version": 1.0,
"description": "return 'Hello World' to the card's data source container",
"returns": "Hello World",
"variables": [
{
"variable": "[variable_name]",
"description": "Name for the data source.",
"default": "",
"options": ".ini header",
},
{
"variable": "platform",
"description": "Name of the platform.",
"default": "hello_world",
"options": "hello_world",
},
],
}
return documentation
def __init__(self, *args, **kwargs):
# This is the initialization function.
# This should be reserved for setting up the platform's configuration.
# NOTE: this is not where you call APIs, as this code is run every time DM
# initializes your 'Platform' class (e.g. to pull documentation, etc).
# These following two lines parses the options supplied by the user in their config.ini
for key, value in kwargs.items():
self.__dict__[key] = value
def process(self):
# this is the function that is ran when the web client requests
# information from this platform (e.g. when the dashboard is loaded)
# this is where you call APIs, or whatever you're doing to get data.
return "Hello World"
```
**NOTE:** After you add a new platform file to your user_data/platforms folder, you will need to restart DashMachine for the platform to be available.
Then, we would add the following two entries to our config.ini **NOTE** All custom platforms must be prefixed with `custom_` when they are referenced in a `data_source` entry:
```ini
[hello_world_ds]
platform = custom_hello_world
[hello_world]
type = custom
data_sources = hello_world_ds
```
Now, on your dashboard, you should see this:
![ds-container](/static/images/docs/hello-world-ds-example1.png)
Ugly right!? Which leads us to the next section,
<div class="divider"></div>
##### Rendering HTML
Let's make some changes to our `hello_world.py` file, at the top of the file we would add:
```python
from flask import render_template_string
```
then we would change our `process()` method like so:
```python
def process(self):
text_to_display = "Hello World"
html_string = """
<div class="row center-align">
<div class="col s12">
<i class="material-icons-outlined theme-primary-text medium">language</i>
<h5 class="font-weight-900">{{ text_to_display }}</h5>
</div>
</div>
"""
return render_template_string(html_string, text_to_display=text_to_display)
```
Much better:
![ds-container](/static/images/docs/hello-world-ds-example2.png)
For a full reference of the html elements you have available to you, check out:
* [Materialize CSS](https://materializecss.com/)
* [Material Icons](https://material.io/resources/icons/?icon=settings&style=outline)
Remeber that your returned HTML can include `script` and `style` tags for including js/jquery/css
<div class="divider"></div>
##### Utilizing user configuration
Okay, let's say that instead of displaying our fixed 'Hello World' message, we'll display the `text_to_display` variable from their configuration. So the entries in the .ini file would look like:
```ini
[hello_world_ds]
platform = custom_hello_world
text_to_display = Hi there.
[hello_world]
type = custom
data_sources = hello_world_ds
```
and in our `hello_world.py` file the only thing we would have to change is:
```python
return render_template_string(html_string, text_to_display=text_to_display)
```
changes to:
```python
return render_template_string(html_string, text_to_display=self.text_to_display)
```
So, now our card displays whatever `text_to_display` is set to:
![ds-container](/static/images/docs/hello-world-ds-example3.png)
**NOTE:** Now that we have added a configuration option, we need to update our `docs()` method, otherwise our new option will not show up in gui forms:
```python
def docs(self):
documentation = {
"name": "hello_world",
"author": "RMountjoy",
"author_url": "https://github.com/rmountjoy92",
"version": 1.0,
"description": "return the value of 'text_to_display' to the card's data source container",
"returns": "text_to_display as formatted html",
"variables": [
{
"variable": "[variable_name]",
"description": "Name for the data source.",
"default": "",
"options": ".ini header",
},
{
"variable": "platform",
"description": "Name of the platform.",
"default": "hello_world",
"options": "hello_world",
},
{
"variable": "text_to_display",
"description": "the text to display on the card",
"default": "Hello World",
"options": "string",
},
],
}
return documentation
```
You'll notice on the new variable we just made that there is a `default` field. We need to make sure we have defaults set up for our configuration options, so users don't have to fill out optional configurations. To do so, we would rewrite our `__init__()` method like so:
```python
def __init__(self, *args, **kwargs):
for key, value in kwargs.items():
self.__dict__[key] = value
if not hasattr(self, "text_to_display"):
self.text_to_display = "Hello World"
```
To sum it all up, here is our user configurable version of our hello world platform:
```python
from flask import render_template_string
class Platform:
def docs(self):
documentation = {
"name": "hello_world",
"author": "RMountjoy",
"author_url": "https://github.com/rmountjoy92",
"version": 1.0,
"description": "return the value of 'text_to_display' to the card's data source container",
"returns": "text_to_display as formatted html",
"variables": [
{
"variable": "[variable_name]",
"description": "Name for the data source.",
"default": "",
"options": ".ini header",
},
{
"variable": "platform",
"description": "Name of the platform.",
"default": "hello_world",
"options": "hello_world",
},
{
"variable": "text_to_display",
"description": "the text to display on the card",
"default": "Hello World",
"options": "string",
},
],
}
return documentation
def __init__(self, *args, **kwargs):
for key, value in kwargs.items():
self.__dict__[key] = value
if not hasattr(self, "text_to_display"):
self.text_to_display = "Hello World"
def process(self):
html_string = """
<div class="row center-align">
<div class="col s12">
<i class="material-icons-outlined theme-primary-text medium">language</i>
<h5 class="font-weight-900">{{ text_to_display }}</h5>
</div>
</div>
"""
return render_template_string(html_string, text_to_display=self.text_to_display)
```
and in our config.ini:
```ini
[hello_world_ds]
platform = custom_hello_world
text_to_display = Hi there.
[hello_world]
type = custom
data_sources = hello_world_ds
```

View File

@ -0,0 +1,39 @@
### Data Sources
Data sources provide information to cards on the dashboard. Here's how it works in a nutshell:
1. If a card is configured with a data source, once the dashboard has loaded, the web client will go through each data source and request information from the server.
2. A data source 'platform' on the server handles the information request, and returns html to the web client.
3. The web client then appends the returned html in the 'data source container' on the card.
<div class="divider"></div>
##### For apps the 'data source container' is this area:
![ds-container](/static/images/docs/app-ds-container.png)
##### For custom cards the 'data source container' is this area:
![ds-container](/static/images/docs/custom-ds-container.png)
<div class="divider"></div>
##### What are 'platforms' and what can they do?
Platforms are simply a python file on the server, set up to take in configuration data from the config.ini and return html back to the web interface. The platforms included with DM are found at `DashMachine/dashmachine/platform`. This is the 'official' set of data source platforms created for DM by Ross and the community.
<br>
Some examples on what a platform can do:
- Any data creation/manipulation that the Python language is capable of
- make calls on REST APIs
- format data as html
- return html with javascript to create interactive cards, or interact with the DOM
- register new API resources on the DM server.
<div class="divider"></div>
##### Value Templates
Some platforms allow for the user to create the template for the data source in the configuration. The platform will provide a number of variables the user's template will have access to. If you see a platform with a variable `value_template`, you will need to set it as a jinja template html string. For example, the `pihole` platform provides `"domain_count", "queries", "blocked", "ads_percentage", "unique_domains", "forwarded", "cached", "total_clients", "unique_clients", "total_queries", "gravity_last_updated"`. So our value template could look like:
```ini
value_template = Ads Blocked Today: {{ blocked }}<br>Status: {{ status }}<br>Queries today: {{ queries }}
```
See [Jinja Templating](https://jinja.palletsprojects.com/en/2.11.x/templates/)

View File

@ -0,0 +1,8 @@
## Updating
For python, use git. For docker, just pull the latest image and recreate the container.
## Configuration
The user data folder is located at DashMachine/dashmachine/user_data. This is where the config.ini, custom backgrounds/icons, and the database file live. A reference for what can go into the config.ini file can be found on the settings page of the dashmachine by clicking the info icon next to 'Config'.
### Note
If you change the config.ini file, you either have to restart the container (or python script) or click the 'save' button in the config section of settings for the config to be applied. Pictures added to the backgrounds/icons folders are available immediately.

View File

@ -0,0 +1,56 @@
# Installation
### Default user/password
```
User: admin
Password: admin
```
### Docker
```bash
docker create \
--name=dashmachine \
-p 5000:5000 \
-v path/to/data:/dashmachine/dashmachine/user_data \
--restart unless-stopped \
rmountjoy/dashmachine:latest
```
To run in a subfolder, use a CONTEXT_PATH environment variable. For example, to run at localhost:5000/dash:
```bash
docker create \
--name=dashmachine \
-p 5000:5000 \
-e CONTEXT_PATH=/dash
-v path/to/data:/dashmachine/dashmachine/user_data \
--restart unless-stopped \
rmountjoy/dashmachine:latest
```
### Docker Compose
```yaml
version: "2"
services:
dashmachine:
image: rmountjoy/dashmachine:latest
container_name: dashmachine
restart: unless-stopped
environment:
- CONTEXT_PATH: /dash #Optional, only if you want to run dashmachine in a subfolder
volumes:
- /path/to/data:/dashmachine/dashmachine/user_data
ports:
- 5000:5000 #You can change the port on the left (host) it's already in use, e.g. Synology NAS
```
### Synology
* [Check out this awesome guide](https://nashosted.com/manage-your-self-hosted-applications-using-dashmachine/)
### Python
Instructions are for linux.
```bash
virtualenv --python=python3 DashMachineEnv
cd DashMachineEnv && source bin/activate
git clone https://github.com/rmountjoy92/DashMachine.git
cd DashMachine && pip install -r requirements.txt
python3 run.py
```
Then open a web browser and go to localhost:5000

View File

@ -0,0 +1,23 @@
### Platform Methods
These are the various methods DM will call on your Platform. Some are optional, some are required.
<div class="divider"></div>
##### `def docs(self):` (*required*)
This is the function DM calls to get the metadata about your plaform. This is what DM uses to generate the documentation and options in the gui forms. It is very important that this information is correct.
<div class="divider"></div>
##### `def __init__(self, *args, **kwargs):` (*required*)
This is the initialization function. This should be reserved for setting up the platform's configuration. NOTE: this is not where you call APIs, as this code is run every time DM initializes your 'Platform' class (e.g. to pull documentation, etc).
<div class="divider"></div>
##### `def process(self):` (*required*)
This is the function that is ran when the web client requests information from this platform (e.g. when the dashboard is loaded) this is where you call APIs, or whatever you're doing to get data.
<div class="divider"></div>
##### `def on_startup(self):` (*optional*)
This function is run when DM starts up, this is a great place for registering API routes or installing dependencies.

View File

@ -0,0 +1,182 @@
import os
from flask import render_template, Blueprint, redirect, request
from flask_login import current_user
from dashmachine.paths import root_folder, wiki_folder
from dashmachine.docs_system.core_docs import (
settings_doc_dict,
user_settings_doc_dict,
apps_doc_dict,
collections_doc_dict,
custom_card_doc_dict,
data_sources_doc_dicts,
access_groups_doc_dict,
)
from dashmachine.docs_system.utils import (
get_md_from_file,
get_md_from_dict,
get_toc_md_from_dicts,
create_edit_wiki,
)
from dashmachine.moment import create_moment
from dashmachine.main.utils import get_apps_and_tags, get_access_group
from dashmachine.main.models import Wiki, WikiTags
docs_system = Blueprint("docs_system", __name__)
@docs_system.route("/docs_home", methods=["GET"])
def docs_home():
access_group, redirect_url = get_access_group(current_user, page="docs")
apps, tags = get_apps_and_tags(access_group)
if redirect_url:
return redirect(redirect_url)
return render_template(
"docs_system/docs-home.html",
about_html=get_md_from_file(os.path.join(root_folder, "README.md")),
install_html=get_md_from_file("install.md"),
getting_started_html=get_md_from_file("getting-started.md"),
access_group=access_group,
apps=apps,
tags=tags,
)
@docs_system.route("/docs_main_settings", methods=["GET"])
def docs_main_settings():
access_group, redirect_url = get_access_group(current_user, page="docs")
if redirect_url:
return redirect(redirect_url)
apps, tags = get_apps_and_tags(access_group)
return render_template(
"docs_system/docs-main-settings.html",
main_settings_html=get_md_from_dict(settings_doc_dict),
user_settings_html=get_md_from_dict(user_settings_doc_dict),
ag_settings_html=get_md_from_dict(access_groups_doc_dict),
access_group=access_group,
apps=apps,
tags=tags,
)
@docs_system.route("/docs_cards", methods=["GET"])
def docs_cards():
access_group, redirect_url = get_access_group(current_user, page="docs")
if redirect_url:
return redirect(redirect_url)
apps, tags = get_apps_and_tags(access_group)
return render_template(
"docs_system/docs-cards.html",
apps_html=get_md_from_dict(apps_doc_dict),
collections_html=get_md_from_dict(collections_doc_dict),
custom_cards_html=get_md_from_dict(custom_card_doc_dict),
access_group=access_group,
apps=apps,
tags=tags,
)
@docs_system.route("/docs_data_sources", methods=["GET"])
def docs_data_sources():
access_group, redirect_url = get_access_group(current_user, page="docs")
if redirect_url:
return redirect(redirect_url)
apps, tags = get_apps_and_tags(access_group)
doc_dicts = data_sources_doc_dicts(get_all=True)
data_sources_html = get_toc_md_from_dicts(doc_dicts)
for doc in doc_dicts:
data_sources_html += f"\n\n{get_md_from_dict(doc)}"
return render_template(
"docs_system/docs-data-sources.html",
data_sources_main_html=get_md_from_file("data-sources.md"),
creating_platforms_html=get_md_from_file("creating-platforms.md"),
platform_methods_html=get_md_from_file("platform-methods.md"),
data_sources_html=data_sources_html,
access_group=access_group,
apps=apps,
tags=tags,
)
@docs_system.route("/wiki_tags", methods=["GET"])
def wiki_tags():
access_group, redirect_url = get_access_group(current_user, page="wikis")
if redirect_url:
return redirect(redirect_url)
apps, tags = get_apps_and_tags(access_group)
wiki_tags_db = WikiTags.query.all()
return render_template(
"docs_system/wiki-tags.html",
access_group=access_group,
apps=apps,
tags=tags,
wiki_tags=wiki_tags_db,
)
@docs_system.route("/wikis", methods=["GET"])
def wikis():
access_group, redirect_url = get_access_group(current_user, page="wikis")
if redirect_url:
return redirect(redirect_url)
apps, tags = get_apps_and_tags(access_group)
if request.args.get("tag", None):
tag = WikiTags.query.filter_by(id=request.args.get("tag")).first()
wikis_db = tag.wikis
else:
tag = None
wikis_db = Wiki.query.all()
for wiki_db in wikis_db:
wiki_db.updated_moment = create_moment(wiki_db.updated)
return render_template(
"docs_system/wikis.html",
access_group=access_group,
apps=apps,
tags=tags,
wikis=wikis_db,
tag=tag,
)
@docs_system.route("/wiki-<permalink>", methods=["GET"])
def wiki(permalink=None):
access_group, redirect_url = get_access_group(current_user, page="wiki")
if redirect_url:
return redirect(redirect_url)
apps, tags = get_apps_and_tags(access_group)
wiki_db = Wiki.query.filter_by(permalink=permalink).first()
if wiki_db:
wiki_fp = os.path.join(wiki_folder, f"{wiki_db.name}.md")
with open(wiki_fp, "r") as file:
wiki_md = file.read()
wiki_md_html = get_md_from_file(file=wiki_db.name, full_path=wiki_fp)
else:
wiki_md_html = None
if wiki_db.wiki_tags.count() > 0:
wiki_db.tags_str = ",".join([tag.name for tag in wiki_db.wiki_tags])
return render_template(
"docs_system/wiki.html",
access_group=access_group,
wiki=wiki_db,
wiki_md_html=wiki_md_html,
wiki_md=wiki_md,
apps=apps,
tags=tags,
)
@docs_system.route("/save_wiki", methods=["POST"])
def save_wiki():
create_edit_wiki(
permalink=request.form.get("wiki_permalink", None),
permalink_new=request.form.get("wiki_permalink_new", None),
name=request.form.get("wiki_name", None),
author=request.form.get("wiki_author", None),
description=request.form.get("wiki_description", None),
md=request.form.get("config", None),
tags=request.form.get("wiki_tags", None),
)
return "ok"

View File

@ -0,0 +1,168 @@
import os
from shutil import copy2
from secrets import token_hex
from datetime import datetime
from markdown2 import markdown
from configparser import ConfigParser
from flask import render_template_string
from dashmachine import db
from dashmachine.paths import docs_folder, wiki_folder, wiki_config_file, root_folder
from dashmachine.docs_system.core_docs import (
base_md_string,
doc_toc_string,
apps_doc_dict,
custom_card_doc_dict,
collections_doc_dict,
)
from dashmachine.main.models import Wiki, WikiTags
def row2dict(row):
d = {}
for column in row.__table__.columns:
d[column.name] = str(getattr(row, column.name))
return d
def convert_html_to_md(md):
html = markdown(
md,
extras=[
"tables",
"fenced-code-blocks",
"break-on-newline",
"header-ids",
"code-friendly",
],
)
return html
def get_md_from_file(file, full_path=None):
if full_path:
path = full_path
else:
path = os.path.join(docs_folder, file)
with open(path) as readme_file:
md = readme_file.read()
html = convert_html_to_md(md)
return html
def get_md_from_dict(doc_dict):
rendered_md = render_template_string(base_md_string, doc_dict=doc_dict)
html = convert_html_to_md(rendered_md)
return html
def get_toc_md_from_dicts(doc_dicts):
rendered_md = render_template_string(doc_toc_string, doc_dicts=doc_dicts)
html = convert_html_to_md(rendered_md)
return html
def get_card_doc_dict(card_type):
if card_type == "app":
card_doc_dict = apps_doc_dict
if card_type == "collection":
card_doc_dict = collections_doc_dict
if card_type == "custom":
card_doc_dict = custom_card_doc_dict
return card_doc_dict
def create_edit_wiki(
permalink=None,
permalink_new=None,
name="Unnamed Wiki",
author=None,
description=None,
md="",
tags=None,
):
wiki = Wiki.query.filter_by(permalink=permalink).first()
if not wiki:
wiki = Wiki()
if not permalink:
wiki.permalink = token_hex(12)
else:
wiki.permalink = permalink
wiki.created = datetime.now()
editing = False
else:
editing = True
if permalink_new:
wiki.permalink = permalink_new
wiki.name = name
wiki.author = author
wiki.description = description
wiki.md = md
wiki.updated = datetime.now()
if not wiki.created:
wiki.created = datetime.now()
if editing:
db.session.merge(wiki)
else:
db.session.add(wiki)
db.session.commit()
if tags:
for tag_name in tags.split(","):
tag_name = tag_name.strip()
tag = WikiTags.query.filter_by(name=tag_name).first()
if not tag:
tag = WikiTags(name=tag_name)
tag.wikis.append(wiki)
db.session.merge(tag)
db.session.commit()
create_wiki_files(wiki)
def create_wiki_files(wiki):
with open(os.path.join(wiki_folder, f"{wiki.name}.md"), "w") as md_file:
md_file.write(wiki.md)
config = ConfigParser(interpolation=None)
config.read(wiki_config_file)
if wiki.name not in config.sections():
config.add_section(wiki.name)
for key, value in row2dict(wiki).items():
if key not in ["id", "md", "name"]:
config.set(wiki.name, key, value)
config.set(wiki.name, "tags", ",".join([tag.name for tag in wiki.wiki_tags]))
config.write(open(wiki_config_file, "w"))
def build_wiki_from_wiki_folder():
if not os.path.isdir(wiki_folder):
os.mkdir(wiki_folder)
if not os.path.isfile(wiki_config_file):
default_config = os.path.join(root_folder, "default_wiki_config.ini")
new_config = os.path.join(wiki_folder, "wiki_config.ini")
copy2(default_config, new_config)
config = ConfigParser(interpolation=None)
config.read(wiki_config_file)
for section in config.sections():
if section != "WikiSettings":
with open(os.path.join(wiki_folder, f"{section}.md"), "r") as file:
md = file.read()
wiki = config[section]
create_edit_wiki(
name=section,
author=wiki.get("author", None),
description=wiki.get("description", None),
md=md,
tags=wiki.get("tags", None),
permalink=wiki.get("permalink", None),
)

View File

@ -1,4 +1,4 @@
from flask import Blueprint, render_template
from flask import Blueprint, render_template, request
error_pages = Blueprint("error_pages", __name__)

View File

@ -6,6 +6,18 @@ rel_app_data_source = db.Table(
db.Column("app_id", db.Integer, db.ForeignKey("apps.id")),
)
rel_apps_tags = db.Table(
"rel_apps_tags",
db.Column("tag_id", db.Integer, db.ForeignKey("tags.id")),
db.Column("app_id", db.Integer, db.ForeignKey("apps.id")),
)
rel_wiki_wiki_tags = db.Table(
"rel_wiki_wiki_tags",
db.Column("wiki_tag_id", db.Integer, db.ForeignKey("wiki_tags.id")),
db.Column("wiki_id", db.Integer, db.ForeignKey("wiki.id")),
)
class Files(db.Model):
id = db.Column(db.Integer, primary_key=True)
@ -26,8 +38,6 @@ class Apps(db.Model):
description = db.Column(db.String())
open_in = db.Column(db.String())
data_template = db.Column(db.String())
groups = db.Column(db.String())
tags = db.Column(db.String())
type = db.Column(db.String())
urls = db.Column(db.String())
@ -51,14 +61,34 @@ class DataSourcesArgs(db.Model):
data_source_id = db.Column(db.Integer, db.ForeignKey("data_sources.id"))
class Groups(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String())
roles = db.Column(db.String())
class Tags(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String())
icon = db.Column(db.String())
sort_pos = db.Column(db.Integer)
apps = db.relationship(
"Apps", secondary=rel_apps_tags, backref=db.backref("tags", lazy="dynamic"),
)
class WikiTags(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String())
wikis = db.relationship(
"Wiki",
secondary=rel_wiki_wiki_tags,
backref=db.backref("wiki_tags", lazy="dynamic"),
)
class Wiki(db.Model):
id = db.Column(db.Integer, primary_key=True)
permalink = db.Column(db.String())
name = db.Column(db.String())
author = db.Column(db.String())
description = db.Column(db.String())
md = db.Column(db.String())
score = db.Column(db.Integer, default=0)
created = db.Column(db.String())
updated = db.Column(db.String())
url = db.Column(db.String())

View File

@ -0,0 +1,92 @@
import os
from configparser import ConfigParser
from flask import jsonify
from dashmachine.paths import user_data_folder
from dashmachine.main.read_config import read_config
from dashmachine.main.utils import make_dict_list_string, convert_form_boolean
from dashmachine.main.models import DataSources
def modify_config(form, convert_form=True):
if convert_form:
form = dict(form)
config = ConfigParser(interpolation=None)
try:
config.read(os.path.join(user_data_folder, "config.ini"))
except Exception as e:
return {"msg": f"Invalid Config: {e}."}
ini_section = form.get("ini_section")
ini_id = form.get("ini_id")
prev_name = form.get("prev_name", None)
del form["ini_section"]
del form["ini_id"]
del form["prev_name"]
ds = DataSources.query.filter_by(name=ini_id).first()
if ds and ds.platform == ini_section:
prev_name = ds.name
form["name"] = form["variable_name"]
ini_section = "Data Sources"
del form["variable_name"]
if ini_section == "Settings":
prev_name = "Settings"
action_providers = []
tags = []
for key, value in form.items():
if "action_providers-" in key:
action_providers.append((key, value))
if "tags-" in key:
tags.append((key, value))
dict_list_string, ini_variable, form = make_dict_list_string(
action_providers, form
)
form[ini_variable] = dict_list_string
dict_list_string, ini_variable, form = make_dict_list_string(tags, form)
form[ini_variable] = dict_list_string
if ini_section == "Users":
ini_section = form["username"]
del form["username"]
if ini_section == "Collection":
urls = []
for key, value in form.items():
if "urls-" in key:
urls.append((key, value))
dict_list_string, ini_variable, form = make_dict_list_string(urls, form)
form[ini_variable] = dict_list_string
if ini_section in [
"App",
"Custom Card",
"Access Groups",
"Collection",
]:
ini_section = form["name"]
del form["name"]
if ini_section == form.get("platform", None):
ini_section = form["variable_name"]
del form["variable_name"]
if prev_name != "None":
config.remove_section(prev_name)
if ini_section in config.sections():
while ini_section in config.sections():
ini_section = f"{ini_section}(1)"
if ini_section not in config.sections():
config.add_section(ini_section)
# print(f"{ini_section}")
for key, value in form.items():
# print(f"{key} - {value}")
value = convert_form_boolean(value)
config.set(ini_section, key, value)
config.write(open(os.path.join(user_data_folder, "config.ini"), "w"))
msg = read_config()
if msg["msg"] != "success":
read_config(from_backup=True)
return jsonify(data=msg)

View File

@ -1,8 +1,9 @@
import os
import json
import socket
from configparser import ConfigParser
from dashmachine.main.models import Apps, Groups, DataSources, DataSourcesArgs, Tags
from dashmachine.user_system.models import User
from dashmachine.main.models import Apps, DataSources, DataSourcesArgs, Tags
from dashmachine.user_system.models import User, AccessGroups
from dashmachine.user_system.utils import (
hash_and_cache_password,
get_cached_password,
@ -10,9 +11,19 @@ from dashmachine.user_system.utils import (
)
from dashmachine.settings_system.models import Settings
from dashmachine.paths import user_data_folder
from dashmachine.docs_system.core_docs import data_sources_doc_dicts
from dashmachine import db
def host_ip():
try:
host_name = socket.gethostname()
host_ip = socket.gethostbyname(host_name)
return host_ip
except Exception:
print("Unable to get Hostname and IP")
def row2dict(row):
d = {}
for column in row.__table__.columns:
@ -21,75 +32,200 @@ def row2dict(row):
return d
def read_config():
def validate_json_csv(json_csv):
try:
for json_dict in json_csv.replace("},{", "}%,%{").split("%,%"):
json.loads(json_dict)
return None
except Exception as e:
return e
config_restored_msg = (
"<i class='material-icons-outlined' style='font-size: 2rem'>warning</i> <br>"
+ "Invalid config! <br>"
+ "CONFIG RESTORED FROM LAST WORKING STATE. <br>"
+ "PLEASE FIX THE FOLLOWING ERRORS IN YOUR CONFIG: <br>"
)
def read_config(from_backup=False):
# GET CONFIG OBJECT
config = ConfigParser(interpolation=None)
try:
config.read(os.path.join(user_data_folder, "config.ini"))
if from_backup is True:
config.read(os.path.join(user_data_folder, ".config-backup.ini"))
else:
config.read(os.path.join(user_data_folder, "config.ini"))
except Exception as e:
return {"msg": f"Invalid Config: {e}."}
return {"msg": f"{config_restored_msg} Invalid Config: {e}."}
# RESET DATABASE VALUES
for ag in AccessGroups.query.all():
ag.apps = []
db.session.merge(ag)
db.session.commit()
for ds in DataSources.query.all():
ds.apps = []
db.session.merge(ds)
db.session.commit()
for tag in Tags.query.all():
tag.apps = []
db.session.merge(tag)
db.session.commit()
ds_list = DataSources.query.all()
for ds in ds_list:
ds.apps.clear()
DataSources.query.delete()
DataSourcesArgs.query.delete()
Apps.query.delete()
Settings.query.delete()
Groups.query.delete()
AccessGroups.query.delete()
Tags.query.delete()
User.query.delete()
# ADD DEFAULT ACCESS GROUPS
if "admin_only" not in config.sections():
config.add_section("admin_only")
config.set("admin_only", "roles", "admin")
config.set("admin_only", "can_access_home", "True")
config.set("admin_only", "can_access_user_settings", "True")
config.set("admin_only", "can_access_main_settings", "True")
config.set("admin_only", "can_access_card_editor", "True")
config.set("admin_only", "can_access_docs", "True")
config.set("admin_only", "can_access_raw_config", "True")
config.set("admin_only", "can_see_sidenav", "True")
config.set("admin_only", "can_edit_users", "True")
config.set("admin_only", "can_edit_images", "True")
config.write(open(os.path.join(user_data_folder, "config.ini"), "w"))
if "public_users" not in config.sections():
config.add_section("public_users")
config.set("public_users", "roles", "public_user")
config.set("public_users", "can_access_home", "False")
config.set("public_users", "can_access_user_settings", "False")
config.write(open(os.path.join(user_data_folder, "config.ini"), "w"))
for section in config.sections():
for key, value in config[section].items():
if len(value) == 0:
del config[section][key]
# Settings creation
if section == "Settings":
settings = Settings()
settings, error = create_settings(config)
if error:
return error
settings.theme = config["Settings"].get("theme", "light")
error = create_access_groups(config)
if error:
return error
settings.accent = config["Settings"].get("accent", "orange")
error = create_users(config)
if error:
return error
settings.background = config["Settings"].get("background", "None")
error = create_data_sources(config)
if error:
return error
if "roles" in config["Settings"]:
settings.roles = config["Settings"]["roles"]
if "admin" not in settings.roles:
settings.roles += ",admin"
if "user" not in settings.roles:
settings.roles += ",user"
if "public_user" not in settings.roles:
settings.roles += ",public_user"
else:
settings.roles = "admin,user,public_user"
error = create_cards(config)
if error:
return error
settings.home_access_groups = config["Settings"].get(
"home_access_groups", "admin_only"
# APPLY TAG SETTINGS
tags_settings = config["Settings"].get("tags", "None")
if tags_settings and tags_settings != "None":
tags_settings = tags_settings.replace("},{", "}%,%{").split("%,%")
for tag_setting in tags_settings:
tag_json = json.loads(tag_setting)
tag = Tags.query.filter_by(name=tag_json.get("name", None)).first()
if tag:
icon = tag_json.get("icon", None)
if icon:
tag.icon = icon
sort_pos = int(tag_json.get("sort_pos", None))
if icon:
tag.sort_pos = sort_pos + 1
db.session.merge(tag)
db.session.commit()
# CREATE DEFAULT USER IF NEEDED
clean_auth_cache()
if not User.query.first():
user = User()
user.username = "admin"
user.role = "admin"
user.password = ""
db.session.add(user)
db.session.commit()
user.password = hash_and_cache_password("admin", user.id)
db.session.merge(user)
db.session.commit()
return {"msg": "success", "settings": row2dict(settings)}
def create_settings(config):
settings = Settings()
settings.theme = config["Settings"].get("theme", "light")
settings.accent = config["Settings"].get("accent", "orange")
settings.background = config["Settings"].get("background", None)
if settings.background == "none":
settings.background = None
if "roles" in config["Settings"]:
settings.roles = config["Settings"]["roles"]
if "admin" not in settings.roles:
settings.roles += ",admin"
if "user" not in settings.roles:
settings.roles += ",user"
if "public_user" not in settings.roles:
settings.roles += ",public_user"
else:
settings.roles = "admin,user,public_user"
settings.custom_app_title = config["Settings"].get(
"custom_app_title", "DashMachine"
)
settings.tags_expanded = config["Settings"].get("tags_expanded", "True")
settings.tags = config["Settings"].get("tags", None)
if settings.tags:
error = validate_json_csv(settings.tags)
if error:
return (
None,
{
"msg": f"{config_restored_msg} Invalid Json for settings - tags: {error}."
},
)
settings.settings_access_groups = config["Settings"].get(
"settings_access_groups", "admin_only"
)
settings.action_providers = config["Settings"].get(
"action_providers",
'{"name": "Google", "macro": "g", "action": "https://www.google.com/search?q={{ value }}"}',
)
error = validate_json_csv(settings.action_providers)
if error:
return (
None,
{
"msg": f"{config_restored_msg} Invalid Json for settings - action_providers: {error}."
},
)
settings.custom_app_title = config["Settings"].get(
"custom_app_title", "DashMachine"
)
db.session.add(settings)
db.session.commit()
return settings, None
settings.sidebar_default = config["Settings"].get("sidebar_default", "open")
settings.tags_expanded = config["Settings"].get("tags_expanded", "True")
db.session.add(settings)
db.session.commit()
# User creation
elif "role" in config[section]:
def create_users(config):
# LOOP CONFIG SECTIONS
for section in config.sections():
if "role" in config[section] and section != "Settings":
user = User()
user.username = section
user.role = config[section]["role"]
user.sidebar_default = config[section].get("sidebar_default", None)
user.theme = config[section].get("theme", None)
user.accent = config[section].get("accent", None)
user.tags_expanded = config[section].get("tags_expanded", None)
user.background = config[section].get("background", None)
user.tags_expanded = config[section].get("tags_expanded", "False")
user.password = ""
if not User.query.filter_by(role="admin").first() and user.role != "admin":
print(
@ -119,17 +255,45 @@ def read_config():
config.set(section, "password", "")
config.set(section, "confirm_password", "")
config.write(open(os.path.join(user_data_folder, "config.ini"), "w"))
return None
# Groups creation
elif "roles" in config[section]:
group = Groups()
def create_access_groups(config):
# LOOP CONFIG SECTIONS
for section in config.sections():
# CREATE ACCESS GROUPS
if "roles" in config[section] and section != "Settings":
group = AccessGroups()
group.name = section
group.roles = config[section]["roles"]
group.roles = config[section].get("roles", None)
group.can_access_home = config[section].get("can_access_home", "True")
group.can_access_user_settings = config[section].get(
"can_access_user_settings", "True"
)
group.can_access_main_settings = config[section].get(
"can_access_main_settings", "False"
)
group.can_access_card_editor = config[section].get(
"can_access_card_editor", "False"
)
group.can_access_raw_config = config[section].get(
"can_access_raw_config", "False"
)
group.can_access_docs = config[section].get("can_access_docs", "False")
group.can_see_sidenav = config[section].get("can_see_sidenav", "False")
group.can_edit_users = config[section].get("can_edit_users", "False")
group.can_edit_images = config[section].get("can_edit_images", "False")
db.session.add(group)
db.session.commit()
return None
# Data source creation
elif "platform" in config[section]:
def create_data_sources(config):
# LOOP CONFIG SECTIONS
for section in config.sections():
# CREATE DATA SOURCES
if "platform" in config[section] and section != "Settings":
data_source = DataSources()
data_source.name = section
data_source.platform = config[section]["platform"]
@ -143,45 +307,80 @@ def read_config():
arg.data_source = data_source
db.session.add(arg)
db.session.commit()
ds_docs = data_sources_doc_dicts(platform_name=data_source.platform)
for variable_dict in ds_docs[0]["variables"]:
if not DataSourcesArgs.query.filter_by(
key=variable_dict["variable"], data_source_id=data_source.id
).first() and variable_dict["variable"] not in [
"platform",
"[variable_name]",
]:
arg = DataSourcesArgs()
arg.key = variable_dict["variable"]
arg.value = None
arg.data_source = data_source
db.session.add(arg)
db.session.commit()
return None
def create_cards(config):
# LOOP CONFIG SECTIONS
for section in config.sections():
# skip section if..
if "platform" in config[section]:
continue
elif "roles" in config[section]:
continue
elif "role" in config[section]:
continue
elif "role" in config[section]:
continue
elif section == "Settings":
continue
else:
# App creation
# START CREATE APPS
app = Apps()
app.name = section
app.type = config[section].get("type", "app")
app.prefix = config[section].get("prefix", None)
app.prefix = config[section].get("prefix", "https://")
if app.type == "app" and not app.prefix:
return {"msg": f"Invalid Config: {section} does not contain prefix."}
return {
"msg": f"{config_restored_msg} Invalid Config: {section} does not contain prefix."
}
app.url = config[section].get("url", None)
app.url = config[section].get("url", "google.com")
host_list = ["127.0.0.1", "localhost"]
for val in host_list[:]:
if app.url and app.url.startswith(val):
app.url = host_ip() + app.url.lstrip(val)
if app.type == "app" and not app.url:
return {"msg": f"Invalid Config: {section} does not contain url."}
return {
"msg": f"{config_restored_msg} Invalid Config: {section} does not contain url."
}
app.icon = config[section].get("icon", None)
app.icon = config[section].get("icon", "static/images/apps/default.png")
app.sidebar_icon = config[section].get("sidebar_icon", None)
app.sidebar_icon = config[section].get(
"sidebar_icon", "static/images/apps/default.png"
)
app.description = config[section].get("description", None)
app.open_in = config[section].get("open_in", "this_tab")
app.urls = config[section].get("urls", None)
if app.urls:
error = validate_json_csv(app.urls)
if error:
return {
"msg": f"{config_restored_msg} Invalid Json for collection - {app.name} - urls: {error}."
}
if "groups" in config[section]:
for group_name in config[section]["groups"].split(","):
if not Groups.query.filter_by(name=group_name.strip()).first():
return {
"msg": f"Invalid Config: {section} contains at group that is not defined."
}
app.groups = config[section]["groups"]
else:
app.groups = None
# Tags creation
# CREATE TAGS (DURING CREATE APPS)
if "tags" in config[section]:
app.tags = config[section]["tags"]
for tag in app.tags.split(","):
for tag in config[section]["tags"].split(","):
tag = tag.strip()
if not Tags.query.filter_by(name=tag).first():
tag_db = Tags(name=tag)
@ -190,16 +389,23 @@ def read_config():
tag_db.sort_pos = tag_db.id
db.session.merge(tag_db)
db.session.commit()
app.tags.append(tag_db)
else:
tag_db = Tags.query.filter_by(name=tag).first()
app.tags.append(tag_db)
else:
app.tags = "Untagged"
if not Tags.query.filter_by(name="Untagged").first():
tag_db = Tags(name="Untagged")
tag_db = Tags(name="Untagged", sort_pos=1)
db.session.add(tag_db)
db.session.commit()
else:
tag_db = Tags.query.filter_by(name="Untagged").first()
app.tags.append(tag_db)
db.session.add(app)
db.session.commit()
# CHECK IF DATA SOURCE EXISTS (DURING CREATE APPS)
if "data_sources" in config[section]:
for config_ds in config[section]["data_sources"].split(","):
db_ds = DataSources.query.filter_by(name=config_ds.strip()).first()
@ -209,43 +415,27 @@ def read_config():
db.session.commit()
else:
return {
"msg": f"Invalid Config: {section} has a data_source variable that doesn't exist."
"msg": f"{config_restored_msg} Invalid Config: {section} has a data_source variable that doesn't exist."
}
group = Groups.query.filter_by(name="admin_only").first()
if not group:
group = Groups()
group.name = "admin_only"
group.roles = "admin"
db.session.add(group)
db.session.commit()
# RELATE APP TO ACCESS GROUP(S)
if "groups" in config[section]:
for group_name in config[section]["groups"].split(","):
ag = AccessGroups.query.filter_by(name=group_name.strip()).first()
if not ag:
return {
"msg": f"{config_restored_msg} Invalid Config: {section} contains at group that is not defined."
}
ag.apps.append(app)
db.session.merge(ag)
db.session.commit()
tags_settings = config["Settings"].get("tags", None)
if tags_settings:
tags_settings = tags_settings.replace("},{", "}%,%{").split("%,%")
for tag_setting in tags_settings:
tag_json = json.loads(tag_setting)
tag = Tags.query.filter_by(name=tag_json.get("name", None)).first()
if tag:
icon = tag_json.get("icon", None)
if icon:
tag.icon = icon
sort_pos = tag_json.get("sort_pos", None)
if icon:
tag.sort_pos = sort_pos
db.session.merge(tag)
# RELATE APP TO 'admin_only' GROUP
ag = AccessGroups.query.filter_by(name="admin_only").first()
if app not in ag.apps:
ag.apps.append(app)
db.session.merge(ag)
db.session.commit()
clean_auth_cache()
if not User.query.first():
user = User()
user.username = "admin"
user.role = "admin"
user.password = ""
db.session.add(user)
db.session.commit()
user.password = hash_and_cache_password("admin", user.id)
db.session.merge(user)
db.session.commit()
return {"msg": "success", "settings": row2dict(settings)}
# END CREATE APP
return None

View File

@ -1,19 +1,43 @@
import os
import glob
import json
from secrets import token_hex
from htmlmin.main import minify
from configparser import ConfigParser
from flask import render_template, url_for, redirect, request, Blueprint, jsonify
from flask import (
render_template,
url_for,
redirect,
request,
Blueprint,
jsonify,
render_template_string,
)
from flask_login import current_user
from dashmachine.main.models import Files, Apps, DataSources
from dashmachine.main.utils import (
check_groups,
get_data_source,
mark_update_message_read,
row2dict,
get_apps_and_tags,
get_access_group,
backup_working_config,
get_template_apps,
)
from dashmachine.user_system.models import User
from dashmachine.main.modify_config import modify_config
from dashmachine.settings_system.models import Settings
from dashmachine.settings_system.forms import ConfigForm
from dashmachine.settings_system.utils import load_files_html
from dashmachine.docs_system.utils import get_card_doc_dict
from dashmachine.docs_system.core_docs import (
configured_data_sources_doc_dicts,
settings_doc_dict,
user_settings_doc_dict,
access_groups_doc_dict,
data_sources_doc_dicts,
)
from dashmachine.user_system.models import User, AccessGroups
from dashmachine.paths import cache_folder, user_data_folder
from dashmachine.version import version, revision_number
from dashmachine import app, db
@ -41,20 +65,142 @@ def response_minify(response):
@main.route("/")
@main.route("/home", methods=["GET"])
def home():
settings = Settings.query.first()
if not check_groups(settings.home_access_groups, current_user):
return redirect(url_for("error_pages.unauthorized"))
return render_template("main/home.html")
access_group, redirect_url = get_access_group(current_user, page="home")
if redirect_url:
return redirect(redirect_url)
apps, tags = get_apps_and_tags(access_group)
return render_template(
"main/home.html", apps=apps, tags=tags, access_group=access_group, page="home"
)
# ------------------------------------------------------------------------------
# /home modules
# ------------------------------------------------------------------------------
@main.route("/load_apps", methods=["GET"])
def load_apps():
access_group, redirect_url = get_access_group(current_user)
apps, tags = get_apps_and_tags(access_group)
if request.args.get("home", None) == "true":
html = render_template_string(
"""
{% from 'main/cards.html' import HomeCards %}
{{ HomeCards(apps, tags) }}
""",
apps=apps,
tags=tags,
)
elif request.args.get("sidenav", None) == "true":
html = render_template_string(
"""
{% from 'main/cards.html' import SidenavApps %}
{{ SidenavApps(apps, tags) }}
""",
apps=apps,
tags=tags,
)
return html
@main.route("/load_card_editor", methods=["GET"])
def load_card_editor():
access_group, redirect_url = get_access_group(current_user)
apps, tags = get_apps_and_tags(access_group)
data_sources = []
for ds in DataSources.query.all():
data_sources.append({"id": ds.id, "name": ds.name, "platform": ds.platform})
card_editor_html = render_template_string(
"""
{% from 'main/card-editor.html' import CardEditor with context %}
{{ CardEditor() }}
""",
data_sources=data_sources,
apps=apps,
)
return card_editor_html
@main.route("/load_config_editor", methods=["GET"])
def load_config_editor():
config_form = ConfigForm()
with open(os.path.join(user_data_folder, "config.ini"), "r") as config_file:
config_form.config.data = config_file.read()
config_editor_html = render_template_string(
"""
{% from 'main/config-editor.html' import ConfigEditor with context %}
{{ ConfigEditor() }}
""",
config_form=config_form,
)
return config_editor_html
@main.route("/load_settings_editor", methods=["GET"])
def load_settings_editor():
settings_db = Settings.query.first()
files_html = load_files_html()
access_group, redirect_url = get_access_group(current_user)
access_groups = AccessGroups.query.all()
users = User.query.all()
# GUI DICTS
user_dicts = [row2dict(user) for user in User.query.all()]
for user_dict in user_dicts:
user_dict["password"] = None
user_settings_doc_dict["docs_url"] = url_for(
"docs_system.docs_main_settings", _anchor="user-settings"
)
settings_dict = row2dict(settings_db)
settings_doc_dict["docs_url"] = url_for("docs_system.docs_main_settings")
settings_dict["action_providers"] = ["list"] + [
json.loads(tag_json)
for tag_json in settings_dict["action_providers"]
.replace("},{", "}%,%{")
.split("%,%")
]
if settings_dict.get("tags", "None") != "None":
settings_dict["tags"] = ["list"] + [
json.loads(tag_json)
for tag_json in settings_dict["tags"].replace("},{", "}%,%{").split("%,%")
]
else:
settings_dict["tags"] = ["list"] + [{"name": "", "icon": "", "sort_pos": ""}]
settings_editor_html = render_template_string(
"""
{% from 'main/settings-editor.html' import SettingsEditor with context %}
{{ SettingsEditor() }}
""",
files_html=files_html,
version=version,
revision_number=revision_number,
user_dicts=user_dicts,
settings_dict=settings_dict,
settings_doc_dict=settings_doc_dict,
user_settings_doc_dict=user_settings_doc_dict,
access_group=access_group,
access_groups=access_groups,
users=users,
)
backup_working_config()
return settings_editor_html
@main.route("/app_view?<app_id>", methods=["GET"])
def app_view(app_id):
settings = Settings.query.first()
if not check_groups(settings.home_access_groups, current_user):
return redirect(url_for("user_system.login"))
access_group, redirect_url = get_access_group(current_user)
apps, tags = get_apps_and_tags(access_group)
if redirect_url:
return redirect(redirect_url)
app_db = Apps.query.filter_by(id=app_id).first()
return render_template(
"main/app-view.html", url=f"{app_db.prefix}{app_db.url}", title=app_db.name
"main/app-view.html",
url=f"{app_db.prefix}{app_db.url}",
title=app_db.name,
access_group=access_group,
apps=apps,
tags=tags,
)
@ -71,6 +217,226 @@ def update_message_read():
return "ok"
@main.route("/build_action_provider_url", methods=["GET"])
def build_action_provider_url():
url = render_template_string(
request.args.get("action"), value=request.args.get("value")
)
return url
@main.route("/get_card_editor_form", methods=["GET"])
def get_card_editor_form():
app_templates = None
if request.args.get("type", None) == "app":
new_app = Apps(type="app")
card = row2dict(new_app)
app_templates = get_template_apps()
elif request.args.get("type", None) == "collection":
new_app = Apps(type="collection")
card = row2dict(new_app)
card["urls"] = '{"url": "", "icon": "", "name": "", "open_in": ""}'
elif request.args.get("type", None) == "custom":
new_app = Apps(type="custom")
card = row2dict(new_app)
else:
card_db = Apps.query.filter_by(id=request.args.get("app_id")).first()
card = row2dict(card_db)
card["tags"] = ",".join([tag.name for tag in card_db.tags])
card["groups"] = ",".join([group.name for group in card_db.access_groups])
card["data_sources"] = ",".join([ds.name for ds in card_db.data_sources])
if not card.get("tags", None):
card["tags"] = ""
if not card.get("groups", None):
card["groups"] = ""
if not card.get("data_sources", None):
card["data_sources"] = ""
if card["urls"] != "None":
urls = card["urls"]
del card["urls"]
card["urls"] = ["list"] + [
json.loads(url_json)
for url_json in urls.replace("},{", "}%,%{").split("%,%")
]
doc_dict = get_card_doc_dict(card["type"])
if card["type"] == "app":
anchor = "cards-apps"
elif card["type"] == "collection":
anchor = "cards-collection"
elif card["type"] == "custom":
anchor = "cards-custom"
doc_dict["docs_url"] = url_for("docs_system.docs_cards", _anchor=anchor)
form_html = render_template_string(
"""
{% from 'main/ini-form.html' import INIForm %}
{% if card['name'] != 'None' %}
<h5 class="theme-primary-text">Editing {{ card['name'] }}</h5>
{% else %}
<h5 class="theme-primary-text">New {{ doc_dict['name'] }}</h5>
{% endif %}
{{ INIForm(card, doc_dict, location="card-editor", app_templates=app_templates) }}
""",
card=card,
doc_dict=doc_dict,
app_templates=app_templates,
)
return form_html
@main.route("/get_card_editor_ds_form", methods=["GET"])
def get_card_editor_ds_form():
ds_selector = None
if request.args.get("new", None) == "True":
data_source = {}
doc_dict = {}
ds_selector = [ds["name"] for ds in data_sources_doc_dicts(get_all=True)]
elif request.args.get("platform", None):
doc_dict = data_sources_doc_dicts(platform_name=request.args.get("platform"))[0]
for variable in doc_dict["variables"]:
if variable["variable"] == "platform":
variable["disabled"] = "True"
data_source = {
"name": "",
"variable_name": "",
"platform": doc_dict["name"],
}
for arg in doc_dict["variables"]:
if arg["variable"] != "platform":
data_source[arg["variable"]] = ""
else:
ds = DataSources.query.filter_by(id=request.args.get("ds_id")).first()
data_source = {
"id": ds.id,
"name": ds.name,
"variable_name": ds.name,
"platform": ds.platform,
}
for arg in ds.args:
data_source[arg.key] = arg.value
doc_dict = configured_data_sources_doc_dicts(data_source["id"])
doc_dict["docs_url"] = url_for(
"docs_system.docs_data_sources", _anchor=ds.platform
)
form_html = render_template_string(
"""
{% from 'main/ini-form.html' import INIForm %}
{% if not ds_selector %}
{% if data_source['name']|length > 0 %}
<h5 class="theme-primary-text">Editing {{ data_source['name'] }}</h5>
{% else %}
<h5 class="theme-primary-text">New {{ doc_dict['name'] }}</h5>
{% endif %}
{% endif %}
{{ INIForm(ini_dict=data_source, doc_dict=doc_dict, ds_selector=ds_selector) }}
""",
data_source=data_source,
doc_dict=doc_dict,
ds_selector=ds_selector,
)
return form_html
@main.route("/get_settings_editor_ag_form", methods=["GET"])
def get_settings_editor_ag_form():
if request.args.get("new", None) == "True":
ag = AccessGroups()
ag = row2dict(ag)
for key, value in ag.items():
if "can_" in key:
ag[key] = "False"
new = True
else:
ag = AccessGroups.query.filter_by(id=request.args.get("ag_id")).first()
ag = row2dict(ag)
new = False
form_html = render_template_string(
"""
{% from 'main/ini-form.html' import INIForm %}
{% if new %}
<h5 class="theme-primary-text">New Access Group</h5>
{% else %}
<h5 class="theme-primary-text">Editing {{ ag['name'] }}</h5>
{% endif %}
{{ INIForm(ag, doc_dict) }}
""",
ag=ag,
doc_dict=access_groups_doc_dict,
new=new,
)
return form_html
@main.route("/get_settings_editor_user_form", methods=["GET"])
def get_settings_editor_user_form():
if request.args.get("new", None) == "True":
user = row2dict(User())
new = True
else:
user = row2dict(User.query.filter_by(id=request.args.get("user_id")).first())
user["password"] = None
new = False
user["name"] = user["username"]
user_settings_doc_dict["docs_url"] = url_for(
"docs_system.docs_main_settings", _anchor="user-settings"
)
form_html = render_template_string(
"""
{% from 'main/ini-form.html' import INIForm %}
{% if new %}
<h5 class="theme-primary-text">New User</h5>
{% else %}
<h5 class="theme-primary-text">Editing {{ user['username'] }}</h5>
{% endif %}
{{ INIForm(user, user_settings_doc_dict, location="settings-editor") }}
""",
user=user,
user_settings_doc_dict=user_settings_doc_dict,
new=new,
)
return form_html
@main.route("/save_ini_form_to_config", methods=["POST"])
def save_ini_form_to_config():
# for key, value in request.form.items():
# print(f"{key} - {value}")
# return "ok"
return modify_config(request.form)
@main.route("/toggle_theme", methods=["GET"])
def toggle_theme():
user = User.query.filter_by(id=request.args.get("id")).first()
if request.args.get("current_status") == "toggle_off":
theme = "dark"
elif request.args.get("current_status") == "toggle_on":
theme = "light"
form = row2dict(user)
form["ini_section"] = "Users"
form["ini_id"] = ""
form["prev_name"] = user.username
form["password"] = ""
form["confirm_password"] = ""
form["theme"] = theme
del form["id"]
del_keys = []
for k, v in form.items():
if v == "None":
del_keys.append(k)
for k in del_keys:
del form[k]
return modify_config(form=form)
# ------------------------------------------------------------------------------
# TCDROP routes
# ------------------------------------------------------------------------------
@ -113,28 +479,3 @@ def deleteCachedFile():
except FileNotFoundError:
pass
return "success"
# @main.route("/tcdrop/addLocalFile", methods=["GET"])
# def addLocalFile():
# f = request.args.get("file")
# email_cache = request.args.get("email_cache")
# ext = f.split(".")[1]
# random_hex = token_hex(16)
# fn = f"{random_hex}.{ext}"
# if email_cache == "true":
# file = Files.query.filter_by(cache=f).first()
# orig_fn = file.name
# old_path = os.path.join(email_cache_folder, f)
# else:
# old_path = os.path.join(pdf_folder, f)
# orig_fn = f
# path = os.path.join(cache_folder, fn)
# copyfile(old_path, path)
# html = render_template(
# "main/tcdrop-file-row.html", orig_fn=orig_fn, fn=fn, id=random_hex
# )
# file = Files(name=orig_fn, path=path, cache=fn, folder="cache")
# db.session.add(file)
# db.session.commit()
# return jsonify(data={"file": fn, "html": html})

View File

@ -1,16 +1,23 @@
import os
import json
import importlib
from shutil import copyfile
from PIL import Image
from markdown2 import markdown
from configparser import ConfigParser
from flask import url_for
from dashmachine.paths import (
dashmachine_folder,
images_folder,
root_folder,
user_data_folder,
template_apps_folder,
custom_platforms_folder,
platform_folder,
)
from dashmachine.main.models import Groups
from dashmachine.main.models import Tags
from dashmachine.main.read_config import read_config
from dashmachine.user_system.models import AccessGroups
from dashmachine.docs_system.utils import build_wiki_from_wiki_folder
from dashmachine.version import version as dashmachine_version
from dashmachine import db
@ -35,8 +42,6 @@ def dashmachine_init():
db.create_all()
db.session.commit()
user_data_folder = os.path.join(dashmachine_folder, "user_data")
# create the user_data subdirectories, link them to static
user_backgrounds_folder = os.path.join(user_data_folder, "backgrounds")
backgrounds_folder = os.path.join(images_folder, "backgrounds")
@ -58,34 +63,82 @@ def dashmachine_init():
read_config()
# delete broken links in platforms
for file in os.listdir(platform_folder):
path = os.path.join(platform_folder, file)
if os.path.islink(path) and not os.path.exists(os.readlink(path)):
os.unlink(path)
def check_groups(groups, current_user):
if current_user.is_anonymous:
current_user.role = "public_user"
# link platforms in user_data/platforms
if os.path.isdir(custom_platforms_folder):
for file in os.listdir(custom_platforms_folder):
real_path = os.path.join(custom_platforms_folder, file)
link_path = os.path.join(platform_folder, f"custom_{file}")
if not os.path.exists(link_path):
os.symlink(real_path, link_path)
if groups:
groups_list = groups.split(",")
roles_list = []
for group in groups_list:
group = Groups.query.filter_by(name=group.strip()).first()
for group_role in group.roles.split(","):
roles_list.append(group_role.strip())
if current_user.role in roles_list:
return True
else:
return False
# run on_startup platform methods
for platform_file in os.listdir(platform_folder):
name, extension = os.path.splitext(platform_file)
if extension.lower() == ".py" and name not in ["__init__"]:
module = importlib.import_module(f"dashmachine.platform.{name}", ".")
platform = module.Platform()
if getattr(platform, "on_startup", None):
platform.on_startup()
# build wiki
build_wiki_from_wiki_folder()
def get_access_group(user, page=None):
access_groups = []
access_group = None
if user.is_authenticated:
access_group = AccessGroups()
for ag in AccessGroups.query.all():
if user.role in ag.roles:
access_groups.append(ag)
for ag in access_groups:
for app in ag.apps:
access_group.apps.append(app)
for key, value in row2dict(ag).items():
if key.startswith("can_") and value == "True":
setattr(access_group, key, value)
if not access_group:
access_group = AccessGroups.query.filter_by(name="public_users").first()
redirect_url = url_for("error_pages.unauthorized")
if page == "home" and access_group.can_access_home == "False":
pass
elif page == "docs" and access_group.can_access_docs == "False":
pass
else:
if current_user.role == "admin":
return True
else:
return False
redirect_url = None
return access_group, redirect_url
def get_apps_and_tags(access_group):
apps = access_group.apps
tags = Tags.query.order_by(Tags.sort_pos).all()
for app in apps:
if app.urls:
url_list = app.urls.replace("},{", "}%,%{").split("%,%")
app.urls_json = []
for url in url_list:
app.urls_json.append(json.loads(url))
return apps, tags
def get_data_source(data_source):
data_source_args = {}
for arg in data_source.args:
arg = row2dict(arg)
data_source_args[arg.get("key")] = arg.get("value")
if arg["value"] != "None":
data_source_args[arg.get("key")] = arg.get("value")
data_source = row2dict(data_source)
module = importlib.import_module(
f"dashmachine.platform.{data_source['platform']}", "."
@ -103,6 +156,25 @@ def resize_template_app_images():
image.save(fp)
def get_template_apps():
app_templates = []
for file in os.listdir(template_apps_folder):
config = ConfigParser(interpolation=None)
config.read(os.path.join(template_apps_folder, file))
app_templates.append(
{
"name": config.sections()[0],
"prefix": config[config.sections()[0]]["prefix"],
"url": config[config.sections()[0]]["url"],
"icon": config[config.sections()[0]]["icon"],
"sidebar_icon": config[config.sections()[0]]["sidebar_icon"],
"description": config[config.sections()[0]]["description"],
"open_in": config[config.sections()[0]]["open_in"],
}
)
return app_templates
def get_update_message_html():
try:
with open(os.path.join(user_data_folder, ".has_read_update"), "r") as has_read:
@ -133,3 +205,41 @@ def get_update_message_html():
def mark_update_message_read():
with open(os.path.join(user_data_folder, ".has_read_update"), "w") as has_read:
has_read.write(dashmachine_version)
def convert_form_boolean(value):
if value == "on":
return_value = "True"
elif value == "off":
return_value = "False"
else:
return_value = value
return return_value
def make_dict_list_string(tuple_list, form):
dict_list = []
form_ids = []
for subvariable_tuple in tuple_list:
del form[subvariable_tuple[0]]
ini_variable = subvariable_tuple[0].split("-")[0]
form_ids.append(subvariable_tuple[0].split("-")[2])
for form_id in set(form_ids):
subvariable_dict = {}
for subvariable_tuple in tuple_list:
if form_id in subvariable_tuple[0]:
st_val = convert_form_boolean(subvariable_tuple[1])
subvariable_dict[subvariable_tuple[0].split("-")[1]] = st_val
dict_list.append(json.dumps(subvariable_dict))
dict_list_string = ",".join(map(str, dict_list))
return dict_list_string, ini_variable, form
def backup_working_config():
with open(os.path.join(user_data_folder, "config.ini"), "r") as config_file:
with open(
os.path.join(user_data_folder, ".config-backup.ini"), "w"
) as bak_file:
bak_file.write(config_file.read())

78
dashmachine/moment.py Normal file
View File

@ -0,0 +1,78 @@
from dateutil import parser
from datetime import datetime
def create_moment(dt):
# get current time, database object time, get the difference in minutes
now_datetime = datetime.now()
item_datetime = parser.parse(dt)
obj_time = item_datetime.strftime("%-I:%M")
obj_day = item_datetime.strftime("%x")
minutes_diff = (now_datetime - item_datetime).total_seconds() / 60.0
if minutes_diff > 0:
# if the time difference is less than 5 minutes
if minutes_diff < 5.0:
moment = "Just now"
# if the time difference is less than 1 hour
elif minutes_diff < 60.0:
minutes_diff = round(minutes_diff)
moment = f"{minutes_diff} minutes ago"
# if the time difference is less than 1 day
elif minutes_diff < 1440.0:
minutes_diff = round(minutes_diff / 60.0)
if minutes_diff == 1:
hour = "hour"
else:
hour = "hours"
moment = f"{minutes_diff} {hour} ago"
# if the time difference is less than 1 week
elif minutes_diff < 10080.0:
day = item_datetime.strftime("%a")
moment = f"{day} at {obj_time}"
# if the time difference is less than 1 year
elif minutes_diff < 525600.0:
day = item_datetime.strftime("%-m/%-d")
moment = f"{day} at {obj_time}"
# if the time difference is more than 1 year
else:
moment = f"{obj_day} at {obj_time}"
else:
# if the time difference is less than 5 minutes in the future
if minutes_diff > -5.0:
moment = "Now"
# if the time difference is less than 1 hour
elif minutes_diff > -60.0:
minutes_diff = round(abs(minutes_diff))
moment = f"in {abs(minutes_diff)} minutes"
# if the time difference is less than 1 day in the future
elif minutes_diff > -1440.0:
minutes_diff = round(abs(minutes_diff) / 60.0)
if minutes_diff == 1:
hour = "hour"
else:
hour = "hours"
moment = f"in {abs(minutes_diff)} {hour}"
# if the time difference is less than 1 week in the future
elif minutes_diff > -10080.0:
day = item_datetime.strftime("%a")
moment = f"{day} at {obj_time}"
# if the time difference is less than 1 year in the future
elif minutes_diff > -525600.0:
day = item_datetime.strftime("%-m/%-d")
moment = f"{day} at {obj_time}"
# if the time difference is more than 1 year in the future
else:
moment = f"{obj_day} at {obj_time}"
return moment

View File

@ -17,8 +17,16 @@ template_apps_folder = os.path.join(root_folder, "template_apps")
platform_folder = os.path.join(dashmachine_folder, "platform")
docs_folder = os.path.join(dashmachine_folder, "docs_system", "docs")
user_data_folder = os.path.join(dashmachine_folder, "user_data")
custom_platforms_folder = os.path.join(user_data_folder, "platform")
wiki_folder = os.path.join(user_data_folder, "wiki")
wiki_config_file = os.path.join(wiki_folder, "wiki_config.ini")
auth_cache = os.path.join(user_data_folder, "auth_cache")
if not os.path.isdir(auth_cache):

View File

@ -1,67 +1,88 @@
"""
##### curl
Curl an URL and show result
```ini
[variable_name]
platform = curl
resource = https://example.com
value_template = {{value}}
response_type = json
```
> **Returns:** `value_template` as rendered string
| Variable | Required | Description | Options |
|-----------------|----------|-----------------------------------------------------------------|-------------------|
| [variable_name] | Yes | Name for the data source. | [variable_name] |
| platform | Yes | Name of the platform. | curl |
| resource | Yes | Url to curl | url |
| value_template | Yes | Jinja template for how the returned data from api is displayed. | jinja template |
| response_type | No | Response type. Use json if response is a JSON. Default is plain.| plain,json |
> **Working example:**
>```ini
>[test]
>platform = curl
>resource = https://api.myip.com
>value_template = My IP: {{value.ip}}
response_type = json
>
>[MyIp.com]
>prefix = https://
>url = myip.com
>icon = static/images/apps/default.png
>description = Link to myip.com
>open_in = new_tab
>data_sources = test
>```
"""
import requests
from flask import render_template_string
class Platform:
def __init__(self, *args, **kwargs):
# parse the user's options from the config entries
for key, value in kwargs.items():
self.__dict__[key] = value
# set defaults for omitted options
if not hasattr(self, "response_type"):
self.response_type = "plain"
def process(self):
if self.response_type.lower() == "json":
try:
value = requests.get(self.resource).json()
print(value)
except Exception as e:
value = f"{e}"
else:
try:
value = requests.get(self.resource)
except Exception as e:
value = f"{e}"
return render_template_string(self.value_template, value=value)
import requests
from flask import render_template_string
class Platform:
def docs(self):
documentation = {
"name": "curl",
"author": "buoyantotter",
"author_url": "https://github.com/buoyantotter",
"version": 1.0,
"description": "Curl an URL and show result",
"example": """
```ini
[test]
platform = curl
resource = https://api.myip.com
value_template = My IP: {{value.ip}}
response_type = json
[MyIp.com]
prefix = https://
url = myip.com
icon = static/images/apps/default.png
description = Link to myip.com
open_in = new_tab
data_sources = test
```
""",
"returns": "`value_template` as rendered string",
"returns_json_keys": ["value"],
"variables": [
{
"variable": "[variable_name]",
"description": "Name for the data source.",
"default": "",
"options": ".ini header",
},
{
"variable": "platform",
"description": "Name of the platform.",
"default": "curl",
"options": "curl",
},
{
"variable": "resource",
"description": "Url to curl",
"default": "https://example.com",
"options": "url",
},
{
"variable": "value_template",
"description": "Jinja template for how the returned data from api is displayed.",
"default": "{{value}}",
"options": "jinja template",
},
{
"variable": "response_type",
"description": "Response type. Use json if response is a JSON.",
"default": "plain",
"options": "plain,json",
},
],
}
return documentation
def __init__(self, *args, **kwargs):
# parse the user's options from the config entries
for key, value in kwargs.items():
self.__dict__[key] = value
# set defaults for omitted options
if not hasattr(self, "response_type"):
self.response_type = "plain"
def process(self):
if self.response_type.lower() == "json":
try:
value = requests.get(self.resource).json()
print(value)
except Exception as e:
value = f"{e}"
else:
try:
value = requests.get(self.resource)
except Exception as e:
value = f"{e}"
return render_template_string(self.value_template, value=value)

View File

@ -1,49 +1,82 @@
"""
##### deluge
Display information from Deluge web ui.
```ini
[variable_name]
platform = deluge
resource = https://deluge.example.com:8112/json
value_template = {{download_rate|filesizeformat}}/s {{upload_rate|filesizeformat}}/s
password = MySecretPassword
```
> **Returns:** `value_template` as rendered string
| Variable | Required | Description | Options |
|-----------------|----------|-----------------------------------------------------------------|-------------------|
| [variable_name] | Yes | Name for the data source. | [variable_name] |
| platform | Yes | Name of the platform. | rest |
| resource | Yes | Url of your deluge instance + '/json' | url |
| value_template | Yes | Jinja template for how the returned data from api is displayed. | jinja template |
| password | No | Password to use for auth. | string |
> **Working example:**
>```
>[deluge]
>platform = deluge
>resource = https://deluge.example.com:8112/json
>value_template = {{download_rate|filesizeformat}}/s {{upload_rate|filesizeformat}}/s
>password = MySecretPassword
>
>[Deluge]
>prefix = https://
>url = https://deluge.example.com:8112
>icon = static/images/apps/deluge.png
>sidebar_icon = static/images/apps/deluge.png
>description = Deluge is a lightweight, Free Software, cross-platform BitTorrent client
>open_in = iframe
>data_sources = deluge
>```
"""
from flask import render_template_string
import requests
class Platform:
def docs(self):
documentation = {
"name": "deluge",
"author": "Azelphur",
"author_url": "https://github.com/Azelphur",
"version": 1.0,
"description": "Display information from Deluge web ui.",
"returns_json_keys": [
"upload_rate",
"download_rate",
"max_upload",
"max_download",
"upload_protocol_rate",
"download_protocol_rate",
"num_connections",
"max_num_connections",
"dht_nodes",
"free_space",
"has_incoming_connections",
],
"example": """
```ini
[deluge]
platform = deluge
resource = https://deluge.example.com:8112/json
value_template = {{download_rate|filesizeformat}}/s {{upload_rate|filesizeformat}}/s
password = MySecretPassword
[Deluge]
prefix = https://
url = https://deluge.example.com:8112
icon = static/images/apps/deluge.png
sidebar_icon = static/images/apps/deluge.png
description = Deluge is a lightweight, Free Software, cross-platform BitTorrent client
open_in = iframe
data_sources = deluge
```
""",
"returns": "`value_template` as rendered string",
"variables": [
{
"variable": "[variable_name]",
"description": "Name for the data source.",
"default": "",
"options": ".ini header",
},
{
"variable": "platform",
"description": "Name of the platform.",
"default": "deluge",
"options": "deluge",
},
{
"variable": "resource",
"description": "Url of your deluge instance + '/json'",
"default": "https://deluge.example.com:8112/json",
"options": "url",
},
{
"variable": "value_template",
"description": "Jinja template for how the returned data from api is displayed.",
"default": "{{download_rate|filesizeformat}}/s ↑{{upload_rate|filesizeformat}}/s",
"options": "jinja template",
},
{
"variable": "password",
"description": "Password to use for auth.",
"default": "",
"options": "string",
},
],
}
return documentation
def __init__(self, *args, **kwargs):
for key, value in kwargs.items():
self.__dict__[key] = value
@ -54,16 +87,19 @@ class Platform:
if not hasattr(self, "password"):
self.password = ""
def pre_process(self):
self.id = 1
self.session = requests.Session()
self._api_call("auth.login", [self.password])
self.password = None # Discard password, no longer needed.
return self
def _api_call(self, method, params=[]):
json = {"id": self.id, "method": method, "params": params}
return self.session.post(self.resource, json=json)
def process(self):
self = self.pre_process()
r = self._api_call("web.update_ui", ["download_rate", "upload_rate"])
json = r.json()
data = {}

View File

@ -0,0 +1,449 @@
from flask import render_template_string
import requests
import re
class Docker(object):
def __init__(
self,
method,
prefix,
host,
port,
api_version,
card_type,
tls_mode,
tls_ca,
tls_cert,
tls_key,
):
self.endpoint = None
self.method = method
self.prefix = prefix
self.host = host
self.port = port
self.api_version = api_version
self.card_type = card_type
self.tls_mode = tls_mode
self.tls_ca = tls_ca
self.tls_key = tls_key
self.tls_cert = tls_cert
# Initialize results
self.error = None
self.version = "?"
self.max_api_version = "?"
self.name = "?"
self.running = 0
self.paused = 0
self.stopped = 0
self.images = 0
self.driver = "?"
self.cpu = "?"
self.memory = "?"
self.html_template = ""
def check(self):
port = "" if self.port == None else ":" + self.port
if self.method.upper() == "GET":
try:
response = ""
request = requests.get(
self.prefix + self.host + port + "/v999/info",
verify=self.tls_ca,
cert=(self.tls_cert, self.tls_key),
timeout=10,
)
response = request.text
if "text/plain" in request.headers["content-type"]:
self.error = request.text
rawdata = None
elif "application/json" in request.headers["content-type"]:
rawdata = request.json()
else:
error = request
rawdata = None
except Exception as e:
rawdata = None
self.error = f"{e}" + " " + response
self.setHtml()
if rawdata != None:
if "message" in rawdata:
regex = r"\bv?[0-9]+\.[0-9]+(?:\.[0-9]+)?\b"
r = re.search(regex, rawdata["message"])
self.max_api_version = r.group(0)
self.api_version = (
self.api_version
if self.api_version != None
else self.max_api_version
)
self.endpoint = "/v" + self.api_version + "/"
def getStatus(self):
port = "" if self.port == None else ":" + self.port
if self.method.upper() == "GET":
try:
rawdata = requests.get(
self.prefix + self.host + port + self.endpoint + "/info",
verify=self.tls_ca,
cert=(self.tls_cert, self.tls_key),
timeout=10,
).json()
except Exception as e:
rawdata = None
self.error = f"{e}"
self.setHtml()
if rawdata != None:
self.name = rawdata["Name"]
self.containers = rawdata["Containers"]
self.containers_running = rawdata["ContainersRunning"]
self.containers_paused = rawdata["ContainersPaused"]
self.containers_stopped = rawdata["ContainersStopped"]
self.images = rawdata["Images"]
self.warnings = rawdata["Warnings"]
self.driver = rawdata["Driver"]
self.cpu = rawdata["NCPU"]
self.memory = self.formatSize(rawdata["MemTotal"])
if self.card_type == "Custom":
self.setHtml()
def formatSize(self, size):
# 2**10 = 1024
power = 2 ** 10
n = 0
power_labels = {0: "", 1: "KB", 2: "MB", 3: "GB", 4: "TB"}
while size > power:
size /= power
n += 1
return str(round(size, 1)) + " " + power_labels[n]
def refresh(self):
self.check()
if self.error == None:
self.error = ""
self.getStatus()
def setHtml(self):
if self.error != None and self.error != "":
self.html_template = """
<div class="row">
<div class="col s6">
<span class="mt-0 mb-0 theme-primary-text font-weight-700" style="font-size: 36px"><i class="material-icons md-18 theme-failure-text" title="Error">error</i></h3>
</div>
<div class="col s6 right-align">
<img height="48px" src="static/images/apps/docker.png" alt="Docker">
</div>
</div>
<div class="row">
<h6 class="font-weight-900 center theme-muted-text">Error</h6>
</div>
<div class="row center-align">
<i class="material-icons-outlined">keyboard_arrow_down</i>
</div>
<div class="row center-align">
<div class="col s12">
<div class="collection theme-muted-text">
<div class="collection-item">{{ error }}</div>
</div>
</div>
</div>
"""
else:
if self.tls_mode == None:
img_tls = """
<i class="material-icons md-18 theme-warning-text" title="TLS disabled">lock_open</i>
"""
else:
img_tls = """
<i class="material-icons md-18 theme-success-text" title="TLS enabled">lock</i>
"""
if len(self.warnings) > 0:
img_warnings = """
<i class="material-icons md-18 theme-warning-text" title="{{warnings}}">warning</i>
"""
else:
img_warnings = """
<i class="material-icons md-18 theme-muted2-text" title="No warnings">warning</i>
"""
self.html_template = (
"""
<div class="row">
<div class="col s6">
<span class="mt-0 mb-0 theme-primary-text font-weight-700" style="font-size: 36px">"""
+ img_tls
+ img_warnings
+ """</h3>
</div>
<div class="col s6 right-align">
<img height="48px" src="static/images/apps/docker.png" alt="Docker">
</div>
</div>
<div class="row">
<h6 class="font-weight-900 center theme-muted-text">{{name}}</h6>
</div>
<div class="row center-align">
<i class="material-icons-outlined">keyboard_arrow_down</i>
</div>
<div class="row center-align">
<div class="col s12">
<div class="collection theme-muted-text">
<div class="collection-item"><span class="font-weight-900">Containers: </span>{{ containers }}</div>
<div class="collection-item"><span class="font-weight-900">Running: </span>{{ containers_running }}</div>
<div class="collection-item"><span class="font-weight-900">Paused: </span>{{ containers_paused }}</div>
<div class="collection-item"><span class="font-weight-900">Stopped: </span>{{ containers_stopped }}</div>
<div class="collection-item"><span class="font-weight-900">Images: </span>{{ images }}</div>
<div class="collection-item"><span class="font-weight-900">Driver: </span>{{ driver }}</div>
<div class="collection-item"><span class="font-weight-900">CPU: </span>{{ cpu }}</div>
<div class="collection-item"><span class="font-weight-900">Memory: </span>{{ memory }}</div>
</div>
</div>
</div>
"""
)
def getHtml(self):
return self.html_template
class Platform:
def docs(self):
documentation = {
"name": "docker",
"author": "Thlb",
"author_url": "https://github.com/Thlb",
"version": 1.0,
"description": "Display information from Docker API. Informations can be displayed on a custom card or on an app card (e.g. Portainer App)",
"returns": "`value_template` as rendered string",
"returns_json_keys": [
"version",
"max_api_version",
"name",
"containers",
"containers_running",
"containers_paused",
"containers_stopped",
"images",
"driver",
"cpu",
"memory",
"warnings",
"error (for debug)",
],
"example": """
```ini
# Working example (using un-encrypted connection, on Portainer card)
[docker-endpoint-1]
platform = docker
prefix = http://
host = 192.168.0.110
port = 2375
value_template = {{error}}<p style="text-align:right;text-transform:uppercase;font-size:14px;font-family: monospace;">{{name}}<br /><i style="position: relative; top: .2rem" class="material-icons md-18 theme-success-text" title="Running">fiber_manual_record</i>{{containers_running}}<i style="position: relative; top: .2rem" class="material-icons md-18 theme-warning-text" title="Paused">fiber_manual_record</i>{{containers_paused}}<i style="position: relative; top: .2rem" class="material-icons md-18 theme-failure-text" title="Stopped">fiber_manual_record</i>{{containers_stopped}}</p>
[Portainer]
prefix = http://
url = 192.168.0.110:2375
icon = static/images/apps/portainer.png
sidebar_icon = static/images/apps/portainer.png
description = Making Docker management easy
open_in = this_tab
data_sources = docker-endpoint-1
# Working example (using encrypted connection, on Portainer card)
```ini
[docker-endpoint-2]
platform = docker
prefix = https://
host = 192.168.0.110
port = 2376
tls_mode = Both
tls_ca = /path/to/ca_file
tls_cert = /path/to/cert_file
tls_key = /path/to/key_file
value_template = {{error}}<p style="text-align:right;text-transform:uppercase;font-size:14px;font-family: monospace;">{{name}}<br /><i style="position: relative; top: .2rem" class="material-icons md-18 theme-success-text" title="Running">fiber_manual_record</i>{{containers_running}}<i style="position: relative; top: .2rem" class="material-icons md-18 theme-warning-text" title="Paused">fiber_manual_record</i>{{containers_paused}}<i style="position: relative; top: .2rem" class="material-icons md-18 theme-failure-text" title="Stopped">fiber_manual_record</i>{{containers_stopped}}</p>
[Portainer]
prefix = http://
url = 192.168.0.110:2375
icon = static/images/apps/portainer.png
sidebar_icon = static/images/apps/portainer.png
description = Making Docker management easy
open_in = this_tab
data_sources = docker-endpoint-2
```
# Working example (using un-encrypted connection, on custom Docker card)
```ini
[docker-endpoint-3]
platform = docker
prefix = http://
host = 192.168.0.110
port = 2375
card_type = Custom
[Docker]
type = custom
data_sources = docker-endpoint-3
```
""",
"variables": [
{
"variable": "[variable_name]",
"description": "Name for the data source.",
"default": "None, entry is required",
"options": ".ini header",
},
{
"variable": "platform",
"description": "Name of the platform.",
"default": "docker",
"options": "docker",
},
{
"variable": "prefix",
"description": "The prefix for the app's url.",
"default": "",
"options": "web prefix, e.g. http:// or https://",
},
{
"variable": "host",
"description": "Docker Host",
"default": "",
"options": "url,ip",
},
{
"variable": "port",
"description": "Docker Port",
"default": "",
"options": "port",
},
{
"variable": "api_version",
"description": "API version, by default platform will try to find latest version",
"default": "",
"options": "1.40",
},
{
"variable": "tls_mode",
"description": "TLS verification mode",
"default": "None",
"options": "Server, Client, Both, None",
},
{
"variable": "tls_ca",
"description": "Requiered for tls_mode=Both or tls_mode=Server",
"default": "None",
"options": "/path/to/ca, None",
},
{
"variable": "tls_cert",
"description": "Requierd for tls_mode=Both or tls_mode=Client",
"default": "None",
"options": "/path/to/cert, None",
},
{
"variable": "tls_key",
"description": "Requierd for tls_mode=Both or tls_mode=Client",
"default": "None",
"options": "/path/to/key, None",
},
{
"variable": "card_type",
"description": "Set to Custom if you want to display informations in a custom card",
"default": "App",
"options": "Custom, App",
},
{
"variable": "value_template",
"description": "Jinja template for how the returned data from API is displayed.",
"default": "",
"options": "jinja template",
},
],
}
return documentation
def __init__(self, *args, **kwargs):
# parse the user's options from the config entries
for key, value in kwargs.items():
self.__dict__[key] = value
# set defaults for omitted options
if not hasattr(self, "method"):
self.method = "GET"
if not hasattr(self, "prefix"):
self.prefix = "http://"
if not hasattr(self, "host"):
self.host = None
if not hasattr(self, "port"):
self.port = 2375
if not hasattr(self, "api_version"):
self.api_version = None
if not hasattr(self, "card_type"):
self.card_type = "App"
if not hasattr(self, "tls_ca"):
self.tls_ca = None
if not hasattr(self, "tls_cert"):
self.tls_cert = None
if not hasattr(self, "tls_key"):
self.tls_key = None
# Without TLS
if not hasattr(self, "tls_mode"):
self.tls_mode = None
self.tls_ca = None
self.tls_cert = None
self.tls_key = None
else:
if self.tls_mode == "Both":
if self.tls_ca == None or self.tls_cert == None or self.tls_key == None:
return "tls_mode set to Both, and missing tls_ca/tls_cert/tls_key"
elif self.tls_mode == "Client":
self.tls_ca = False
elif self.tls_mode == "Server":
self.tls_cert = ""
self.tls_key = ""
elif self.tls_mode == "None":
self.tls_ca = None
self.tls_cert = None
self.tls_key = None
self.docker = Docker(
self.method,
self.prefix,
self.host,
self.port,
self.api_version,
self.card_type,
self.tls_mode,
self.tls_ca,
self.tls_cert,
self.tls_key,
)
def process(self):
if self.host == None:
return "host missing"
# TLS check
if self.tls_mode == "Both":
if self.tls_ca == None or self.tls_cert == None or self.tls_key == None:
return "tls_mode set to Both, and missing tls_ca/tls_cert/tls_key"
elif self.tls_mode == "Client":
if self.tls_cert == None or self.tls_key == None:
return "tls_mode set to Client, and missing tls_cert/tls_key"
elif self.tls_mode == "Server":
if self.tls_ca == None:
return "tls_mode set to Server, and missing tls_ca"
else:
if self.tls_mode != None:
return "Invalid tls_mode : " + self.tls_mode
self.docker.refresh()
if self.card_type == "Custom":
return render_template_string(self.docker.getHtml(), **self.docker.__dict__)
else:
return render_template_string(self.value_template, **self.docker.__dict__)

View File

@ -0,0 +1,238 @@
from flask import render_template_string
import requests
class Healthchecks(object):
def __init__(self, method, prefix, host, port, api_key, project, verify):
self.endpoint = "/api/v1/checks/"
self.method = method
self.prefix = prefix
self.host = host
self.port = port
self.api_key = api_key
self.project = project
self.verify = verify
# Initialize results
self.error = None
self.status = ""
self.count_checks = 0
self.count_up = 0
self.count_down = 0
self.count_grace = 0
self.count_paused = 0
def check(self):
verify = (
False
if str(self.verify).lower() == "false"
or str(self.prefix).lower() == "http://"
else True
)
headers = {"X-Api-Key": self.api_key}
port = "" if self.port == None else ":" + self.port
if self.method.upper() == "GET":
try:
rawdata = requests.get(
self.prefix + self.host + port + self.endpoint,
headers=headers,
verify=verify,
timeout=10,
).json()
except Exception as e:
rawdata = None
self.error = f"{e}"
if rawdata != None:
if "error" in rawdata:
self.error = rawdata["error"]
def getChecks(self):
verify = (
False
if str(self.verify).lower() == "false"
or str(self.prefix).lower() == "http://"
else True
)
headers = {"X-Api-Key": self.api_key}
port = "" if self.port == None else ":" + self.port
if self.method.upper() == "GET":
try:
rawdata = requests.get(
self.prefix + self.host + port + self.endpoint,
headers=headers,
verify=verify,
timeout=10,
).json()
except Exception as e:
rawdata = None
self.error = f"{e}"
if rawdata != None:
for check in rawdata["checks"]:
self.count_checks += 1
if check["status"] == "up":
self.count_up += 1
if check["status"] == "down":
self.count_down += 1
if check["status"] == "grace":
self.count_grace += 1
if check["status"] == "paused":
self.count_paused += 1
if self.count_down > 0:
self.status = "down"
if self.count_down == 0 and self.count_grace > 0:
self.status = "grace"
if self.count_down == 0 and self.count_grace == 0:
self.status = "up"
def refresh(self):
self.check()
if self.error == None:
self.error = ""
self.getChecks()
class Platform:
def docs(self):
documentation = {
"name": "healthchecks",
"author": "Thlb",
"author_url": "https://github.com/Thlb",
"version": 1.0,
"description": "Display information from Healthchecks API",
"returns": "`value_template` as rendered string",
"returns_json_keys": [
"status",
"count_checks",
"count_up",
"count_down",
"count_grace",
"count_paused",
"error (for debug)",
],
"example": """
```ini
[healthchecks-data]
platform = healthchecks
prefix = http://
host = 192.168.0.110
port = 8080
api_key = {{ API Key }}
project = {{ Project name }}
verify = False
value_template = {{error}}<p style="text-align:right;text-transform:uppercase;font-size:14px;font-family: monospace;"><i style="position: relative; top: .2rem" class="material-icons md-18 theme-success-text" title="Up">fiber_manual_record</i>{{count_up}}<i style="position: relative; top: .2rem" class="material-icons md-18 theme-warning-text" title="Grace">fiber_manual_record</i>{{count_grace}}<i style="position: relative; top: .2rem" class="material-icons md-18 theme-failure-text" title="Down">fiber_manual_record</i>{{count_down}}</p>
[Healthchecks]
prefix = http://
url = 192.168.0.110
icon = static/images/apps/healthchecks.png
description = Healthchecks is a watchdog for your cron jobs. It's a web server that listens for pings from your cron jobs, plus a web interface.
open_in = this_tab
data_sources = healthchecks-data
```
""",
"variables": [
{
"variable": "[variable_name]",
"description": "Name for the data source.",
"default": "None, entry is required",
"options": ".ini header",
},
{
"variable": "platform",
"description": "Name of the platform.",
"default": "healthchecks",
"options": "healthchecks",
},
{
"variable": "prefix",
"description": "The prefix for the app's url.",
"default": "",
"options": "web prefix, e.g. http:// or https://",
},
{
"variable": "host",
"description": "Healthchecks Host",
"default": "",
"options": "url,ip",
},
{
"variable": "port",
"description": "Healthchecks Port",
"default": "",
"options": "port",
},
{
"variable": "api_key",
"description": "ApiKey",
"default": "",
"options": "api key",
},
{
"variable": "project",
"description": "Healthchecks project name",
"default": "",
"options": "project name",
},
{
"variable": "verify",
"description": "Turn TLS verification on or off, default is true",
"default": "",
"options": "true,false",
},
{
"variable": "value_template",
"description": "Jinja template for how the returned data from API is displayed.",
"default": "",
"options": "jinja template",
},
],
}
return documentation
def __init__(self, *args, **kwargs):
# parse the user's options from the config entries
for key, value in kwargs.items():
self.__dict__[key] = value
# set defaults for omitted options
if not hasattr(self, "method"):
self.method = "GET"
if not hasattr(self, "prefix"):
self.prefix = "http://"
if not hasattr(self, "host"):
self.host = None
if not hasattr(self, "port"):
self.port = None
if not hasattr(self, "api_key"):
self.api_key = None
if not hasattr(self, "project"):
self.project = None
if not hasattr(self, "verify"):
self.verify = True
self.healthchecks = Healthchecks(
self.method,
self.prefix,
self.host,
self.port,
self.api_key,
self.project,
self.verify,
)
def process(self):
if self.api_key == None:
return "api_key missing"
if self.host == None:
return "host missing"
self.healthchecks.refresh()
value_template = render_template_string(
self.value_template, **self.healthchecks.__dict__
)
return value_template

View File

@ -1,54 +1,90 @@
"""
##### http_status
Make a http call on a given URL and display if the service is online.
```ini
[variable_name]
platform = http_status
resource = https://your-website.com/api
method = get
authentication = basic
username = my_username
password = my_password
headers = {"Content-Type": "application/json"}
return_codes = 2xx,3xx
```
> **Returns:** a right-aligned colored bullet point on the app card.
| Variable | Required | Description | Options |
|-----------------|----------|-----------------------------------------------------------------|-------------------|
| [variable_name] | Yes | Name for the data source. | [variable_name] |
| platform | Yes | Name of the platform. | rest |
| resource | Yes | Url of rest api resource. | url |
| method | No | Method for the api call, default is GET | GET,HEAD,OPTIONS,TRACE|
| authentication | No | Authentication for the api call, default is None | None,basic,digest |
| username | No | Username to use for auth. | string |
| password | No | Password to use for auth. | string |
| headers | No | Request headers | json |
| return_codes | No | Acceptable http status codes, x is handled as wildcard | string |
> **Working example:**
>```ini
>[http_status_test]
>platform = http_status
>resource = https://google.com
>return_codes = 2xx,3xx
>
>[Google]
>prefix = https://
>url = google.com
>icon = static/images/apps/default.png
>open_in = this_tab
>data_sources = http_status_test
>```
"""
from requests import Request, Session
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
class Platform:
def docs(self):
documentation = {
"name": "http_status",
"author": "franznemeth",
"author_url": "https://github.com/franznemeth",
"version": 1.0,
"description": "Make a http call on a given URL and display if the service is online.",
"example": """
```ini
[http_status_test]
platform = http_status
resource = https://google.com
return_codes = 2xx,3xx
[Google]
prefix = https://
url = google.com
icon = static/images/apps/default.png
open_in = this_tab
data_sources = http_status_test
```
""",
"returns": "a right-aligned colored bullet point on the app card.",
"variables": [
{
"variable": "[variable_name]",
"description": "Name for the data source.",
"default": "",
"options": ".ini header",
},
{
"variable": "platform",
"description": "Name of the platform.",
"default": "http_status",
"options": "http_status",
},
{
"variable": "resource",
"description": "Url of rest api resource.",
"default": "https://google.com",
"options": "url",
},
{
"variable": "method",
"description": "Method for the api call",
"default": "GET",
"options": "GET,HEAD,OPTIONS,TRACE",
},
{
"variable": "authentication",
"description": "Authentication for the api call",
"default": "",
"options": "None,basic,digest",
},
{
"variable": "username",
"description": "Username to use for auth.",
"default": "",
"options": "string",
},
{
"variable": "password",
"description": "Password to use for auth.",
"default": "",
"options": "string",
},
{
"variable": "headers",
"description": "Request headers",
"default": "",
"options": "json",
},
{
"variable": "return_codes",
"description": "Acceptable http status codes, x is handled as wildcard",
"default": "2xx,3xx",
"options": "string",
},
],
}
return documentation
def __init__(self, *args, **kwargs):
# parse the user's options from the config entries
for key, value in kwargs.items():
@ -65,7 +101,7 @@ class Platform:
self.return_codes = "2xx,3xx"
if not hasattr(self, "ssl_ignore"):
self.ssl_ignore = "No"
def process(self):
# Check if method is within allowed methods for http_status
if self.method.upper() not in ["GET", "HEAD", "OPTIONS", "TRACE"]:
@ -87,7 +123,7 @@ class Platform:
)
prepped = req.prepare()
if self.ssl_ignore == "yes":
resp = s.send(prepped,verify=False)
resp = s.send(prepped, verify=False)
else:
resp = s.send(prepped)
resp = s.send(prepped)

View File

@ -0,0 +1,330 @@
from flask import render_template_string
import requests
class Lidarr(object):
def __init__(self, method, prefix, host, port, api_key, api_version, verify):
self.api_version = api_version
self.endpoint = "/api/" + self.api_version
self.method = method
self.prefix = prefix
self.host = host
self.port = port
self.api_key = api_key
self.verify = verify
# Initialize results
self.error = None
self.version = "?"
self.wanted_missing = 0
self.wanted_cutoff = 0
self.queue = 0
self.diskspace = [
{"path": "", "total": "", "free": "", "used": ""},
{"path": "", "total": "", "free": "", "used": ""},
]
def check(self):
verify = (
False
if str(self.verify).lower() == "false"
or str(self.prefix).lower() == "http://"
else True
)
headers = {"X-Api-Key": self.api_key}
port = "" if self.port == None else ":" + self.port
if self.method.upper() == "GET":
try:
rawdata = requests.get(
self.prefix + self.host + port + self.endpoint + "/system/status",
headers=headers,
verify=verify,
timeout=10,
).json()
except Exception as e:
rawdata = None
self.error = f"{e}"
if rawdata != None:
if "error" in rawdata:
self.error = rawdata["error"]
def getVersion(self):
verify = (
False
if str(self.verify).lower() == "false"
or str(self.prefix).lower() == "http://"
else True
)
headers = {"X-Api-Key": self.api_key}
port = "" if self.port == None else ":" + self.port
if self.method.upper() == "GET":
try:
rawdata = requests.get(
self.prefix + self.host + port + self.endpoint + "/system/status",
headers=headers,
verify=verify,
timeout=10,
).json()
except Exception as e:
rawdata = None
self.error = f"{e}"
if rawdata != None:
self.version = rawdata["version"]
def getWanted(self, wType):
verify = (
False
if str(self.verify).lower() == "false"
or str(self.prefix).lower() == "http://"
else True
)
headers = {"X-Api-Key": self.api_key}
port = "" if self.port == None else ":" + self.port
if self.method.upper() == "GET":
try:
rawdata = requests.get(
self.prefix
+ self.host
+ port
+ self.endpoint
+ "/wanted/"
+ wType
+ "/",
headers=headers,
verify=verify,
timeout=10,
).json()
except Exception as e:
rawdata = None
self.error = f"{e}"
if rawdata != None:
if wType == "missing":
self.wanted_missing = rawdata["totalRecords"]
else:
self.wanted_cutoff = rawdata["totalRecords"]
def getQueue(self):
verify = (
False
if str(self.verify).lower() == "false"
or str(self.prefix).lower() == "http://"
else True
)
headers = {"X-Api-Key": self.api_key}
port = "" if self.port == None else ":" + self.port
if self.method.upper() == "GET":
try:
rawdata = requests.get(
self.prefix + self.host + port + self.endpoint + "/queue",
headers=headers,
verify=verify,
timeout=10,
).json()
except Exception as e:
rawdata = None
self.error = f"{e}"
if rawdata != None:
self.queue = rawdata["totalRecords"]
def getDiskspace(self):
verify = (
False
if str(self.verify).lower() == "false"
or str(self.prefix).lower() == "http://"
else True
)
headers = {"X-Api-Key": self.api_key}
port = "" if self.port == None else ":" + self.port
if self.method.upper() == "GET":
try:
rawdata = requests.get(
self.prefix + self.host + port + self.endpoint + "/diskspace",
headers=headers,
verify=verify,
timeout=10,
).json()
except Exception as e:
rawdata = None
self.error = f"{e}"
if rawdata != None:
self.diskspace = rawdata
for item in self.diskspace:
item["used"] = self.formatSize(item["totalSpace"] - item["freeSpace"])
item["total"] = self.formatSize(item["totalSpace"])
item["free"] = self.formatSize(item["freeSpace"])
item.pop("totalSpace", None)
item.pop("freeSpace", None)
def formatSize(self, size):
# 2**10 = 1024
power = 2 ** 10
n = 0
power_labels = {0: "", 1: "KB", 2: "MB", 3: "GB", 4: "TB"}
while size > power:
size /= power
n += 1
return str(round(size, 1)) + " " + power_labels[n]
def refresh(self):
self.check()
if self.error == None:
self.error = ""
self.getVersion()
self.getWanted("missing")
self.getWanted("cutoff")
self.getQueue()
self.getDiskspace()
class Platform:
def docs(self):
documentation = {
"name": "lidarr",
"author": "Thlb",
"author_url": "https://github.com/Thlb",
"version": 1.0,
"description": "Display information from Lidarr API",
"returns": "`value_template` as rendered string",
"returns_json_keys": [
"version",
"wanted_missing",
"wanted_cutoff",
"queue",
"diskspace[x]['path']",
"diskspace[x]['total']",
"diskspace[x]['used']",
"diskspace[x]['free']",
"error (for debug)",
],
"example": """
```ini
[lidarr-data]
platform = lidarr
prefix = http://
host = 192.168.0.110
port = 8686
api_key = {{ API Key }}
verify = False
value_template = {{error}}Missing : {{wanted_missing}}<br />Queue : {{queue}} <br />Free ({{diskspace[0]['path']}}) : {{diskspace[0]['free']}}
[Lidarr]
prefix = http://
url = 192.168.0.110:8686
icon = static/images/apps/lidarr.png
sidebar_icon = static/images/apps/lidarr.png
description = Looks and smells like Sonarr but made for music
open_in = this_tab
data_sources = lidarr-data
```
""",
"variables": [
{
"variable": "[variable_name]",
"description": "Name for the data source.",
"default": "None, entry is required",
"options": ".ini header",
},
{
"variable": "platform",
"description": "Name of the platform.",
"default": "lidarr",
"options": "lidarr",
},
{
"variable": "prefix",
"description": "The prefix for the app's url.",
"default": "",
"options": "web prefix, e.g. http:// or https://",
},
{
"variable": "host",
"description": "Lidarr Host",
"default": "",
"options": "url,ip",
},
{
"variable": "port",
"description": "Lidarr Port",
"default": "",
"options": "port",
},
{
"variable": "api_key",
"description": "ApiKey",
"default": "",
"options": "api key",
},
{
"variable": "api_version",
"description": "API version",
"default": "v1",
"options": "v1",
},
{
"variable": "verify",
"description": "Turn TLS verification on or off, default is true",
"default": "",
"options": "true,false",
},
{
"variable": "value_template",
"description": "Jinja template for how the returned data from API is displayed.",
"default": "",
"options": "jinja template",
},
],
}
return documentation
def __init__(self, *args, **kwargs):
# parse the user's options from the config entries
for key, value in kwargs.items():
self.__dict__[key] = value
# set defaults for omitted options
if not hasattr(self, "method"):
self.method = "GET"
if not hasattr(self, "prefix"):
self.prefix = "http://"
if not hasattr(self, "host"):
self.host = None
if not hasattr(self, "port"):
self.port = None
if not hasattr(self, "api_key"):
self.api_key = None
if not hasattr(self, "api_version"):
self.api_version = "v1"
if not hasattr(self, "verify"):
self.verify = True
self.lidarr = Lidarr(
self.method,
self.prefix,
self.host,
self.port,
self.api_key,
self.api_version,
self.verify,
)
def process(self):
if self.api_key == None:
return "api_key missing"
if self.host == None:
return "host missing"
self.lidarr.refresh()
value_template = render_template_string(
self.value_template, **self.lidarr.__dict__
)
return value_template

View File

@ -1,58 +1,3 @@
"""
##### PiHole
Display information from the PiHole API
```ini
[variable_name]
platform = pihole
host = 192.168.1.101
password = {{ PiHole password }}
value_template = {{ value_template }}
```
> **Returns:** `value_template` as rendered string
| Variable | Required | Description | Options |
|-----------------|----------|-----------------------------------------------------------------|-------------------|
| [variable_name] | Yes | Name for the data source. | [variable_name] |
| platform | Yes | Name of the platform. | pihole |
| host | Yes | Host of the PiHole | host |
| password | Yes | Password for the PiHole | password |
| value_template | Yes | Jinja template for how the returned data from API is displayed. | jinja template |
<br />
###### **Available fields for value_template**
* domain_count
* queries
* blocked
* ads_percentage
* unique_domains
* forwarded
* cached
* total_clients
* unique_clients
* total_queries
* gravity_last_updated
> **Working example:**
>```ini
> [pihole-data]
> platform = pihole
> host = 192.168.1.101
> password = password123
> value_template = Ads Blocked Today: {{ blocked }}<br>Status: {{ status }}<br>Queries today: {{ queries }}
>
> [PiHole]
> prefix = http://
> url = 192.168.1.101
> icon = static/images/apps/pihole.png
> description = A black hole for Internet advertisements
> open_in = new_tab
> data_sources = pihole-data
>```
"""
from flask import render_template_string
@ -242,12 +187,85 @@ class PiHole(object):
class Platform:
def docs(self):
documentation = {
"name": "pihole",
"author": "Nixellion",
"author_url": "https://github.com/Nixellion",
"version": 1.0,
"description": "Display information from the PiHole API",
"example": """
```ini
[pihole-data]
platform = pihole
host = 192.168.x.x
password = password123
value_template = Ads Blocked Today: {{ blocked }}<br>Status: {{ status }}<br>Queries today: {{ queries }}
[PiHole]
prefix = http://
url = 192.168.x.x
icon = static/images/apps/pihole.png
description = A black hole for Internet advertisements
open_in = new_tab
data_sources = pihole-data
```
""",
"returns": "`value_template` as rendered string",
"returns_json_keys": [
"domain_count",
"queries",
"blocked",
"ads_percentage",
"unique_domains",
"forwarded",
"cached",
"total_clients",
"unique_clients",
"total_queries",
"gravity_last_updated",
],
"variables": [
{
"variable": "[variable_name]",
"description": "Name for the data source.",
"default": "",
"options": ".ini header",
},
{
"variable": "platform",
"description": "Name of the platform.",
"default": "pihole",
"options": "pihole",
},
{
"variable": "host",
"description": "Host of the PiHole",
"default": "192.168.x.x",
"options": "host",
},
{
"variable": "password",
"description": "Password for the PiHole ",
"default": "password123",
"options": "password",
},
{
"variable": "value_template",
"description": "Jinja template for how the returned data from API is displayed.",
"default": "Ads Blocked Today: {{ blocked }}<br>Status: {{ status }}<br>Queries today: {{ queries }}",
"options": "jinja template",
},
],
}
return documentation
def __init__(self, *args, **kwargs):
# parse the user's options from the config entries
for key, value in kwargs.items():
self.__dict__[key] = value
self.pihole = PiHole(self.host)
# self.pihole = PiHole(self.host)
def process(self):
self.pihole.refresh()

View File

@ -1,28 +1,53 @@
"""
##### ping
Check if a service is online.
```ini
[variable_name]
platform = ping
resource = 192.168.1.1
```
> **Returns:** a right-aligned colored bullet point on the app card.
| Variable | Required | Description | Options |
|-----------------|----------|-----------------------------------------------------------------|-------------------|
| [variable_name] | Yes | Name for the data source. | [variable_name] |
| platform | Yes | Name of the platform. | rest |
| resource | Yes | Url of whatever you want to ping | url |
"""
import platform
import subprocess
class Platform:
def docs(self):
documentation = {
"name": "ping",
"author": "Nixellion",
"author_url": "https://github.com/Nixellion",
"version": 1.0,
"description": "Check if a service is online.",
"example": """
```ini
[ping_test]
platform = ping
resource = localhost
[localhost]
prefix = http://
url = localhost
icon = static/images/apps/default.png
open_in = this_tab
data_sources = ping_test
```
""",
"returns": "a right-aligned colored bullet point on the app card.",
"variables": [
{
"variable": "[variable_name]",
"description": "Name for the data source.",
"default": "",
"options": ".ini header",
},
{
"variable": "platform",
"description": "Name of the platform.",
"default": "ping",
"options": "ping",
},
{
"variable": "resource",
"description": "Url of rest api resource.",
"default": "localhost",
"options": "url",
},
],
}
return documentation
def __init__(self, *args, **kwargs):
# parse the user's options from the config entries
for key, value in kwargs.items():

View File

@ -0,0 +1,154 @@
"""
##### Plex Media Server
Connect to Plex Media Server and see current sessions details
```ini
[variable_name]
platform = plex
url = http://plex_host:plex_port
token = plex_token
value_template = {{ value_template }}
```
> **Returns:** `value_template` as rendered string
| Variable | Required | Description | Options |
|-----------------|----------|-----------------------------------------------------------------|-------------------|
| [variable_name] | Yes | Name for the data source. | [variable_name] |
| platform | Yes | Name of the platform. | plex |
| host | Yes | URL of Plex Media Server (include port, normally 32400) | url |
| token | Yes | X-Plex-Token (See [here](https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/) for how to find it.) | string |
| value_template | Yes | Jinja template for how the returned data from API is displayed. | jinja template |
<br />
###### **Available fields for value_template**
* sessions
* transcodes
* libraries
> **Example:**
>```ini
>[plex]
>platform = plex
>host = http://plex.example.com:32400
>token = abcde_fghi_jklmnopqr
>value_template = Sessions: {{sessions}}<br />Transcodes: {{transcodes}}
>
>[Plex]
>prefix = http://
>url = plex.example.com:32400
>icon = static/images/apps/plex.png
>description = Plex data sources example
>open_in = this_tab
>data_sources = plex
>```
"""
import requests
from flask import render_template_string
json_header = {"Accept": "application/json"}
class Plex(object):
def __init__(self, url, token):
self.url = url
self.token = token
def refresh(self):
if self.token != None:
sessions = requests.get(
self.url + "/status/sessions?X-Plex-Token=" + self.token,
headers=json_header,
).json()
self.sessions = sessions["MediaContainer"]["size"]
transcodes = requests.get(
self.url + "/transcode/sessions?X-Plex-Token=" + self.token,
headers=json_header,
).json()
self.transcodes = transcodes["MediaContainer"]["size"]
libraries = requests.get(
self.url + "/library/sections?X-Plex-Token=" + self.token,
headers=json_header,
).json()
self.libraries = libraries["MediaContainer"]["size"]
class Platform:
def docs(self):
documentation = {
"name": "plex",
"author": "reedhaffner",
"author_url": "https://github.com/reedhaffner",
"version": 1.0,
"description": "Connect to Plex Media Server and see current sessions details",
"returns": "`value_template` as rendered string",
"returns_json_keys": ["sessions", "transcodes", "libraries",],
"example": """
```ini
[plex]
platform = plex
host = http://plex.example.com:32400
token = abcde_fghi_jklmnopqr
value_template = Sessions: {{sessions}}<br />Transcodes: {{transcodes}}
>
[Plex]
prefix = http://
url = plex.example.com:32400
icon = static/images/apps/plex.png
description = Plex data sources example
open_in = this_tab
data_sources = plex
```
""",
"variables": [
{
"variable": "[variable_name]",
"description": "Name for the data source.",
"default": "None, entry is required",
"options": ".ini header",
},
{
"variable": "platform",
"description": "Name of the platform.",
"default": "plex",
"options": "plex",
},
{
"variable": "host",
"description": "URL of Plex Media Server (include port, normally 32400)",
"default": "",
"options": "url",
},
{
"variable": "token",
"description": "X-Plex-Token (See [here](https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/) for how to find it.)",
"default": "",
"options": "string",
},
{
"variable": "value_template",
"description": "Jinja template for how the returned data from API is displayed.",
"default": "",
"options": "jinja template",
},
],
}
return documentation
def __init__(self, *args, **kwargs):
# parse the user's options from the config entries
for key, value in kwargs.items():
self.__dict__[key] = value
if not hasattr(self, "token"):
self.token = None
else:
self.plex = Plex(self.host, self.token)
def process(self):
self.plex.refresh()
value_template = render_template_string(
self.value_template, **self.plex.__dict__
)
return value_template

View File

@ -0,0 +1,303 @@
from flask import render_template_string
import requests
class Radarr(object):
def __init__(self, method, prefix, host, port, api_key, verify):
self.endpoint = "/api"
self.method = method
self.prefix = prefix
self.host = host
self.port = port
self.api_key = api_key
self.verify = verify
# Initialize results
self.error = None
self.version = "?"
self.movies = 0
self.queue = 0
self.diskspace = [
{"path": "", "total": "", "free": "", "used": ""},
{"path": "", "total": "", "free": "", "used": ""},
]
def check(self):
verify = (
False
if str(self.verify).lower() == "false"
or str(self.prefix).lower() == "http://"
else True
)
headers = {"X-Api-Key": self.api_key}
port = "" if self.port == None else ":" + self.port
if self.method.upper() == "GET":
try:
rawdata = requests.get(
self.prefix + self.host + port + self.endpoint + "/system/status",
headers=headers,
verify=verify,
timeout=10,
).json()
except Exception as e:
rawdata = None
self.error = f"{e}"
if rawdata != None:
if "error" in rawdata:
self.error = rawdata["error"]
def getVersion(self):
verify = (
False
if str(self.verify).lower() == "false"
or str(self.prefix).lower() == "http://"
else True
)
headers = {"X-Api-Key": self.api_key}
port = "" if self.port == None else ":" + self.port
if self.method.upper() == "GET":
try:
rawdata = requests.get(
self.prefix + self.host + port + self.endpoint + "/system/status",
headers=headers,
verify=verify,
timeout=10,
).json()
except Exception as e:
rawdata = None
self.error = f"{e}"
if rawdata != None:
self.version = rawdata["version"]
def getMovies(self):
verify = (
False
if str(self.verify).lower() == "false"
or str(self.prefix).lower() == "http://"
else True
)
headers = {"X-Api-Key": self.api_key}
port = "" if self.port == None else ":" + self.port
if self.method.upper() == "GET":
try:
rawdata = requests.get(
self.prefix + self.host + port + self.endpoint + "/movie",
headers=headers,
verify=verify,
timeout=10,
).json()
except Exception as e:
rawdata = None
self.error = f"{e}"
if rawdata != None:
self.movies = len(rawdata)
def getQueue(self):
verify = (
False
if str(self.verify).lower() == "false"
or str(self.prefix).lower() == "http://"
else True
)
headers = {"X-Api-Key": self.api_key}
port = "" if self.port == None else ":" + self.port
if self.method.upper() == "GET":
try:
rawdata = requests.get(
self.prefix + self.host + port + self.endpoint + "/queue",
headers=headers,
verify=verify,
timeout=10,
).json()
except Exception as e:
rawdata = None
self.error = f"{e}"
if rawdata != None:
self.queue = len((rawdata))
def getDiskspace(self):
verify = (
False
if str(self.verify).lower() == "false"
or str(self.prefix).lower() == "http://"
else True
)
headers = {"X-Api-Key": self.api_key}
port = "" if self.port == None else ":" + self.port
if self.method.upper() == "GET":
try:
rawdata = requests.get(
self.prefix + self.host + port + self.endpoint + "/diskspace",
headers=headers,
verify=verify,
timeout=10,
).json()
except Exception as e:
rawdata = None
self.error = f"{e}"
if rawdata != None:
self.diskspace = rawdata
for item in self.diskspace:
item["used"] = self.formatSize(item["totalSpace"] - item["freeSpace"])
item["total"] = self.formatSize(item["totalSpace"])
item["free"] = self.formatSize(item["freeSpace"])
item.pop("totalSpace", None)
item.pop("freeSpace", None)
def formatSize(self, size):
# 2**10 = 1024
power = 2 ** 10
n = 0
power_labels = {0: "", 1: "KB", 2: "MB", 3: "GB", 4: "TB"}
while size > power:
size /= power
n += 1
return str(round(size, 1)) + " " + power_labels[n]
def refresh(self):
self.check()
if self.error == None:
self.error = ""
self.getVersion()
self.getMovies()
self.getQueue()
self.getDiskspace()
class Platform:
def docs(self):
documentation = {
"name": "radarr",
"author": "Thlb",
"author_url": "https://github.com/Thlb",
"version": 1.0,
"description": "Display information from Radarr API",
"returns": "`value_template` as rendered string",
"returns_json_keys": [
"version",
"movies",
"queue",
"diskspace[x]['path']",
"diskspace[x]['total']",
"diskspace[x]['used']",
"diskspace[x]['free']",
"error (for debug)",
],
"example": """
```ini
[radarr-data]
platform = radarr
prefix = http://
host = 192.168.0.110
port = 7878
api_key = {{ API Key }}
verify = False
value_template = {{error}}Movies : {{movies}}<br />Queue : {{queue}} <br />Free ({{diskspace[0]['path']}}) : {{diskspace[0]['free']}}
[Radarr]
prefix = http://
url = 192.168.0.110:7878
icon = static/images/apps/radarr.png
sidebar_icon = static/images/apps/radarr.png
description = A fork of Sonarr to work with movies à la Couchpotato
open_in = this_tab
data_sources = radarr-data
```
""",
"variables": [
{
"variable": "[variable_name]",
"description": "Name for the data source.",
"default": "None, entry is required",
"options": ".ini header",
},
{
"variable": "platform",
"description": "Name of the platform.",
"default": "radarr",
"options": "radarr",
},
{
"variable": "prefix",
"description": "The prefix for the app's url.",
"default": "",
"options": "web prefix, e.g. http:// or https://",
},
{
"variable": "host",
"description": "Radarr Host",
"default": "",
"options": "url,ip",
},
{
"variable": "port",
"description": "Radarr Port",
"default": "",
"options": "port",
},
{
"variable": "api_key",
"description": "ApiKey",
"default": "",
"options": "api key",
},
{
"variable": "verify",
"description": "Turn TLS verification on or off, default is true",
"default": "",
"options": "true,false",
},
{
"variable": "value_template",
"description": "Jinja template for how the returned data from API is displayed.",
"default": "",
"options": "jinja template",
},
],
}
return documentation
def __init__(self, *args, **kwargs):
# parse the user's options from the config entries
for key, value in kwargs.items():
self.__dict__[key] = value
# set defaults for omitted options
if not hasattr(self, "method"):
self.method = "GET"
if not hasattr(self, "prefix"):
self.prefix = "http://"
if not hasattr(self, "host"):
self.host = None
if not hasattr(self, "port"):
self.port = None
if not hasattr(self, "api_key"):
self.api_key = None
if not hasattr(self, "verify"):
self.verify = True
self.radarr = Radarr(
self.method, self.prefix, self.host, self.port, self.api_key, self.verify
)
def process(self):
if self.api_key == None:
return "api_key missing"
if self.host == None:
return "host missing"
self.radarr.refresh()
value_template = render_template_string(
self.value_template, **self.radarr.__dict__
)
return value_template

View File

@ -1,54 +1,3 @@
"""
##### rest
Make a call on a REST API and display the results as a jinja formatted string.
```ini
[variable_name]
platform = rest
resource = https://your-website.com/api
value_template = {{value}}
method = post
authentication = basic
username = my_username
password = my_password
payload = {"var1": "hi", "var2": 1}
headers = {"Content-Type": "application/json"}
verify = false
```
> **Returns:** `value_template` as rendered string
| Variable | Required | Description | Options |
|-----------------|----------|-----------------------------------------------------------------|-------------------|
| [variable_name] | Yes | Name for the data source. | [variable_name] |
| platform | Yes | Name of the platform. | rest |
| resource | Yes | Url of rest api resource. | url |
| value_template | Yes | Jinja template for how the returned data from api is displayed. | jinja template |
| method | No | Method for the api call, default is GET | GET,POST |
| authentication | No | Authentication for the api call, default is None | None,basic,digest |
| username | No | Username to use for auth. | string |
| password | No | Password to use for auth. | string |
| payload | No | Payload for post request. | json |
| headers | No | Custom headers for get or post | json |
| verify | No | Turn TLS verification on or off, default is True | true,false |
> **Working example:**
>```ini
>[test]
>platform = rest
>resource = https://pokeapi.co/api/v2/pokemon
>value_template = Pokemon: {{value['count']}}
>
>[Pokemon]
>prefix = https://
>url = pokemon.com
>icon = static/images/apps/default.png
>description = Data sources example
>open_in = this_tab
>data_sources = test
>```
"""
import json
from requests import get, post
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
@ -56,6 +5,102 @@ from flask import render_template_string
class Platform:
def docs(self):
documentation = {
"name": "rest",
"author": "RMountjoy",
"author_url": "https://github.com/rmountjoy92",
"version": 1.0,
"description": "Make a call on a REST API and display the results as a jinja formatted string.",
"returns": "`value_template` as rendered string",
"returns_json_keys": ["value"],
"example": """
```ini
[test]
platform = rest
resource = https://pokeapi.co/api/v2/pokemon
value_template = Pokemon: {{value['count']}}
[Pokemon]
prefix = https://
url = pokemon.com
icon = static/images/apps/default.png
description = Data sources example
open_in = this_tab
data_sources = test
```
""",
"variables": [
{
"variable": "[variable_name]",
"description": "Name for the data source.",
"default": "None, entry is required",
"options": ".ini header",
},
{
"variable": "platform",
"description": "Name of the platform.",
"default": "rest",
"options": "rest",
},
{
"variable": "resource",
"description": "Url of rest api resource.",
"default": "",
"options": "url",
},
{
"variable": "value_template",
"description": "Jinja template for how the returned data from api is displayed.",
"default": "{{value}}",
"options": "jinja template",
},
{
"variable": "method",
"description": "Method for the api call",
"default": "GET",
"options": "GET,POST",
},
{
"variable": "authentication",
"description": "Authentication for the api call",
"default": "",
"options": "None,basic,digest",
},
{
"variable": "username",
"description": "Username to use for auth.",
"default": "",
"options": "string",
},
{
"variable": "password",
"description": "Password to use for auth.",
"default": "",
"options": "string",
},
{
"variable": "payload",
"description": "Payload for post request.",
"default": "",
"options": "json",
},
{
"variable": "headers",
"description": "Custom headers for get or post",
"default": "",
"options": "json",
},
{
"variable": "verify",
"description": "Turn TLS verification on or off",
"default": "True",
"options": "True, False",
},
],
}
return documentation
def __init__(self, *args, **kwargs):
# parse the user's options from the config entries
for key, value in kwargs.items():
@ -64,6 +109,8 @@ class Platform:
self.__dict__[key] = value
# set defaults for omitted options
if not hasattr(self, "value_template"):
self.method = "{{value}}"
if not hasattr(self, "method"):
self.method = "GET"
if not hasattr(self, "authentication"):

View File

@ -0,0 +1,130 @@
from flask import render_template_string
import requests
class Sabnzbd(object):
# Takes the ip address of Sabnzbd
def __init__(self, ip_address, port, api_key):
self.ip_address = ip_address
self.port = port
self.api_key = api_key
def refresh(self):
if self.api_key != None:
rawdata = requests.get(
"http://"
+ self.ip_address
+ ":"
+ self.port
+ "/api?"
+ "apikey="
+ self.api_key
+ "&mode=queue"
+ "&output=json"
).json()
queue = rawdata["queue"]
self.status = queue["status"]
self.no_of_slots = queue["noofslots_total"]
self.speed = queue["speed"]
self.size = queue["size"]
self.disk_free = queue["diskspace1_norm"]
self.eta = queue["eta"]
self.mb_left = queue["mbleft"]
self.time_left = queue["timeleft"]
class Platform:
def docs(self):
documentation = {
"name": "sabnzbd",
"author": "rxmii4269",
"author_url": "https://github.com/rxmii4269",
"version": 1.0,
"description": "Display information from the SABnzbd API",
"example": """
```ini
[sabnzbd-data]
platform = sabnzbd
host = 192.168.x.x
port = 8080
api_key = my_api_key
value_template = Status:{{status}}<br> {{speed}}<br>Size: {{size}}<br>
[Sabnzbd]
prefix = http://
url = 192.168.1.32:8080
icon = static/images/apps/sabnzbd.png
description = SABnzbd is a multi-platform binary newsgroup downloader. The program works in the background and simplifies the downloading verifying and extracting of files from Usenet.
open_in = iframe
data_sources = sabnzbd-data
```
""",
"returns": "`value_template` as rendered string",
"returns_json_keys": [
"status",
"no_of_slots",
"speed",
"size",
"disk_free",
"eta",
"mb_left",
"time_left",
],
"variables": [
{
"variable": "[variable_name]",
"description": "Name for the data source.",
"default": "",
"options": ".ini header",
},
{
"variable": "platform",
"description": "Name of the platform.",
"default": "sabnzbd",
"options": "sabnzbd",
},
{
"variable": "host",
"description": "Host of Sabnzbd",
"default": "192.168.x.x",
"options": "host",
},
{
"variable": "port",
"description": "Port of Sabnzbd",
"default": "8080",
"options": "port",
},
{
"variable": "api_key",
"description": "Api key for the Sabnzbd",
"default": "my_api_key",
"options": "api key",
},
{
"variable": "value_template",
"description": "Jinja template for how the returned data from API is displayed.",
"default": "Status:{{status}}<br>⬇ {{speed}}<br>Size: {{size}}<br>",
"options": "jinja template",
},
],
}
return documentation
def __init__(self, *args, **kwargs):
# parse the user's options from the config entries
for key, value in kwargs.items():
self.__dict__[key] = value
def pre_process(self):
self.sabnzbd = Sabnzbd(self.host, self.port, self.api_key)
return self
def process(self):
self = self.pre_process(self)
self.sabnzbd.refresh()
value_template = render_template_string(
self.value_template, **self.sabnzbd.__dict__
)
return value_template

View File

@ -0,0 +1,303 @@
from flask import render_template_string
import requests
class Sonarr(object):
def __init__(self, method, prefix, host, port, api_key, verify):
self.endpoint = "/api"
self.method = method
self.prefix = prefix
self.host = host
self.port = port
self.api_key = api_key
self.verify = verify
# Initialize results
self.error = None
self.version = "?"
self.wanted_missing = 0
self.queue = 0
self.diskspace = [
{"path": "", "total": "", "free": "", "used": ""},
{"path": "", "total": "", "free": "", "used": ""},
]
def check(self):
verify = (
False
if str(self.verify).lower() == "false"
or str(self.prefix).lower() == "http://"
else True
)
headers = {"X-Api-Key": self.api_key}
port = "" if self.port == None else ":" + self.port
if self.method.upper() == "GET":
try:
rawdata = requests.get(
self.prefix + self.host + port + self.endpoint + "/system/status",
headers=headers,
verify=verify,
timeout=10,
).json()
except Exception as e:
rawdata = None
self.error = f"{e}"
if rawdata != None:
if "error" in rawdata:
self.error = rawdata["error"]
def getVersion(self):
verify = (
False
if str(self.verify).lower() == "false"
or str(self.prefix).lower() == "http://"
else True
)
headers = {"X-Api-Key": self.api_key}
port = "" if self.port == None else ":" + self.port
if self.method.upper() == "GET":
try:
rawdata = requests.get(
self.prefix + self.host + port + self.endpoint + "/system/status",
headers=headers,
verify=verify,
timeout=10,
).json()
except Exception as e:
rawdata = None
self.error = f"{e}"
if rawdata != None:
self.version = rawdata["version"]
def getWanted(self):
verify = (
False
if str(self.verify).lower() == "false"
or str(self.prefix).lower() == "http://"
else True
)
headers = {"X-Api-Key": self.api_key}
port = "" if self.port == None else ":" + self.port
if self.method.upper() == "GET":
try:
rawdata = requests.get(
self.prefix + self.host + port + self.endpoint + "/wanted/missing/",
headers=headers,
verify=verify,
timeout=10,
).json()
except Exception as e:
rawdata = None
self.error = f"{e}"
if rawdata != None:
self.wanted_missing = rawdata["totalRecords"]
def getQueue(self):
verify = (
False
if str(self.verify).lower() == "false"
or str(self.prefix).lower() == "http://"
else True
)
headers = {"X-Api-Key": self.api_key}
port = "" if self.port == None else ":" + self.port
if self.method.upper() == "GET":
try:
rawdata = requests.get(
self.prefix + self.host + port + self.endpoint + "/queue",
headers=headers,
verify=verify,
timeout=10,
).json()
except Exception as e:
rawdata = None
self.error = f"{e}"
if rawdata != None:
self.queue = len(rawdata)
def getDiskspace(self):
verify = (
False
if str(self.verify).lower() == "false"
or str(self.prefix).lower() == "http://"
else True
)
headers = {"X-Api-Key": self.api_key}
port = "" if self.port == None else ":" + self.port
if self.method.upper() == "GET":
try:
rawdata = requests.get(
self.prefix + self.host + port + self.endpoint + "/diskspace",
headers=headers,
verify=verify,
timeout=10,
).json()
except Exception as e:
rawdata = None
self.error = f"{e}"
if rawdata != None:
self.diskspace = rawdata
for item in self.diskspace:
item["used"] = self.formatSize(item["totalSpace"] - item["freeSpace"])
item["total"] = self.formatSize(item["totalSpace"])
item["free"] = self.formatSize(item["freeSpace"])
item.pop("totalSpace", None)
item.pop("freeSpace", None)
def formatSize(self, size):
# 2**10 = 1024
power = 2 ** 10
n = 0
power_labels = {0: "", 1: "KB", 2: "MB", 3: "GB", 4: "TB"}
while size > power:
size /= power
n += 1
return str(round(size, 1)) + " " + power_labels[n]
def refresh(self):
self.check()
if self.error == None:
self.error = ""
self.getVersion()
self.getWanted()
self.getQueue()
self.getDiskspace()
class Platform:
def docs(self):
documentation = {
"name": "sonarr",
"author": "Thlb",
"author_url": "https://github.com/Thlb",
"version": 1.0,
"description": "Display information from Sonarr API",
"returns": "`value_template` as rendered string",
"returns_json_keys": [
"version",
"wanted_missing",
"queue",
"diskspace[x]['path']",
"diskspace[x]['total']",
"diskspace[x]['used']",
"diskspace[x]['free']",
"error (for debug)",
],
"example": """
```ini
[sonarr-data]
platform = sonarr
prefix = http://
host = 192.168.0.110
port = 8989
api_key = {{ API Key }}
verify = False
value_template = {{error}}Missing : {{wanted_missing}}<br />Queue : {{queue}} <br />Free ({{diskspace[0]['path']}}) : {{diskspace[0]['free']}}
[Sonarr]
prefix = http://
url = 192.168.0.110:8989
icon = static/images/apps/sonarr.png
sidebar_icon = static/images/apps/sonarr.png
description = Smart PVR for newsgroup and bittorrent users
open_in = this_tab
data_sources = sonarr-data
```
""",
"variables": [
{
"variable": "[variable_name]",
"description": "Name for the data source.",
"default": "None, entry is required",
"options": ".ini header",
},
{
"variable": "platform",
"description": "Name of the platform.",
"default": "sonarr",
"options": "sonarr",
},
{
"variable": "prefix",
"description": "The prefix for the app's url.",
"default": "",
"options": "web prefix, e.g. http:// or https://",
},
{
"variable": "host",
"description": "Sonarr Host",
"default": "",
"options": "url,ip",
},
{
"variable": "port",
"description": "Sonarr Port",
"default": "",
"options": "port",
},
{
"variable": "api_key",
"description": "ApiKey",
"default": "",
"options": "api key",
},
{
"variable": "verify",
"description": "Turn TLS verification on or off, default is true",
"default": "",
"options": "true,false",
},
{
"variable": "value_template",
"description": "Jinja template for how the returned data from API is displayed.",
"default": "",
"options": "jinja template",
},
],
}
return documentation
def __init__(self, *args, **kwargs):
# parse the user's options from the config entries
for key, value in kwargs.items():
self.__dict__[key] = value
# set defaults for omitted options
if not hasattr(self, "method"):
self.method = "GET"
if not hasattr(self, "prefix"):
self.prefix = "http://"
if not hasattr(self, "host"):
self.host = None
if not hasattr(self, "port"):
self.port = None
if not hasattr(self, "api_key"):
self.api_key = None
if not hasattr(self, "verify"):
self.verify = True
self.sonarr = Sonarr(
self.method, self.prefix, self.host, self.port, self.api_key, self.verify
)
def process(self):
if self.api_key == None:
return "api_key missing"
if self.host == None:
return "host missing"
self.sonarr.refresh()
value_template = render_template_string(
self.value_template, **self.sonarr.__dict__
)
return value_template

View File

@ -0,0 +1,260 @@
from flask import render_template_string
import requests
class Tautulli(object):
def __init__(self, method, prefix, host, port, api_key, verify):
self.endpoint = "/api/v2"
self.method = method
self.prefix = prefix
self.host = host
self.port = port
self.api_key = api_key
self.verify = verify
# Initialize results
self.error = None
self.update_available = ""
self.update_message = ""
self.stream_count = ""
def check(self):
verify = (
False
if str(self.verify).lower() == "false"
or str(self.prefix).lower() == "http://"
else True
)
headers = {"X-Api-Key": self.api_key}
port = "" if self.port == None else ":" + self.port
if self.method.upper() == "GET":
try:
rawdata = requests.get(
self.prefix
+ self.host
+ port
+ self.endpoint
+ "?apikey="
+ self.api_key
+ "&cmd="
+ "update_check",
verify=verify,
timeout=10,
).json()
except Exception as e:
rawdata = None
self.error = f"{e}"
if rawdata != None:
if "response" in rawdata and rawdata["response"]["result"] == "error":
self.error = rawdata["response"]["message"]
def getUpdate(self):
verify = (
False
if str(self.verify).lower() == "false"
or str(self.prefix).lower() == "http://"
else True
)
port = "" if self.port == None else ":" + self.port
if self.method.upper() == "GET":
try:
rawdata = requests.get(
self.prefix
+ self.host
+ port
+ self.endpoint
+ "?apikey="
+ self.api_key
+ "&cmd="
+ "update_check",
verify=verify,
timeout=10,
).json()
except Exception as e:
rawdata = None
self.error = f"{e}"
if rawdata != None:
self.update_message = rawdata["response"]["message"]
self.update_available = rawdata["response"]["data"]["update"]
def getActivity(self):
verify = (
False
if str(self.verify).lower() == "false"
or str(self.prefix).lower() == "http://"
else True
)
port = "" if self.port == None else ":" + self.port
if self.method.upper() == "GET":
try:
rawdata = requests.get(
self.prefix
+ self.host
+ port
+ self.endpoint
+ "?apikey="
+ self.api_key
+ "&cmd="
+ "get_activity",
verify=verify,
timeout=10,
).json()
except Exception as e:
rawdata = None
self.error = f"{e}"
if rawdata != None:
self.stream_count = rawdata["response"]["data"]["stream_count"]
self.stream_count_direct_play = rawdata["response"]["data"][
"stream_count_direct_play"
]
self.stream_count_direct_stream = rawdata["response"]["data"][
"stream_count_direct_stream"
]
self.stream_count_transcode = rawdata["response"]["data"][
"stream_count_transcode"
]
self.total_bandwidth = rawdata["response"]["data"]["total_bandwidth"]
self.wan_bandwidth = rawdata["response"]["data"]["wan_bandwidth"]
def refresh(self):
self.check()
if self.error == None:
self.error = ""
self.getUpdate()
self.getActivity()
class Platform:
def docs(self):
documentation = {
"name": "tautulli",
"author": "Thlb",
"author_url": "https://github.com/Thlb",
"version": 1.0,
"description": "Display information from Tautulli API",
"returns": "`value_template` as rendered string",
"returns_json_keys": [
"stream_count",
"stream_count_direct_play",
"stream_count_direct_stream",
"stream_count_transcode",
"total_bandwidth",
"wan_bandwidth",
"update_available",
"update_message",
"error (for debug)",
],
"example": """
```ini
[tautulli-data]
platform = tautulli
prefix = http://
host = 192.168.0.110
port = 8181
api_key = myApiKey
verify = False
value_template = {{error}}Active sessions : {{stream_count}}
[Tautulli]
prefix = http://
url = 192.168.0.110:8181
icon = static/images/apps/tautulli.png
sidebar_icon = static/images/apps/tautulli.png
description = A Python based monitoring and tracking tool for Plex Media Server
open_in = this_tab
data_sources = tautulli-data
```
""",
"variables": [
{
"variable": "[variable_name]",
"description": "Name for the data source.",
"default": "None, entry is required",
"options": ".ini header",
},
{
"variable": "platform",
"description": "Name of the platform.",
"default": "tautulli",
"options": "tautulli",
},
{
"variable": "prefix",
"description": "The prefix for the app's url.",
"default": "",
"options": "web prefix, e.g. http:// or https://",
},
{
"variable": "host",
"description": "Tautulli Host",
"default": "",
"options": "url,ip",
},
{
"variable": "port",
"description": "Tautulli Port",
"default": "",
"options": "port",
},
{
"variable": "api_key",
"description": "ApiKey",
"default": "",
"options": "api key",
},
{
"variable": "verify",
"description": "Turn TLS verification on or off, default is true",
"default": "",
"options": "true,false",
},
{
"variable": "value_template",
"description": "Jinja template for how the returned data from API is displayed.",
"default": "",
"options": "jinja template",
},
],
}
return documentation
def __init__(self, *args, **kwargs):
# parse the user's options from the config entries
for key, value in kwargs.items():
self.__dict__[key] = value
# set defaults for omitted options
if not hasattr(self, "method"):
self.method = "GET"
if not hasattr(self, "prefix"):
self.prefix = "http://"
if not hasattr(self, "host"):
self.host = None
if not hasattr(self, "port"):
self.port = None
if not hasattr(self, "api_key"):
self.api_key = None
if not hasattr(self, "verify"):
self.verify = True
self.tautulli = Tautulli(
self.method, self.prefix, self.host, self.port, self.api_key, self.verify
)
def process(self):
if self.api_key == None:
return "api_key missing"
if self.host == None:
return "host missing"
self.tautulli.refresh()
value_template = render_template_string(
self.value_template, **self.tautulli.__dict__
)
return value_template

View File

@ -1,67 +1,89 @@
"""
##### Transmission
Display information from the Trasnmission API
```ini
[variable_name]
platform = transmission
host = localhost
port = 9091
user = {{ transmission Web UI username }}
password = {{ Transmission Web UI password }}
value_template = {{ value_template }}
```
> **Returns:** `value_template` as rendered string
| Variable | Required | Description | Options |
|-----------------|----------|-----------------------------------------------------------------|-------------------|
| [variable_name] | Yes | Name for the data source. | [variable_name] |
| platform | Yes | Name of the platform. | transmission |
| host | Yes | Host of Transmission Web UI | host |
| port | Yes | Port of Transmission Web UI | port |
| user | Yes | Username for Transmission Web UI | username |
| password | Yes | Password for Transmission Web UI | password |
| value_template | Yes | Jinja template for how the returned data from API is displayed. | jinja template |
<br />
###### **Available fields for value_template**
* downloadSpeed
* uploadSpeed
* activeTorrentCount
* pausedTorrentCount
* torrentCount
> **Working example:**
>```ini
> [transmission-data]
> platform = transmission
> host = 192.168.1.30
> port = 9091
> user = admin
> password = password123
> value_template = 🔽 {{(downloadSpeed/1024/1024)|round(2)}} MB/s<br>🔼 {{(uploadSpeed/1024/1024)|round(2)}} MB/s<br><strong>Active:</strong> {{activeTorrentCount}}<br>
>
> [Transmission]
> prefix = http://
> url = 192.168.1.30:9091
> icon = static/images/apps/transmission.png
> description = A Fast, Easy, and Free BitTorrent Client
> open_in = new_tab
> data_sources = transmission-data
>```
"""
import json
from flask import render_template_string
import transmissionrpc
# from pprint import PrettyPrinter
# pp = PrettyPrinter()
class Platform:
def docs(self):
documentation = {
"name": "transmission",
"author": "Nixellion",
"author_url": "https://github.com/Nixellion",
"version": 1.0,
"description": "Display information from the Trasnmission API",
"example": """
```ini
[transmission-data]
platform = transmission
host = localhost
port = 9091
user = my_username
password = my_password
value_template = 🔽 {{(downloadSpeed/1024/1024)|round(2)}} MB/s<br>🔼 {{(uploadSpeed/1024/1024)|round(2)}} MB/s<br><strong>Active:</strong> {{activeTorrentCount}}<br>
[Transmission]
prefix = http://
url = 192.168.1.30:9091
icon = static/images/apps/transmission.png
description = A Fast, Easy, and Free BitTorrent Client
open_in = new_tab
data_sources = transmission-data
```
""",
"returns": "`value_template` as rendered string",
"returns_json_keys": [
"downloadSpeed",
"uploadSpeed",
"activeTorrentCount",
"pausedTorrentCount",
"torrentCount",
],
"variables": [
{
"variable": "[variable_name]",
"description": "Name for the data source.",
"default": "",
"options": ".ini header",
},
{
"variable": "platform",
"description": "Name of the platform.",
"default": "transmission",
"options": "transmission",
},
{
"variable": "host",
"description": "Host of Transmission Web UI ",
"default": "localhost",
"options": "host",
},
{
"variable": "port",
"description": "Port of Transmission Web UI ",
"default": "9091",
"options": "port",
},
{
"variable": "user",
"description": "Username for Transmission Web UI ",
"default": "my_username",
"options": "string",
},
{
"variable": "password",
"description": "Password for Transmission Web UI.",
"default": "my_password",
"options": "string",
},
{
"variable": "value_template",
"description": "Jinja template for how the returned data from API is displayed.",
"default": "{{(downloadSpeed/1024/1024)|round(2)}} MB/s<br>🔼 {{(uploadSpeed/1024/1024)|round(2)}} MB/s<br><strong>Active:</strong> {{activeTorrentCount}}<br>",
"options": "jinja template",
},
],
}
return documentation
def __init__(self, *args, **kwargs):
# parse the user's options from the config entries
for key, value in kwargs.items():
@ -72,12 +94,11 @@ class Platform:
if not hasattr(self, "host"):
self.host = "localhost"
def process(self):
self.tc = transmissionrpc.Client(
self.host, port=self.port, user=self.user, password=self.password
)
def process(self):
torrents = len(self.tc.get_torrents())
data = {}
for key, field in self.tc.session_stats().__dict__["_fields"].items():

View File

@ -1,46 +1,74 @@
"""
##### Weather
Weather is a great example of how you can populate a custom card on the dash. This plugin creates a custom card with weather data from [metaweather.com](https://www.metaweather.com)
```ini
[variable_name]
platform = weather
woeid = 2514815
temp_unit = c
wind_speed_unit = kph
air_pressure_unit = mbar
visibility_unit = km
```
> **Returns:** HTML for custom card
| Variable | Required | Description | Options |
|-----------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------|-------------------|
| [variable_name] | Yes | Name for the data source. | [variable_name] |
| platform | Yes | Name of the platform. | weather |
| woeid | Yes | woeid of location to use. Go here to get (replace lat and long): https://www.metaweather.com/api/location/search/?lattlong=50.068,-5.316 | url |
| temp_unit | No | The unit to be used for temperature | c,f |
| wind_speed_unit | No | The unit to be used for wind speed | kph,mph |
| air_pressure_unit | No | The unit to be used for air pressure | mbar, inHg |
| visibility_unit | No | The unit to be used for visibility | km,mi |
> **Working example:**
>```ini
>[variable_name]
>platform = weather
>woeid = 2514815
>
>[custom_card_name]
>type = custom
>data_sources = variable_name
>```
"""
import requests
from flask import render_template_string
class Platform:
def docs(self):
documentation = {
"name": "weather",
"author": "RMountjoy",
"author_url": "https://github.com/rmountjoy92",
"version": 1.0,
"description": "Weather is a great example of how you can populate a custom card on the dash. This plugin creates a custom card with weather data from https://www.metaweather.com",
"example": """
```ini
[variable_name]
platform = weather
woeid = 2514815
[custom_card_name]
type = custom
data_sources = variable_name
```
""",
"returns": "HTML for custom card",
"variables": [
{
"variable": "[variable_name]",
"description": "Name for the data source.",
"default": "",
"options": ".ini header",
},
{
"variable": "platform",
"description": "Name of the platform.",
"default": "weather",
"options": "weather",
},
{
"variable": "woeid",
"description": "woeid of location to use. Go here to get (replace lat and long): https://www.metaweather.com/api/location/search/?lattlong=50.068,-5.316",
"default": "2514815",
"options": "woeid",
},
{
"variable": "temp_unit",
"description": "The unit to be used for temperature",
"default": "c",
"options": "c,f",
},
{
"variable": "wind_speed_unit",
"description": "The unit to be used for wind speed",
"default": "kph",
"options": "kph,mph",
},
{
"variable": "air_pressure_unit",
"description": "The unit to be used for air pressure",
"default": "mbar",
"options": "mbar,inHg",
},
{
"variable": "visibility_unit",
"description": "The unit to be used for visibility",
"default": "km",
"options": "km,mi",
},
],
}
return documentation
def __init__(self, *args, **kwargs):
# parse the user's options from the config entries
for key, value in kwargs.items():
@ -54,14 +82,14 @@ class Platform:
if not hasattr(self, "wind_speed_unit"):
self.wind_speed_unit = "kph"
if not hasattr(self, "air_pressure_unit"):
self.air_pressure_unit = "x"
self.air_pressure_unit = "mbar"
if not hasattr(self, "visibility_unit"):
self.visibility_unit = "km"
self.html_template = """
<div class="row">
<div class="col s6">
<span class="mt-0 mb-0 theme-primary-text font-weight-700" style="font-size: 36px">{{ value.consolidated_weather[0].the_temp|round(1, 'floor') }}&deg;</h3>
<span class="mt-0 mb-0 theme-primary-text font-weight-700" style="font-size: 36px">{{ value.consolidated_weather[0].the_temp|round(1, 'floor') }}&deg;</span>
</div>
<div class="col s6 right-align">
<img height="48px" src="https://www.metaweather.com/static/img/weather/{{ value.consolidated_weather[0].weather_state_abbr }}.svg">

View File

@ -1,6 +1,11 @@
from flask_wtf import FlaskForm
from wtforms import TextAreaField
from wtforms import TextAreaField, StringField
class ConfigForm(FlaskForm):
config = TextAreaField()
wiki_name = StringField()
wiki_permalink_new = StringField()
wiki_author = StringField()
wiki_description = TextAreaField()
wiki_tags = StringField()

View File

@ -7,8 +7,7 @@ class Settings(db.Model):
accent = db.Column(db.String())
background = db.Column(db.String())
roles = db.Column(db.String())
home_access_groups = db.Column(db.String())
settings_access_groups = db.Column(db.String())
custom_app_title = db.Column(db.String())
sidebar_default = db.Column(db.String())
tags_expanded = db.Column(db.String())
action_providers = db.Column(db.String())
tags = db.Column(db.String())

View File

@ -1,61 +1,33 @@
import os
from shutil import move
from configparser import ConfigParser
from flask_login import current_user
from flask import render_template, request, Blueprint, jsonify, redirect, url_for
from dashmachine.user_system.forms import UserForm
from dashmachine.user_system.models import User
from dashmachine.main.utils import public_route, check_groups
from flask import (
request,
Blueprint,
jsonify,
render_template_string,
)
from dashmachine.main.read_config import read_config
from dashmachine.main.models import Files
from dashmachine.settings_system.forms import ConfigForm
from dashmachine.settings_system.utils import load_files_html, get_config_html
from dashmachine.settings_system.models import Settings
from dashmachine.settings_system.utils import load_files_html
from dashmachine.paths import (
backgrounds_images_folder,
icons_images_folder,
user_data_folder,
template_apps_folder,
)
from dashmachine.version import version, revision_number
settings_system = Blueprint("settings_system", __name__)
@public_route
@settings_system.route("/settings", methods=["GET"])
def settings():
settings_db = Settings.query.first()
if not check_groups(settings_db.settings_access_groups, current_user):
return redirect(url_for("main.home"))
config_form = ConfigForm()
user_form = UserForm()
user_form.role.choices += [(role, role) for role in settings_db.roles.split(",")]
with open(os.path.join(user_data_folder, "config.ini"), "r") as config_file:
config_form.config.data = config_file.read()
files_html = load_files_html()
template_apps = []
config = ConfigParser()
for template_app_ini in os.listdir(template_apps_folder):
config.read(os.path.join(template_apps_folder, template_app_ini))
entry = config[template_app_ini.replace(".ini", "")]
template_apps.append(f"{template_app_ini.replace('.ini', '')}&&{entry['icon']}")
users = User.query.all()
config_readme = get_config_html()
return render_template(
"settings_system/settings.html",
config_form=config_form,
files_html=files_html,
user_form=user_form,
template_apps=",".join(template_apps),
version=version,
revision_number=revision_number,
users=users,
config_readme=config_readme,
@settings_system.route("/get_settings_data", methods=["GET"])
def get_settings_data():
html = render_template_string(
"""
{% from "main/base.html" import SettingsData with context%}
{{ SettingsData() }}
"""
)
return html
@settings_system.route("/settings/save_config", methods=["POST"])
@ -63,6 +35,8 @@ def save_config():
with open(os.path.join(user_data_folder, "config.ini"), "w") as config_file:
config_file.write(request.form.get("config"))
msg = read_config()
if msg["msg"] != "success":
read_config(from_backup=True)
return jsonify(data=msg)

View File

@ -4,9 +4,7 @@ import random
from jsmin import jsmin
from flask_login import current_user
from dashmachine import app
from dashmachine.main.models import Apps, Tags
from dashmachine.main.utils import check_groups, get_update_message_html
from dashmachine.main.forms import TagsForm
from dashmachine.main.utils import get_update_message_html, row2dict
from dashmachine.settings_system.models import Settings
from dashmachine.paths import static_folder, backgrounds_images_folder
from dashmachine.cssmin import cssmin
@ -27,8 +25,9 @@ def process_js_sources(process_bundle=None, src=None, app_global=False):
elif app_global is True:
process_bundle = [
"global/dashmachine.js",
"global/tcdrop.js",
"main/dashmachine.js",
"main/ini-form.js",
"main/tcdrop.js",
]
html = ""
@ -82,49 +81,45 @@ def tag_sort_func(e):
@app.context_processor
def context_processor():
apps = []
temp_tags = []
tags = []
apps_db = Apps.query.all()
for app_db in apps_db:
if app_db.urls:
url_list = app_db.urls.replace("},{", "}%,%{").split("%,%")
app_db.urls_json = []
for url in url_list:
app_db.urls_json.append(json.loads(url))
if not app_db.groups:
app_db.groups = None
if check_groups(app_db.groups, current_user):
apps.append(app_db)
if app_db.tags:
temp_tags += app_db.tags.split(",")
tags_form = TagsForm()
if len(temp_tags) > 0:
temp_tags = list(dict.fromkeys([tag.strip() for tag in temp_tags]))
tags_form.tags.choices += [(tag, tag) for tag in temp_tags]
for tag in temp_tags:
tag_db = Tags.query.filter_by(name=tag).first()
if tag_db:
tags.append(tag_db)
tags.sort(key=tag_sort_func)
settings = Settings.query.first()
action_providers = []
for provider_json in settings.action_providers.replace("},{", "}%,%{").split("%,%"):
action_providers.append(json.loads(provider_json))
if settings.background == "random":
if len(os.listdir(backgrounds_images_folder)) < 1:
settings.background = None
settings.selected_background = None
else:
settings.background = (
settings.selected_background = (
f"static/images/backgrounds/"
f"{random.choice(os.listdir(backgrounds_images_folder))}"
)
else:
settings.selected_background = settings.background
if current_user.is_authenticated:
user = row2dict(current_user)
if user["background"] == "random":
if len(os.listdir(backgrounds_images_folder)) < 1:
user["selected_background"] = None
else:
user["selected_background"] = (
f"static/images/backgrounds/"
f"{random.choice(os.listdir(backgrounds_images_folder))}"
)
else:
user["selected_background"] = user["background"]
else:
user = {}
update_message = get_update_message_html()
return dict(
test_key="test",
process_js_sources=process_js_sources,
process_css_sources=process_css_sources,
apps=apps,
settings=settings,
tags=tags,
tags_form=tags_form,
user=user,
action_providers=action_providers,
update_message=update_message,
)

View File

@ -0,0 +1,147 @@
#nav-mobile .btn {
height: 36px;
}
#nav-mobile .btn i {
line-height: unset;
}
#md-container h1 {
color: var(--theme-color-font-muted);
font-size: 3rem;
}
#md-container h2 {
color: var(--theme-color-font-muted2);
font-size: 2.5rem;
}
#md-container h3 {
color: var(--theme-primary);
font-size: 2rem;
}
#md-container h4 {
color: var(--theme-primary);
font-size: 1.5rem;
}
#md-container h5 {
color: var(--theme-secondary);
font-size: 1.5rem;
font-weight: 800;
}
#md-container code, #md-container p, #md-container li {
-webkit-touch-callout: text;
-webkit-user-select: text;
-khtml-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
cursor: text;
white-space: pre-wrap;
}
#md-container th {
color: var(--theme-primary);
}
#md-container td {
-webkit-touch-callout: text !important;
-webkit-user-select: text !important;
-khtml-user-select: text !important;
-moz-user-select: text !important;
-ms-user-select: text !important;
user-select: text !important;
cursor: text;
}
#md-container strong {
font-weight: 900;
}
#md-container img {
max-width: 100%;
max-height: 250px;
}
#md-container li {
border: 1px solid var(--theme-surface-2);
border-radius: 20px;
padding: 1rem;
margin-bottom: 1rem;
}
.codehilite .hll { background-color: #404040 }
.codehilite {
background: #202020;
color: #d0d0d0;
padding: 1rem;
border-radius: 10px;
margin: 1rem;
}
.codehilite .c { color: #999999; font-style: italic } /* Comment */
.codehilite .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.codehilite .esc { color: #d0d0d0 } /* Escape */
.codehilite .g { color: #d0d0d0 } /* Generic */
.codehilite .k { color: #6ab825; font-weight: bold } /* Keyword */
.codehilite .l { color: #d0d0d0 } /* Literal */
.codehilite .n { color: #d0d0d0 } /* Name */
.codehilite .o { color: #d0d0d0 } /* Operator */
.codehilite .x { color: #d0d0d0 } /* Other */
.codehilite .p { color: #d0d0d0 } /* Punctuation */
.codehilite .ch { color: #999999; font-style: italic } /* Comment.Hashbang */
.codehilite .cm { color: #999999; font-style: italic } /* Comment.Multiline */
.codehilite .cp { color: #cd2828; font-weight: bold } /* Comment.Preproc */
.codehilite .cpf { color: #999999; font-style: italic } /* Comment.PreprocFile */
.codehilite .c1 { color: #999999; font-style: italic } /* Comment.Single */
.codehilite .cs { color: #e50808; font-weight: bold; background-color: #520000 } /* Comment.Special */
.codehilite .gd { color: #d22323 } /* Generic.Deleted */
.codehilite .ge { color: #d0d0d0; font-style: italic } /* Generic.Emph */
.codehilite .gr { color: #d22323 } /* Generic.Error */
.codehilite .gh { color: #ffffff; font-weight: bold } /* Generic.Heading */
.codehilite .gi { color: #589819 } /* Generic.Inserted */
.codehilite .go { color: #cccccc } /* Generic.Output */
.codehilite .gp { color: #aaaaaa } /* Generic.Prompt */
.codehilite .gs { color: #d0d0d0; font-weight: bold } /* Generic.Strong */
.codehilite .gu { color: #ffffff; text-decoration: underline } /* Generic.Subheading */
.codehilite .gt { color: #d22323 } /* Generic.Traceback */
.codehilite .kc { color: #6ab825; font-weight: bold } /* Keyword.Constant */
.codehilite .kd { color: #6ab825; font-weight: bold } /* Keyword.Declaration */
.codehilite .kn { color: #6ab825; font-weight: bold } /* Keyword.Namespace */
.codehilite .kp { color: #6ab825 } /* Keyword.Pseudo */
.codehilite .kr { color: #6ab825; font-weight: bold } /* Keyword.Reserved */
.codehilite .kt { color: #6ab825; font-weight: bold } /* Keyword.Type */
.codehilite .ld { color: #d0d0d0 } /* Literal.Date */
.codehilite .m { color: #3677a9 } /* Literal.Number */
.codehilite .s { color: #ed9d13 } /* Literal.String */
.codehilite .na { color: #bbbbbb } /* Name.Attribute */
.codehilite .nb { color: #24909d } /* Name.Builtin */
.codehilite .nc { color: #447fcf; text-decoration: underline } /* Name.Class */
.codehilite .no { color: #40ffff } /* Name.Constant */
.codehilite .nd { color: #ffa500 } /* Name.Decorator */
.codehilite .ni { color: #d0d0d0 } /* Name.Entity */
.codehilite .ne { color: #bbbbbb } /* Name.Exception */
.codehilite .nf { color: #447fcf } /* Name.Function */
.codehilite .nl { color: #d0d0d0 } /* Name.Label */
.codehilite .nn { color: #447fcf; text-decoration: underline } /* Name.Namespace */
.codehilite .nx { color: #d0d0d0 } /* Name.Other */
.codehilite .py { color: #d0d0d0 } /* Name.Property */
.codehilite .nt { color: #6ab825; font-weight: bold } /* Name.Tag */
.codehilite .nv { color: #40ffff } /* Name.Variable */
.codehilite .ow { color: #6ab825; font-weight: bold } /* Operator.Word */
.codehilite .w { color: #666666 } /* Text.Whitespace */
.codehilite .mb { color: #3677a9 } /* Literal.Number.Bin */
.codehilite .mf { color: #3677a9 } /* Literal.Number.Float */
.codehilite .mh { color: #3677a9 } /* Literal.Number.Hex */
.codehilite .mi { color: #3677a9 } /* Literal.Number.Integer */
.codehilite .mo { color: #3677a9 } /* Literal.Number.Oct */
.codehilite .sa { color: #ed9d13 } /* Literal.String.Affix */
.codehilite .sb { color: #ed9d13 } /* Literal.String.Backtick */
.codehilite .sc { color: #ed9d13 } /* Literal.String.Char */
.codehilite .dl { color: #ed9d13 } /* Literal.String.Delimiter */
.codehilite .sd { color: #ed9d13 } /* Literal.String.Doc */
.codehilite .s2 { color: #ed9d13 } /* Literal.String.Double */
.codehilite .se { color: #ed9d13 } /* Literal.String.Escape */
.codehilite .sh { color: #ed9d13 } /* Literal.String.Heredoc */
.codehilite .si { color: #ed9d13 } /* Literal.String.Interpol */
.codehilite .sx { color: #ffa500 } /* Literal.String.Other */
.codehilite .sr { color: #ed9d13 } /* Literal.String.Regex */
.codehilite .s1 { color: #ed9d13 } /* Literal.String.Single */
.codehilite .ss { color: #ed9d13 } /* Literal.String.Symbol */
.codehilite .bp { color: #24909d } /* Name.Builtin.Pseudo */
.codehilite .fm { color: #447fcf } /* Name.Function.Magic */
.codehilite .vc { color: #40ffff } /* Name.Variable.Class */
.codehilite .vg { color: #40ffff } /* Name.Variable.Global */
.codehilite .vi { color: #40ffff } /* Name.Variable.Instance */
.codehilite .vm { color: #40ffff } /* Name.Variable.Magic */
.codehilite .il { color: #3677a9 } /* Literal.Number.Integer.Long */

View File

@ -5,7 +5,8 @@
--theme-surface-rgb: 255, 255, 255;
--theme-surface-1: #fcfcfc;
--theme-surface-2: #e0e0e0;
--theme-primary: #FF9966;
--theme-almost-transparent: rgba(255, 255, 255, 0.2);
--theme-primary: #ff9800;
--theme-secondary: #9e9e9e;
--theme-accent: #3399FF;
--theme-color-font: #2c2f3a;
@ -22,8 +23,10 @@
--theme-surface-rgb: 47, 47, 47;
--theme-surface-1: #434343;
--theme-surface-2: #575757;
--theme-almost-transparent: rgba(47, 47, 47, 0.4);
--theme-color-font: #fff;
--theme-color-font-muted: #f9f9f9;
--theme-color-font-muted2: #9e9e9e;
--theme-warning: #dc584e;
}
[data-accent="red"] {
@ -149,6 +152,9 @@
.theme-surface-transparent1 {
background: rgba(var(--theme-surface-rgb), 0.9) !important;
}
.theme-almost-transparent {
background: var(--theme-almost-transparent) !important;
}
.theme-on-primary {
background-color: var(--theme-on-primary) !important;
}

View File

@ -33,17 +33,20 @@
}
.scrollbar {
overflow-y: scroll !important;
overflow-y: auto !important;
overflow-x: hidden !important;
}
.scrollbar-x {
overflow-y: scroll !important;
overflow-x: scroll !important;
overflow-y: auto !important;
overflow-x: auto !important;
}
/* ELEMENT STLYES */
body {
overflow: scroll;
background-position: center;
background-size: cover;
background-attachment: fixed;
overflow: auto;
overflow-x: hidden !important;
min-height: 100%;
color: var(--theme-color-font);
@ -142,7 +145,8 @@ textarea {
}
input:disabled {
color: var(--theme-secondary);
color: var(--theme-secondary) !important;
border-bottom-color: var(--theme-secondary) !important;
}
/* label color */
@ -198,6 +202,18 @@ input:disabled {
max-height: 30px;
}
.input-field ::-webkit-input-placeholder { /* Edge */
line-height: 3em !important;
}
.input-field :-ms-input-placeholder { /* Internet Explorer 10-11 */
line-height: 3em !important;
}
.input-field ::placeholder {
line-height: 3em !important;
}
.input-field > label {
font-size: .9rem;
webkit-transform: unset;
@ -285,25 +301,89 @@ input:disabled {
/* END FORM STYLES */
/* SIDENAV*/
#show-sidenav {
position: fixed;
bottom: 10px;
left: 0px;
height: 40px;
width: 48px;
z-index: 9999;
border-radius: 0 10px 10px 0;
background: var(--theme-primary);
#main-sidenav {
height: 100vh;
top: 0;
overflow-y: scroll;
}
#show-sidenav .material-icons-outlined {
font-size: 32px;
@media only screen and (min-width: 993px)
{
#main-sidenav
{
width: 30vw;
}
}
@media only screen and (max-width: 993px)
{
#main-sidenav
{
width: 100vw;
}
}
#main-sidenav .collection {
border: 0;
}
#main-sidenav .collection-item {
background: var(--theme-surface);
}
#main-sidenav .collection-item:hover {
background: var(--theme-surface-1);
}
#main-sidenav .app-name {
font-size: 1.3rem;
position: relative;
left: 15px;
top: 5px;
margin-left: 1rem;
bottom: 2px;
}
.sidenav-main .sidenav-collapsible {
border-radius: 0px;
background-color: var(--theme-surface);
#main-sidenav .app-description {
margin-left: .4rem;
}
#main-sidenav .app-icon {
height: 24px;
position: relative;
top: 4px;
}
#main-sidenav .collection .material-icons-outlined {
font-size: 1.2rem;
position: relative;
margin-left: .5rem;
top: 1px;
}
#main-sidenav .filter-tags-dropdown-a .s2 {
top: 10px !important;
max-height: 30px;
}
#main-sidenav .filter-tags-dropdown-a .s10 {
top: 2px !important;
max-height: 30px;
}
#sidenav-toggle-svg {
position: fixed;
}
#sidenav-toggle-svg-container {
z-index: 8000;
}
.drag-target {
width: 0;
height: 0;
}
#main-sidenav .collection .collection-item {
border-bottom: unset;
}
#sidenav-drag-handle-svg path:not(.on-primary) {
fill: var(--theme-primary);
}
#sidenav-drag-handle-svg path:not(.primary) {
fill: var(--theme-on-primary);
}
#sidenav-expand-area-svg path:not(.on-primary) {
fill: var(--theme-primary);
}
#sidenav-expand-area-svg path:not(.primary) {
fill: var(--theme-on-primary);
}
.sidenav li > a > i.material-icons-outlined {
@ -343,12 +423,6 @@ input:disabled {
overflow-y: scroll;
scrollbar-width: none;
}
#sidenav-mobile-toggle-btn {
position: fixed;
top: unset;
bottom: 10px;
}
.sidenav-active-rounded .sidenav li > a.active > i {
color: var(--theme-on-primary) !important;
}
@ -362,7 +436,7 @@ input:disabled {
/* MODALS AND CARDS */
.modal {
background-color: var(--theme-surface);
border-radius: 12px !important;
border-radius: 10px !important;
position: fixed;
}
@ -384,6 +458,13 @@ input:disabled {
position: fixed;
top: 0 !important;
}
@media only screen and (max-width: 992px) {
.full-height-modal {
max-height: 90%;
min-height: 90%;
top: 5% !important;
}
}
.three-qtr-height-modal {
height: 85%;
@ -593,3 +674,256 @@ span.badge.new {
.tap-target-wave::before, .tap-target-wave::after {
background-color: var(--theme-background);
}
/* xxl classes */
@media only screen and (min-width: 1901px) {
.row .col.xxl1 {
width: 8.3333333333%;
margin-left: auto;
left: auto;
right: auto;
}
.row .col.xxl2 {
width: 16.6666666667%;
margin-left: auto;
left: auto;
right: auto;
}
.row .col.xxl3 {
width: 25%;
margin-left: auto;
left: auto;
right: auto;
}
.row .col.xxl4 {
width: 33.3333333333%;
margin-left: auto;
left: auto;
right: auto;
}
.row .col.xxl5 {
width: 41.6666666667%;
margin-left: auto;
left: auto;
right: auto;
}
.row .col.xxl6 {
width: 50%;
margin-left: auto;
left: auto;
right: auto;
}
.row .col.xxl7 {
width: 58.3333333333%;
margin-left: auto;
left: auto;
right: auto;
}
.row .col.xxl8 {
width: 66.6666666667%;
margin-left: auto;
left: auto;
right: auto;
}
.row .col.xxl9 {
width: 75%;
margin-left: auto;
left: auto;
right: auto;
}
.row .col.xxl10 {
width: 83.3333333333%;
margin-left: auto;
left: auto;
right: auto;
}
.row .col.xxl11 {
width: 91.6666666667%;
margin-left: auto;
left: auto;
right: auto;
}
.row .col.xxl12 {
width: 100%;
margin-left: auto;
left: auto;
right: auto;
}
.row .col.offset-xxl1 {
margin-left: 8.3333333333%;
}
.row .col.pull-xxl1 {
right: 8.3333333333%;
}
.row .col.push-xxl1 {
left: 8.3333333333%;
}
.row .col.offset-xxl2 {
margin-left: 16.6666666667%;
}
.row .col.pull-xxl2 {
right: 16.6666666667%;
}
.row .col.push-xxl2 {
left: 16.6666666667%;
}
.row .col.offset-xxl3 {
margin-left: 25%;
}
.row .col.pull-xxl3 {
right: 25%;
}
.row .col.push-xxl3 {
left: 25%;
}
.row .col.offset-xxl4 {
margin-left: 33.3333333333%;
}
.row .col.pull-xxl4 {
right: 33.3333333333%;
}
.row .col.push-xxl4 {
left: 33.3333333333%;
}
.row .col.offset-xxl5 {
margin-left: 41.6666666667%;
}
.row .col.pull-xxl5 {
right: 41.6666666667%;
}
.row .col.push-xxl5 {
left: 41.6666666667%;
}
.row .col.offset-xxl6 {
margin-left: 50%;
}
.row .col.pull-xxl6 {
right: 50%;
}
.row .col.push-xxl6 {
left: 50%;
}
.row .col.offset-xxl7 {
margin-left: 58.3333333333%;
}
.row .col.pull-xxl7 {
right: 58.3333333333%;
}
.row .col.push-xxl7 {
left: 58.3333333333%;
}
.row .col.offset-xxl8 {
margin-left: 66.6666666667%;
}
.row .col.pull-xxl8 {
right: 66.6666666667%;
}
.row .col.push-xxl8 {
left: 66.6666666667%;
}
.row .col.offset-xxl9 {
margin-left: 75%;
}
.row .col.pull-xxl9 {
right: 75%;
}
.row .col.push-xxl9 {
left: 75%;
}
.row .col.offset-xxl10 {
margin-left: 83.3333333333%;
}
.row .col.pull-xxl10 {
right: 83.3333333333%;
}
.row .col.push-xxl10 {
left: 83.3333333333%;
}
.row .col.offset-xxl11 {
margin-left: 91.6666666667%;
}
.row .col.pull-xxl11 {
right: 91.6666666667%;
}
.row .col.push-xxl11 {
left: 91.6666666667%;
}
.row .col.offset-xxl12 {
margin-left: 100%;
}
.row .col.pull-xxl12 {
right: 100%;
}
.row .col.push-xxl12 {
left: 100%;
}
}
/* DM Logo */
.dm-logo-svg path:not(.clear-fill) {
fill: var(--theme-primary);
}
/* INI FORM */
.ini-form-info-dropdown-dropdown-content {
background-color: var(--theme-surface);
max-width: 50%;
min-width: 50%;
}
.ini-form-info-dropdown-dropdown-content li, .ini-form-info-dropdown-dropdown-content span {
background-color: var(--theme-surface) !important;
cursor: default;
}
.ini-form-info-dropdown-dropdown-content .selectable {
cursor: text;
color: var(--theme-color-font-muted)
}
.ini-form-info-dropdown-dropdown-content .theme-primary-text {
font-size: 1.1rem;
font-weight: 900;
}
/* MODULE SIDENAVS */
#card-editor-sidenav, #settings-editor-sidenav {
width: 25%;
z-index: 8000;
}
@media only screen and (min-width: 1901px) {
#card-editor-sidenav, #settings-editor-sidenav {
width: 40%;
}
}
@media only screen and (max-width: 1901px) {
#card-editor-sidenav, #settings-editor-sidenav {
width: 50%;
}
}
@media only screen and (max-width: 1200px) {
#card-editor-sidenav, #settings-editor-sidenav {
width: 60%;
}
}
@media only screen and (max-width: 991px) {
#card-editor-sidenav, #settings-editor-sidenav {
width: 100%;
}
}
#config-editor-sidenav {
width: 100vw;
z-index: 7999;
}
#card-editor-sidenav table, #settings-editor-sidenav table {
border-collapse: separate;
border-spacing: 0 1rem;
}
#card-editor-sidenav table.highlight>tbody>tr:hover, #settings-editor-sidenav table.highlight>tbody>tr:hover {
background-color: var(--theme-surface-1);
}
#card-editor-sidenav table.highlight>tbody>tr, #settings-editor-sidenav table.highlight>tbody>tr {
background-color: var(--theme-surface);
}
#card-editor-sidenav td th, #settings-editor-sidenav td th {
border-radius: 0;
}

View File

@ -123,7 +123,7 @@ select
}
#main.main-full
{
padding-left: 64px;
padding-left: 0px;
}
footer
{

View File

@ -22,41 +22,11 @@
}
}
.app-card .card-reveal {
position:
}
#list-view-collection .app-a {
background: rgba(var(--theme-surface-rgb), 0.8);
}
#list-view-collection .app-a:hover {
background: var(--theme-surface-1);
}
#list-view-collection .app-name {
font-size: 1.3rem;
position: relative;
margin-left: 1rem;
bottom: 2px;
}
#list-view-collection .app-description {
margin-left: .4rem;
}
#list-view-collection .app-icon {
height: 24px;
position: relative;
top: 4px;
}
#list-view-collection {
.expandable-card {
max-height: 146px;
min-height: 146px;
border: 0;
}
#list-view-collection .data-source-container {
position: relative;
top: 6px;
}
#list-view-collection .material-icons-outlined {
font-size: 1.2rem;
position: relative;
margin-left: .5rem;
top: 1px;
.collection-url-collection-item:hover {
background: var(--theme-background) !important;
}

View File

@ -1,66 +0,0 @@
@media (min-width: 990px)
{
body {
overflow: hidden !important;
}
}
@media (max-width: 990px)
{
body {
max-height: 200vh !important;
}
}
.settings-page-card-right {
max-height: calc(100vh - 142px) !important;
min-height: calc(100vh - 142px) !important;
}
.settings-page-card-left {
max-height: calc(100vh - 130px) !important;
min-height: calc(100vh - 130px) !important;
}
#apps .dropdown-content {
border-top-left-radius: 0px;
border-top-right-radius: 0px;
background: var(--theme-surface-1);
}
#settings-readme h5, #cards-readme h5, #data-sources-readme h5 {
color: var(--theme-primary);
margin-top: 5%;
}
#settings-readme h4, #cards-readme h4, #data-sources-readme h4 {
color: var(--theme-color-font-muted);
margin-top: 5%;
}
#configini-readme {
margin-top: 2% !important;
}
#settings-readme code, #cards-readme code, #data-sources-readme code {
-webkit-touch-callout: all;
-webkit-user-select: all;
-khtml-user-select: all;
-moz-user-select: all;
-ms-user-select: all;
user-select: all;
cursor: text;
white-space: pre-wrap;
}
#settings-readme th, #cards-readme th, #data-sources-readme th {
color: var(--theme-primary);
}
#settings-readme td, #cards-readme td, #data-sources-readme td {
-webkit-touch-callout: text !important;
-webkit-user-select: text !important;
-khtml-user-select: text !important;
-moz-user-select: text !important;
-ms-user-select: text !important;
user-select: text !important;
cursor: text;
}
#settings-readme strong, #cards-readme strong, #data-sources-readme strong {
font-weight: 900;
}

View File

@ -3599,8 +3599,8 @@
}
.animated.faster {
-webkit-animation-duration: 500ms;
animation-duration: 500ms;
-webkit-animation-duration: 300ms;
animation-duration: 300ms;
}
.animated.slow {

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 10 81" width="10pt" height="81pt"><defs><clipPath id="_clipPath_yX1mePdBd72kddEcR31mokfaMyxRQrZw"><rect width="10" height="81"/></clipPath></defs><g clip-path="url(#_clipPath_yX1mePdBd72kddEcR31mokfaMyxRQrZw)"><rect width="10" height="81" style="fill:rgb(0,0,0)" fill-opacity="0"/><path d="M 0 0 L 7.57 0 C 8.911 0 10 1.089 10 2.43 L 10 78.57 C 10 79.911 8.911 81 7.57 81 L 0 81 L 0 0 Z" style="stroke:none;fill:#000000;stroke-miterlimit:10;"/><path d=" M 3.167 43.177 C 3.105 43.177 3.046 43.163 2.994 43.134 C 2.863 43.063 2.786 42.911 2.786 42.721 L 2.786 38.278 C 2.786 38.085 2.861 37.935 2.994 37.865 C 3.126 37.795 3.294 37.813 3.454 37.92 L 6.757 40.094 C 6.912 40.196 7 40.345 7 40.502 C 7 40.66 6.912 40.808 6.757 40.91 L 3.454 43.084 C 3.359 43.143 3.26 43.177 3.167 43.177 L 3.167 43.177 L 3.167 43.177 Z " fill="rgb(255,255,255)"/></g></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 10 19" width="10pt" height="19pt"><defs><clipPath id="_clipPath_dmW8rGtQBb7JmtyTnmffRe5NJ2ORo9jW"><rect width="10" height="19"/></clipPath></defs><g clip-path="url(#_clipPath_dmW8rGtQBb7JmtyTnmffRe5NJ2ORo9jW)"><rect width="10" height="19" style="fill:rgb(0,0,0)" fill-opacity="0"/><path d="M 0 0 L 8.05 0 C 9.126 0 10 0.874 10 1.95 L 10 17.05 C 10 18.126 9.126 19 8.05 19 L 0 19 L 0 0 Z" style="stroke:none;fill:#000000;stroke-miterlimit:10;"/><path d=" M 5 5.929 C 5.563 5.929 6.02 6.386 6.02 6.949 C 6.02 7.512 5.563 7.969 5 7.969 C 4.437 7.969 3.98 7.512 3.98 6.949 C 3.98 6.386 4.437 5.929 5 5.929 L 5 5.929 Z M 5 8.48 C 5.563 8.48 6.02 8.937 6.02 9.5 C 6.02 10.063 5.563 10.52 5 10.52 C 4.437 10.52 3.98 10.063 3.98 9.5 C 3.98 8.937 4.437 8.48 5 8.48 L 5 8.48 Z M 5 11.031 C 5.563 11.031 6.02 11.488 6.02 12.051 C 6.02 12.614 5.563 13.071 5 13.071 C 4.437 13.071 3.98 12.614 3.98 12.051 C 3.98 11.488 4.437 11.031 5 11.031 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 10 100" width="10pt" height="100pt"><defs><clipPath id="_clipPath_BT6tYYvBGgDeqy5FcApTpgOQGXFhR2Fi"><rect width="10" height="100"/></clipPath></defs><g clip-path="url(#_clipPath_BT6tYYvBGgDeqy5FcApTpgOQGXFhR2Fi)"><rect width="10" height="100" style="fill:rgb(0,0,0)" fill-opacity="0"/><path d="M 0 0 L 7.3 0 C 8.79 0 10 1.21 10 2.7 L 10 97.3 C 10 98.79 8.79 100 7.3 100 L 0 100 L 0 0 Z" style="stroke:none;fill:#000000;stroke-miterlimit:10;"/><path d=" M 3.167 96.353 C 3.105 96.353 3.046 96.34 2.994 96.31 C 2.863 96.24 2.786 96.088 2.786 95.898 L 2.786 91.454 C 2.786 91.262 2.861 91.112 2.994 91.042 C 3.126 90.972 3.294 90.99 3.454 91.096 L 6.757 93.27 C 6.912 93.372 7 93.522 7 93.678 C 7 93.837 6.912 93.984 6.757 94.086 L 3.454 96.26 C 3.359 96.319 3.26 96.353 3.167 96.353 L 3.167 96.353 L 3.167 96.353 Z " fill="rgb(255,255,255)"/><path d=" M 5 5.929 C 5.563 5.929 6.02 6.386 6.02 6.949 C 6.02 7.512 5.563 7.969 5 7.969 C 4.437 7.969 3.98 7.512 3.98 6.949 C 3.98 6.386 4.437 5.929 5 5.929 L 5 5.929 Z M 5 8.48 C 5.563 8.48 6.02 8.937 6.02 9.5 C 6.02 10.063 5.563 10.52 5 10.52 C 4.437 10.52 3.98 10.063 3.98 9.5 C 3.98 8.937 4.437 8.48 5 8.48 L 5 8.48 Z M 5 11.031 C 5.563 11.031 6.02 11.488 6.02 12.051 C 6.02 12.614 5.563 13.071 5 13.071 C 4.437 13.071 3.98 12.614 3.98 12.051 C 3.98 11.488 4.437 11.031 5 11.031 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/><rect x="0" y="0" width="10" height="19" transform="matrix(1,0,0,1,0,0)" fill="none"/></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,9 @@
$(document).ready(function(){
$('#mobile-demo').sidenav();
$("#md-container").find('a').on('click', function(e) {
sleep(100).then(() => {
var y = $(window).scrollTop();
$(window).scrollTop(y-185);
});
});
});

View File

@ -0,0 +1,47 @@
function reset_config_editor(){
$("#config-card-title").text("Config.ini");
$("#save-config-btn").removeClass('hide');
$("#save-editing-wiki-btn").addClass('hide');
$("#wiki-config-form").addClass('hide');
config_textarea_codemirror.toTextArea();
$("#config-textarea").val($("#config-editor-config-data").val());
init_codemirror('properties');
}
$( document ).ready(function() {
$("#edit-wiki-btn").on('click', function(e) {
config_textarea_codemirror.setValue($(this).attr("data-md"));
config_textarea_codemirror.toTextArea();
init_codemirror('markdown');
$("#wiki-config-form-permalink").val($(this).attr('data-permalink'));
$("#wiki-config-form-permalink-new").val($(this).attr('data-permalink'));
$("#wiki-config-form-name").val($(this).attr('data-name'));
$("#wiki-config-form-author").val($(this).attr('data-author'));
$("#wiki-config-form-description").val($(this).attr('data-description'));
$("#wiki-config-form-tags").val($(this).attr('data-tags'));
M.updateTextFields();
$("#wiki-config-form").removeClass('hide');
$("#config-editor-sidenav").sidenav('open');
$("#save-config-btn").addClass('hide');
$("#save-editing-wiki-btn").removeClass('hide');
$("#config-card-title").text(`Editing ${$(this).attr("data-name")}`);
$("#close-config-editor-sidenav").one('click', function (e) {
reset_config_editor();
})
$("#save-editing-wiki-btn").on('click', function(e) {
M.toast({html: "Reloading.."})
config_textarea_codemirror.save();
$.ajax({
url: $(this).attr('data-url'),
type: 'POST',
data: $("#config-form").serialize(),
success: function(data){
location.reload();
}
});
});
});
});

View File

@ -1,345 +0,0 @@
const sleep = (milliseconds) => {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}
function js_Load() {
document.body.style.visibility = 'visible';
}
function updateTabIndicator(){
sleep(250).then(() => {
$(".tabs").tabs('updateTabIndicator');
});
}
function init_select(){
$('select').formSelect({
dropdownOptions:{
container: document.body,
constrainWidth: true,
}
});
$('input').each(function(index, el) {
if ($(this).attr('data-autocomplete-options')){
let options_list = $(this).attr('data-autocomplete-options').split(',');
let options_dict = options_list.map(x => ({'key': x, 'val': null}));
options_dict = options_dict.reduce(function(map, obj) {
map[obj.key] = obj.val;
return map;
}, {});
$(this).autocomplete({
data: options_dict,
dropdownOptions:{
container: document.body,
}
});
if ($(this).attr('data-auto-only') === 'true') {
$(this).on('blur', function(e) {
if (options_list.includes($(this).val()) === false) {
$(this).val('');
}
});
}
}
});
}
function init_copy_btn(parent_class){
$(".copy-btn").on('click', function(e) {
let target_text = $(this).closest(parent_class).find('.copy-target').text();
let copy_input = $("#copy-input");
copy_input.val(target_text);
copy_input.removeClass("hide");
copy_input.select();
document.execCommand("copy");
copy_input.addClass("hide");
copy_input.val('');
M.toast({html: "Copied to Clipboard"})
});
}
function hide_sidenav() {
$("#main-sidenav").addClass('hide');
$("#main.main-full").css('padding-left', 0);
$("#show-sidenav").removeClass('hide');
localStorage.setItem('sidenav_hidden', 'true');
}
function no_sidebar() {
$("#main-sidenav").remove();
$("#main.main-full").css('padding-left', 0);
$("#no-sidenav").removeClass('hide');
localStorage.setItem('sidenav_hidden', 'no_sidebar');
}
function show_sidenav(){
$("#main-sidenav").removeClass('hide');
$("#main.main-full").css('padding-left', 64);
$("#show-sidenav").addClass('hide');
localStorage.setItem('sidenav_hidden', null);
}
function apply_settings(settings){
// theme
if (settings['user_theme'] != "None" && settings['user_theme'].length > 1) {
console.log(settings['user_theme'].length)
localStorage.setItem('mode', settings['user_theme']);
document.documentElement.setAttribute('data-theme', settings['user_theme']);
} else {
localStorage.setItem('mode', settings['settings_theme']);
document.documentElement.setAttribute('data-theme', settings['settings_theme']);
}
// accent
if (settings['user_accent'] != "None" && settings['user_accent'].length > 1) {
localStorage.setItem('accent', settings['user_accent']);
document.documentElement.setAttribute('data-accent', settings['user_accent']);
} else {
localStorage.setItem('accent', settings['settings_accent']);
document.documentElement.setAttribute('data-accent', settings['settings_accent']);
}
if (settings['settings_sidebar_default'] == "closed"){
localStorage.setItem('sidenav_hidden', 'true');
} else if (settings['settings_sidebar_default'] == "open"){
localStorage.setItem('sidenav_hidden', 'false');
} else if (settings['settings_sidebar_default'] == "no_sidebar"){
localStorage.setItem('sidenav_hidden', 'no_sidebar');
}
if (settings['user_sidebar_default'] == "closed"){
localStorage.setItem('sidenav_hidden', 'true');
} else if (settings['user_sidebar_default'] == "open"){
localStorage.setItem('sidenav_hidden', 'false');
} else if (settings['user_sidebar_default'] == "no_sidebar"){
localStorage.setItem('sidenav_hidden', 'no_sidebar');
}
if (localStorage.getItem('sidenav_hidden') === 'true'){
hide_sidenav();
} else if (localStorage.getItem('sidenav_hidden') === 'no_sidebar'){
no_sidebar();
} else if (settings['user_name'].length < 1) {
no_sidebar();
}
}
//--------------------------------------------------------------------------------------
// Document ready function
//--------------------------------------------------------------------------------------
$(document).ready(function () {
"use strict";
apply_settings({
settings_theme: $("#settings-theme").val(),
settings_accent: $("#settings-accent").val(),
settings_sidebar_default: $("#settings-sidebar_default").val(),
user_name: $("#user-name").val(),
user_theme: $("#user-theme").val(),
user_accent: $("#user-accent").val(),
user_sidebar_default: $("#user-sidebar_default").val(),
});
// INITS
init_select();
$("#update-message-modal").modal({
dismissible: false
});
if ($("#update-message-content").text().length > 1){
$("#update-message-modal").modal('open');
}
$("#update-message-read-btn").on('click', function(e) {
$.ajax({
url: $(this).attr('data-url'),
type: 'GET',
success: function(data){
$("#update-message-modal").modal('close');
}
});
});
$("#hide-sidenav").on('click', function(e) {
hide_sidenav();
});
$("#show-sidenav .material-icons-outlined").on('click', function(e) {
show_sidenav();
});
$( "#show-sidenav" ).draggable({ axis: "y" });
$(".dropdown-trigger").dropdown({
coverTrigger: false,
constrainWidth: false
});
$(".tabs").tabs();
// Fab
$(".fixed-action-btn").floatingActionButton();
$(".fixed-action-btn.horizontal").floatingActionButton({
direction: "left"
});
$(".fixed-action-btn.click-to-toggle").floatingActionButton({
hoverEnabled: false
});
$(".fixed-action-btn.toolbar").floatingActionButton({
toolbarEnabled: true
});
$('.tap-target').tapTarget();
$('.tap-target').tapTarget('open');
// Detect touch screen and enable scrollbar if necessary
function is_touch_device() {
try {
document.createEvent("TouchEvent");
return true;
} catch (e) {
return false;
}
}
if (is_touch_device()) {
$("#nav-mobile").css({
overflow: "auto"
});
}
// mobile sidenav for top-nav layout
$('.top-nav-mobile-sidenav').sidenav({
edge: 'right'
});
// Init collapsible
$(".collapsible").collapsible({
accordion: true,
onOpenStart: function() {
// Removed open class first and add open at collapsible active
$(".collapsible > li.open").removeClass("open");
setTimeout(function() {
$("#slide-out > li.active > a")
.parent()
.addClass("open");
}, 10);
}
});
// Add open class on init
$("#slide-out > li.active > a")
.parent()
.addClass("open");
// Open active menu for multi level
if ($("li.active .collapsible-sub .collapsible").find("a.active").length > 0) {
$("li.active .collapsible-sub .collapsible")
.find("a.active")
.closest("div.collapsible-body")
.show();
$("li.active .collapsible-sub .collapsible")
.find("a.active")
.closest("div.collapsible-body")
.closest("li")
.addClass("active");
}
// Auto Scroll menu to the active item
var position;
if (
$(".sidenav-main li a.active")
.parent("li.active")
.parent("ul.collapsible-sub").length > 0
) {
position = $(".sidenav-main li a.active")
.parent("li.active")
.parent("ul.collapsible-sub")
.position();
} else {
position = $(".sidenav-main li a.active")
.parent("li.active")
.position();
}
setTimeout(function() {
if (position !== undefined) {
$(".sidenav-main ul")
.stop()
.animate({ scrollTop: position.top - 300 }, 300);
}
}, 300);
$("#slide-out").sidenav();
// Collapsible navigation menu
$(".nav-collapsible .navbar-toggler").click(function() {
// Toggle navigation expan and collapse on radio click
if ($(".sidenav-main").hasClass("nav-expanded") && !$(".sidenav-main").hasClass("nav-lock")) {
$(".sidenav-main").toggleClass("nav-expanded");
$("#main").toggleClass("main-full");
} else {
$("#main").toggleClass("main-full");
}
// Set navigation lock / unlock with radio icon
if (
$(this)
.children()
.text() == "radio_button_unchecked"
) {
$(this)
.children()
.text("radio_button_checked");
$(".sidenav-main").addClass("nav-lock");
$(".navbar .nav-collapsible").addClass("sideNav-lock");
} else {
$(this)
.children()
.text("radio_button_unchecked");
$(".sidenav-main").removeClass("nav-lock");
$(".navbar .nav-collapsible").removeClass("sideNav-lock");
}
});
// Expand navigation on mouseenter event
$(".sidenav-main.nav-collapsible, .navbar .brand-sidebar").mouseenter(function() {
if (!$(".sidenav-main.nav-collapsible").hasClass("nav-lock")) {
$(".sidenav-main.nav-collapsible, .navbar .nav-collapsible")
.addClass("nav-expanded")
.removeClass("nav-collapsed");
$("#slide-out > li.close > a")
.parent()
.addClass("open")
.removeClass("close");
setTimeout(function() {
// Open only if collapsible have the children
if ($(".collapsible .open").children().length > 1) {
$(".collapsible").collapsible("open", $(".collapsible .open").index());
}
}, 100);
}
});
// Collapse navigation on mouseleave event
$(".sidenav-main.nav-collapsible, .navbar .brand-sidebar").mouseleave(function() {
if (!$(".sidenav-main.nav-collapsible").hasClass("nav-lock")) {
var openLength = $(".collapsible .open").children().length;
$(".sidenav-main.nav-collapsible, .navbar .nav-collapsible")
.addClass("nav-collapsed")
.removeClass("nav-expanded");
$("#slide-out > li.open > a")
.parent()
.addClass("close")
.removeClass("open");
setTimeout(function() {
// Open only if collapsible have the children
if (openLength > 1) {
$(".collapsible").collapsible("close", $(".collapsible .close").index());
}
}, 100);
}
});
// make jquery contains selector case unaware
jQuery.expr[':'].contains = function(a, i, m) {
return jQuery(a).text().toUpperCase()
.indexOf(m[3].toUpperCase()) >= 0;
};
});

View File

@ -0,0 +1,81 @@
// CARD EDITOR
$("#card-editor-open-btn").on('click', function(e) {
$('#main-sidenav').sidenav('close');
});
$(".show-card-editor-data-sources-table").on('click', function(e) {
$("#card-editor-cards-table").addClass('hide');
$("#card-editor-data-sources-table").removeClass('hide');
});
$(".show-card-editor-cards-table").on('click', function(e) {
$("#card-editor-cards-table").removeClass('hide');
$("#card-editor-data-sources-table").addClass('hide');
});
$("#card-editor-add-btn").dropdown({
container: '#card-editor-sidenav',
constrainWidth: false,
onOpenStart: function () {
$(".card-editor-add-dropdown-overlay").removeClass('hide');
},
onCloseStart: function () {
$(".card-editor-add-dropdown-overlay").addClass('hide');
}
});
$("#card-editor-data-source-add-btn").dropdown({
container: '#card-editor-sidenav',
constrainWidth: false,
onOpenStart: function () {
$(".card-editor-add-dropdown-overlay").removeClass('hide');
},
onCloseStart: function () {
$(".card-editor-add-dropdown-overlay").addClass('hide');
}
});
$(".card-editor-app-row").on('click', function(e) {
var form = $("#card-editor-form-container")
var table = $("#card-editor-cards-table")
$.ajax({
url: $(this).attr('data-url'),
type: 'GET',
data: {app_id: $(this).attr('data-id')},
success: function(data){
after_ini_form_ajax_load(form, table, data);
}
});
});
$(".card-editor-data-source-row").on('click', function(e) {
var form = $("#card-editor-data-sources-form-container")
var table = $("#card-editor-data-sources-table")
$.ajax({
url: $(this).attr('data-url'),
type: 'GET',
data: {ds_id: $(this).attr('data-id')},
success: function(data){
after_ini_form_ajax_load(form, table, data);
}
});
});
$(".card-editor-add-dropdown-a").on('click', function(e) {
var form = $("#card-editor-form-container")
var table = $("#card-editor-cards-table")
$.ajax({
url: $("#card-editor-add-dropdown").attr('data-url'),
type: 'GET',
data: {type: $(this).attr('data-type')},
success: function(data){
after_ini_form_ajax_load(form, table, data);
}
});
});
$("#card-editor-add-new-ds-btn").on('click', function(e) {
var form = $("#card-editor-data-sources-form-container")
var table = $("#card-editor-data-sources-table")
$.ajax({
url: $(this).attr('data-url'),
type: 'GET',
success: function(data){
after_ini_form_ajax_load(form, table, data);
}
});
});

View File

@ -0,0 +1,46 @@
sleep(500).then(() => {
init_codemirror('properties');
$("#save-config-btn").on('click', function(e) {
$.ajax({
url: $(this).attr('data-url'),
type: 'POST',
data: {config: config_textarea_codemirror.getValue()},
success: function(data){
if (data.data.msg === "success"){
M.toast({html: 'Config applied successfully'});
$("#config-editor-error-div").closest('.col').addClass('hide');
fetch_settings();
load_apps();
load_card_editor();
load_settings_editor();
} else {
$("#config-editor-error-div").empty();
$("#config-editor-error-div").closest('.col').removeClass('hide');
$("#config-editor-error-div").append(data.data.msg);
}
}
});
});
var ctrlDown = false;
var saved = false;
$("#config-editor-container").keydown(function( e ) {
if (e.key === 'Control') {
ctrlDown = true;
}
if (e.key === 's' && ctrlDown && !saved) {
e.preventDefault(); // prevent save-as-webpage popup
saved = true;
$("#save-config-btn").trigger("click")
}
});
$("#config-editor-container").keyup(function( e ) {
if (e.key === 'Control') {
ctrlDown = false;
}
if (e.key === 's' && saved) {
saved = false;
}
});
});

View File

@ -0,0 +1,647 @@
const sleep = (milliseconds) => {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}
function js_Load() {
document.body.style.visibility = 'visible';
}
function updateTabIndicator(){
sleep(250).then(() => {
$(".tabs").tabs('updateTabIndicator');
});
}
function init_select(){
$('select').formSelect({
dropdownOptions:{
container: document.body,
constrainWidth: true,
}
});
$('input').each(function(index, el) {
if ($(this).attr('data-autocomplete-options')){
let options_list = $(this).attr('data-autocomplete-options').split(',');
let options_dict = options_list.map(x => ({'key': x, 'val': null}));
options_dict = options_dict.reduce(function(map, obj) {
map[obj.key] = obj.val;
return map;
}, {});
$(this).autocomplete({
data: options_dict,
dropdownOptions:{
container: document.body,
}
});
if ($(this).attr('data-auto-only') === 'true') {
$(this).on('blur', function(e) {
if (options_list.includes($(this).val()) === false) {
$(this).val('');
}
});
}
}
});
}
// function animateCSS(el, animationName, speed, callback) {
// el.addClass(`animated ${animationName} ${speed}`);
//
// function handleAnimationEnd() {
// el.removeClass(`animated ${animationName} ${speed}`);
// el.off("animationend");
//
// if (typeof callback === 'function') callback()
// }
//
// el.on("animationend", function () {
// handleAnimationEnd();
// })
// }
function init_copy_btn(parent_class){
$(".copy-btn").on('click', function(e) {
let target_text = $(this).closest(parent_class).find('.copy-target').text();
let copy_input = $("#copy-input");
copy_input.val(target_text);
copy_input.removeClass("hide");
copy_input.select();
document.execCommand("copy");
copy_input.addClass("hide");
copy_input.val('');
M.toast({html: "Copied to Clipboard"})
});
}
function fetch_settings() {
$.ajax({
url: $("#settings-data").attr('data-url'),
type: 'GET',
success: function(data){
$("#settings-data-container").empty();
$("#settings-data-container").append(data);
apply_settings();
}
});
}
function apply_settings(){
// Get settings data from inputs
var settings_background = $("#settings-background").val();
var settings_theme = $("#settings-theme").val();
var settings_accent = $("#settings-accent").val();
var user_background = $("#user-background").val();
var user_theme = $("#user-theme").val();
var user_accent = $("#user-accent").val();
// background
var bg_to_set = ""
if (user_background != "None" && user_background.length > 0){
bg_to_set = user_background
} else if (settings_background != "None" && settings_background.length > 0){
bg_to_set = settings_background
} else {
bg_to_set = 'none'
}
if (bg_to_set.startsWith('#') || bg_to_set.startsWith('var(') ){
$('body').css("background-color", bg_to_set);
$('body').css("background-image", 'unset');
} else if (bg_to_set.toLowerCase() == 'none') {
$('body').css("background-color", 'var(--theme-background)');
$('body').css("background-image", 'unset');
} else {
$('body').css("background-color", 'unset');
$('body').css("background-image", `url("${bg_to_set}")`);
}
// theme
if (user_theme != "None" && user_theme.length > 1) {
localStorage.setItem('mode', user_theme);
document.documentElement.setAttribute('data-theme', user_theme);
} else {
localStorage.setItem('mode', settings_theme);
document.documentElement.setAttribute('data-theme', settings_theme);
}
// accent
if (user_accent != "None" && user_accent.length > 1) {
localStorage.setItem('accent', user_accent);
document.documentElement.setAttribute('data-accent', user_accent);
} else {
localStorage.setItem('accent', settings_accent);
document.documentElement.setAttribute('data-accent', settings_accent);
}
}
function set_navbar_toggle_pos(){
let loc_y = parseInt(localStorage.getItem('sidenav-toggle-loc-y'));
let avail_height = $(window).height() - 146;
if (loc_y != undefined){
if (loc_y > avail_height){
loc_y = avail_height
}
$("#sidenav-toggle-svg-container").css({'top': loc_y});
} else {
$("#sidenav-toggle-svg-container").css({'top': avail_height});
}
$("#sidenav-toggle-svg-container").removeClass('hide');
}
// MODULE SIDENAVS
function load_card_editor() {
$("#card-editor-sidenav").sidenav({
edge: 'left',
inDuration: 350,
outDuration: 300,
preventScrolling: false,
onOpenStart: function () {
$("#settings-editor-sidenav").sidenav('close');
$("#config-editor-sidenav").sidenav('close');
$("#main-sidenav").sidenav('close');
}
});
$.ajax({
url: $("#card-editor-container").attr('data-url'),
type: 'GET',
success: function(data){
if (data.data != undefined){
M.toast({html: data.data.msg, classes: "theme-failure"})
}
$("#card-editor-container").empty();
$("#card-editor-container").append(data);
}
});
}
function reset_config_editor(){
$("#config-card-title").text("Config.ini");
config_textarea_codemirror.setValue($("#config-editor-config-data").val());
config_textarea_codemirror.toTextArea();
init_codemirror('properties');
}
function load_config_editor() {
$("#config-editor-sidenav").sidenav({
edge: 'left',
inDuration: 350,
outDuration: 300,
preventScrolling: true,
draggable: false,
onOpenStart: function () {
$("#settings-editor-sidenav").sidenav('close');
$("#card-editor-sidenav").sidenav('close');
$("#main-sidenav").sidenav('close');
},
});
$.ajax({
url: $("#config-editor-container").attr('data-url'),
type: 'GET',
success: function(data){
if (data.data != undefined){
M.toast({html: data.data.msg, classes: "theme-failure"})
}
$("#config-editor-container").empty();
$("#config-editor-container").append(data);
}
});
}
var config_textarea_codemirror = ""
function init_codemirror(mode) {
config_textarea_codemirror = CodeMirror.fromTextArea(document.getElementById("config-textarea"), {
lineNumbers: true,
mode: mode,
theme: 'dashmachine',
scrollbarStyle: null,
});
}
function load_settings_editor() {
$("#settings-editor-sidenav").sidenav({
edge: 'left',
inDuration: 350,
outDuration: 300,
preventScrolling: false,
draggable: true,
onOpenStart: function () {
$("#config-editor-sidenav").sidenav('close');
$("#card-editor-sidenav").sidenav('close');
$("#main-sidenav").sidenav('close');
}
});
$.ajax({
url: $("#settings-editor-container").attr('data-url'),
type: 'GET',
success: function(data){
if (data.data != undefined){
M.toast({html: data.data.msg, classes: "theme-failure"})
}
$("#settings-editor-container").empty();
$("#settings-editor-container").append(data);
}
});
}
function load_apps(){
var home_url = $("#home-cards-container").attr("data-url");
if (home_url != undefined){
$.ajax({
url: home_url,
type: 'GET',
success: function(data){
if (data.data != undefined){
M.toast({html: data.data.msg, classes: "theme-failure"})
}
var container = $("#home-cards-container")
container.fadeOut(300);
container.empty();
container.append(data);
init_home_cards();
container.fadeIn(400);
}
});
}
$.ajax({
url: $("#sidenav-cards-container").attr("data-url"),
type: 'GET',
success: function(data){
if (data.data != undefined){
M.toast({html: data.data.msg, classes: "theme-failure"})
}
var container = $("#sidenav-cards-container")
container.fadeOut(300);
container.empty();
container.append(data);
container.fadeIn(400);
}
});
}
function toggle_tag_expand(el) {
if (el.attr("data-expanded") == "true"){
el.attr("data-expanded", "false");
el.text('keyboard_arrow_down');
var tag_row = el.closest('.tag-group').find('.tag-apps-row')
tag_row.addClass('hide');
} else {
el.attr("data-expanded", "true");
el.text('keyboard_arrow_up');
var tag_row = el.closest('.tag-group').find('.tag-apps-row')
tag_row.removeClass('hide');
}
var x = 0
$(".toggle-tag-expand-btn").each(function(e) {
if ($(this).attr("data-expanded") == "true") {
x = x + 1
}
});
if (x > 0) {
$(".toggle-tag-expand-all-btn").text('unfold_less');
} else {
$(".toggle-tag-expand-all-btn").text('unfold_more');
}
}
function hide_empty_tag_groups() {
$(".tag-group").each(function(i, e) {
var x = 0
$(this).find('.app-card').each(function(i, e) {
if ($(this).hasClass("hide") === false){
x = x + 1
}
});
if (x === 0){
$(this).addClass('hide');
} else {
$(this).removeClass('hide');
}
});
}
//--------------------------------------------------------------------------------------
// Document ready function
//--------------------------------------------------------------------------------------
$(document).ready(function () {
// var redirect_url = $(".access-group-redirect-url").val()
// if (redirect_url != undefined){
// $(location).attr("href", redirect_url);
// }
// console.log($(".access-group-redirect-url").val())
set_navbar_toggle_pos();
"use strict";
apply_settings();
// INITS
init_select();
$("#update-message-modal").modal({
dismissible: false
});
if ($("#update-message-content").text().length > 1){
$("#update-message-modal").modal('open');
}
$("#update-message-read-btn").on('click', function(e) {
$.ajax({
url: $(this).attr('data-url'),
type: 'GET',
success: function(data){
$("#update-message-modal").modal('close');
}
});
});
$(".tabs").tabs();
// Fab
$(".fixed-action-btn").floatingActionButton();
$(".fixed-action-btn.horizontal").floatingActionButton({
direction: "left"
});
$(".fixed-action-btn.click-to-toggle").floatingActionButton({
hoverEnabled: false
});
$(".fixed-action-btn.toolbar").floatingActionButton({
toolbarEnabled: true
});
$('.tap-target').tapTarget();
$('.tap-target').tapTarget('open');
// Detect touch screen and enable scrollbar if necessary
function is_touch_device() {
try {
document.createEvent("TouchEvent");
return true;
} catch (e) {
return false;
}
}
if (is_touch_device()) {
$("#nav-mobile").css({
overflow: "auto"
});
}
// Init collapsible
$(".collapsible").collapsible({
accordion: true,
onOpenStart: function() {
// Removed open class first and add open at collapsible active
$(".collapsible > li.open").removeClass("open");
setTimeout(function() {
$("#slide-out > li.active > a")
.parent()
.addClass("open");
}, 10);
}
});
// make jquery contains selector case unaware
jQuery.expr[':'].contains = function(a, i, m) {
return jQuery(a).text().toUpperCase()
.indexOf(m[3].toUpperCase()) >= 0;
};
// MAIN SIDENAV
$('#main-sidenav').sidenav({
edge: 'left',
draggable: true,
inDuration: 350,
outDuration: 300,
preventScrolling: false,
onCloseStart: function () {
$("#sidenav-toggle-btn .toggler").attr("data-open", "false");
$("#sidenav-toggle-btn .toggler").text('list');
}
});
var cursorInPage = false;
$(window).on('mouseout', function() {
cursorInPage = false;
});
$(window).on('mouseover', function() {
cursorInPage = true;
});
$('#main-sidenav').on('mouseleave', function(e) {
sleep(100).then(() => {
if (cursorInPage == true) {
$("#main-sidenav").sidenav('close');
}
});
});
$("#sidenav-expand-area-svg").on('mouseenter', function(e) {
$("#main-sidenav").sidenav('open');
});
//
$("#sidenav-toggle-svg-container").draggable({
axis: 'y',
containment: "window",
iframeFix: true,
});
$("#sidenav-toggle-svg-container").on('dragstop', function(event, ui) {
localStorage.setItem('sidenav-toggle-loc-y', ui.position.top);
});
$(window).on('resize', function(e) {
set_navbar_toggle_pos();
$("#card-editor-add-btn").dropdown('recalculateDimensions');
});
$("#toggle-user-theme-btn").on('click', function(e) {
var icon_btn = $(this).find('.icon-btn');
$.ajax({
url: $(this).attr('data-url'),
type: 'GET',
data: {id: $(this).attr("data-user_id"), current_status: icon_btn.text()},
success: function(data){
fetch_settings();
if (icon_btn.text() == "toggle_on"){
icon_btn.text('toggle_off');
icon_btn.removeClass('theme-primary-text');
icon_btn.addClass('theme-secondary-text');
} else {
icon_btn.text('toggle_on');
icon_btn.removeClass('theme-secondary-text');
icon_btn.addClass('theme-primary-text');
}
}
});
});
// ACTION BARS
var action_providers = {}
$(".action-provider-span").each(function(e) {
action_providers[`!${$(this).attr("data-macro")} - ${$(this).attr("data-name")}`] = null
});
$(".filter-tags-dropdown-trigger").dropdown({
constrainWidth: false,
alignment: 'right',
coverTrigger: false,
closeOnClick: false
});
$(".filter-tags-dropdown-a").on('click', function(e) {
var el = $(this);
$('.filter-tags-dropdown').find('input').each(function(e) {
if ($(this).attr("data-name") == el.find('input').attr("data-name")){
if ($(this).prop('checked') == true){
$(this).prop('checked', false);
} else {
$(this).prop('checked', true);
}
}
});
if (el.hasClass('show-all')){
$('.filter-tags-dropdown').find('input').each(function(e) {
$(this).prop('checked', false);
});
}
var selected_tags = [];
el.closest('.filter-tags-dropdown').find('input').each(function(e) {
if ($(this).prop('checked') == true){
selected_tags.push($(this).attr("data-name"));
}
});
$(".tag-group").each(function(i, e) {
var tag_group = $(this);
if (selected_tags.length < 1){
tag_group.removeClass('filtered');
if (tag_group.find('.toggle-tag-expand-btn').attr("data-expanded") == "false"){
toggle_tag_expand(tag_group.find('.toggle-tag-expand-btn'));
}
} else {
tag_group.find('.toggle-tag-expand-btn').each(function(e) {
$(this).attr("data-expanded", "false");
toggle_tag_expand($(this));
});
$.each(selected_tags, function(i, e) {
if (tag_group.attr("data-tag").indexOf(e) > -1) {
tag_group.removeClass('filtered');
return false;
} else {
tag_group.addClass('filtered');
}
});
}
});
});
$(".action-bar").each(function(e) {
var action_bar = $(this);
action_bar.autocomplete({
data: action_providers,
onAutocomplete: function () {
var cut_val = action_bar.val().slice(0, action_bar.val().indexOf('-'))
action_bar.val(cut_val)
action_bar.focus();
}
});
action_bar.on('keyup', function(e) {
if ($(this).val()[0] != "!"){
var value = ""
if ($(this).val().length > 1){
value = $(this).val().toLowerCase();
}
$(".app-card").each(function(e) {
var x = 0
$(this).find('.searchable').each(function(e) {
if ($(this).text().toLowerCase().indexOf(value) > -1) {
x = x + 1
}
});
if (x > 0){
$(this).removeClass('hide');
} else {
$(this).addClass('hide');
}
});
$(".tag-group").each(function(i, e) {
var x = 0
$(this).find('.app-card').each(function(i, e) {
if ($(this).hasClass("hide") === false){
x = x + 1
}
});
if (x === 0){
$(this).addClass('hide');
} else {
$(this).removeClass('hide');
$(this).find(".toggle-tag-expand-btn").attr('data-expanded', "false")
toggle_tag_expand($(this).find(".toggle-tag-expand-btn"));
}
});
}
});
action_bar.on('keydown', function(i, e) {
if ($(this).val()[0] == "!" && i.which === 13){
var v = $(this).val();
var macro = v.slice(0, v.indexOf(' '));
v = v.replace(`${macro} `, '')
if (v.length > 0){
macro = macro.replace('!', "")
var action = ""
$(".action-provider-span").each(function(e) {
if ($(this).attr("data-macro") == macro){
action = $(this).attr("data-action")
}
});
$.ajax({
url: $(this).attr('data-search-providers-url'),
type: 'GET',
data: {action: action, value: v},
success: function(data){
$(location).attr("href", data);
}
});
}
}
});
});
// TAG EXPAND/COLLAPSE
if ($("#settings-tags_expanded").val() == "False" || $("#user-tags_expanded").val() == "False"){
$(".toggle-tag-expand-btn").each(function(e) {
toggle_tag_expand($(this), false);
});
if ($("#user-tags_expanded").val() == "True"){
$(".toggle-tag-expand-btn").each(function(e) {
toggle_tag_expand($(this));
});
}
}
$(".toggle-tag-expand-all-btn").on('click', function(e) {
if ($(this).text() == "unfold_more") {
$(".toggle-tag-expand-btn").each(function(e) {
toggle_tag_expand($(this));
});
} else {
$(".toggle-tag-expand-btn").each(function(e) {
if ($(this).attr("data-expanded") == "true"){
toggle_tag_expand($(this));
}
});
}
});
});

View File

@ -1,6 +1,3 @@
var d = document.getElementById("dashboard-sidenav");
d.className += " active theme-primary";
function get_data_source(el){
el.html("");
el.closest('.col').find('.data-source-loading').removeClass('hide');
@ -16,46 +13,7 @@ function get_data_source(el){
});
}
$( document ).ready(function() {
$(".tooltipped").tooltip();
$("#apps-filter").on('keyup', function(e) {
$(".toggle-tag-expand-btn").each(function(e) {
if ($(this).attr("data-expanded") == 'false'){
$(this)[0].click();
}
});
var value = $(this).val().toLowerCase();
$(".app-card").each(function(e) {
var x = 0
$(this).find('.searchable').each(function(e) {
if ($(this).text().toLowerCase().indexOf(value) > -1) {
x = x + 1
}
});
if (x > 0){
$(this).removeClass('hide');
} else {
$(this).addClass('hide');
}
});
$(".tag-group").each(function(i, e) {
var x = 0
$(this).find('.app-card').each(function(i, e) {
if ($(this).hasClass("hide") === false){
x = x + 1
}
});
if (x === 0){
$(this).addClass('hide');
} else {
$(this).removeClass('hide');
}
});
});
function init_home_cards(){
$(".data-source-container").each(function(e) {
get_data_source($(this));
});
@ -67,66 +25,76 @@ $( document ).ready(function() {
});
});
$("#tags-select").on('change', function(e) {
var value = $(this).val();
$(".tag-group").each(function(i, e) {
if ($(this).find('.toggle-tag-expand-btn').attr("data-expanded") == "false"){
$(this).find('.toggle-tag-expand-btn')[0].click();
}
if ($(this).attr("data-tag").indexOf(value) > -1 || value === "All tags") {
$(this).removeClass('filtered');
} else {
$(this).addClass('filtered');
}
});
});
$(".toggle-tag-expand-btn").on('click', function(e) {
if ($(this).attr("data-expanded") == "true"){
$(this).attr("data-expanded", "false");
$(this).text('keyboard_arrow_down');
$(this).closest('.tag-group').find('.tag-apps-row').addClass('hide');
} else {
$(this).attr("data-expanded", "true");
$(this).text('keyboard_arrow_up');
$(this).closest('.tag-group').find('.tag-apps-row').removeClass('hide');
}
var x = 0
$(".tag-group-btn").off('click');
$(".tag-group-btn").on('click', function(e) {
var tag_name = $(this).closest('.tag-group').attr("data-tag");
$(".toggle-tag-expand-btn").each(function(e) {
if ($(this).attr("data-expanded") == "true") {
x = x + 1
if ($(this).closest('.tag-group').attr("data-tag") == tag_name){
toggle_tag_expand($(this));
}
});
if (x > 0) {
$("#toggle-tag-expand-all-btn").text('unfold_less');
} else {
$("#toggle-tag-expand-all-btn").text('unfold_more');
}
});
$("#toggle-tag-expand-all-btn").on('click', function(e) {
if ($(this).text() == "unfold_more") {
$(".toggle-tag-expand-btn").each(function(e) {
$(this)[0].click();
});
} else {
$(".toggle-tag-expand-btn").each(function(e) {
if ($(this).attr("data-expanded") == "true"){
$(this)[0].click();
}
});
}
});
if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) {
$(".expandable-card").addClass('scrollbar');
} else {
$(".expandable-card").on('mouseenter', function(e) {
var tag_row = $(this).closest('.tag-apps-row');
tag_row.css("min-height", tag_row.height());
if ($("#settings-tags_expanded").val() == "False" || $("#user-tags_expanded").val() == "False"){
$(".toggle-tag-expand-btn").each(function(e) {
$(this)[0].click();
var column = $(this).closest('.col')
column.css('min-width', column.width());
column.css('min-height', column.height());
var width = $(this).width();
$(this).css("position", "absolute");
$(this).css("max-height", "unset");
$(this).css("overflow", "auto");
$(this).css("height", "auto");
$(this).css("width", width);
$(this).css("z-index", 888);
});
$(".expandable-card").on('mouseleave', function(e) {
var tag_row = $(this).closest('.tag-apps-row');
tag_row.css("min-height", "unset");
var column = $(this).closest('.col');
column.css('min-width', "unset");
column.css('min-height', "unset");
var width = $(this).width()
$(this).css("position", "relative");
$(this).css("max-height", "146px");
$(this).css("overflow", "hidden");
$(this).css("height", "146px");
$(this).css("width", "unset");
$(this).css("z-index", 1);
});
if ($("#user-tags_expanded").val() == "True"){
$(".toggle-tag-expand-btn").each(function(e) {
$(this)[0].click();
});
}
}
}
$( document ).ready(function() {
$(".tooltipped").tooltip();
init_home_cards();
$(".card-editor-add-from-home-btn").on('mouseenter', function(e) {
$('body')[0].click();
});
$(".card-editor-add-from-home-btn").on('click', function(e) {
$("#card-editor-data-sources-form-container").addClass('hide');
$("#card-editor-data-sources-table").addClass('hide');
$("#card-editor-form-container").removeClass('hide');
$("#card-editor-cards-table").removeClass('hide');
sleep(250).then(() => {
$("#card-editor-add-btn").dropdown('open');
});
});
$('#add-new-app-tap-target').tapTarget();
$('#add-new-app-tap-target').tapTarget('open');
});

View File

@ -0,0 +1,163 @@
// INI FORM
function init_ini_form(container){
container.find(".ini-form-info-dropdown-trigger").each(function(e) {
var ini_dropdown_content_id = $(this).closest('.ini-form-info-dropdown-dropdown-container').find('ul').attr('id');
$(this).dropdown({
constrainWidth: false,
closeOnClick: false,
container: container
});
$(this).attr("data-content-id", ini_dropdown_content_id);
});
container.find(".ini-form").find('input:not(".hide")').each(function(e) {
if ($(this).val() == "None"){
$(this).val("");
}
if ($(this).hasClass('ini-form-subvariable-input')){
} else {
var id_str = $(this).attr("id").replace("ini-form-", "");
if ($("#variable-dict-" + id_str).attr("data-variable") == undefined){
// console.log(id_str + " was hidden")
$(this).closest('.row').next().remove();
$(this).closest('.row').remove();
}
if ($("#variable-dict-" + id_str).attr("data-disabled") == "True"){
$(this).prop('disabled', true);
}
}
});
M.updateTextFields();
container.find(".ini-form-info-dropdown-trigger").each(function(e) {
var ini_dropdown_content = $("#" + $(this).attr('data-content-id'));
var dict_name = $(this).closest('.ini-form-container').attr("data-name");
var variable_name = $(this).attr("data-name");
var variable_dict_div = $("#variable-dict-" + dict_name + "-" + variable_name)
ini_dropdown_content.find('.ini-form-info-variable').text(variable_dict_div.attr("data-variable"));
ini_dropdown_content.find('.ini-form-info-description').text(variable_dict_div.attr("data-description"));
ini_dropdown_content.find('.ini-form-info-default').text(variable_dict_div.attr("data-default"));
ini_dropdown_content.find('.ini-form-info-options').text(variable_dict_div.attr("data-options"));
});
container.find(".ini-form-save-btn").on('click', function(e) {
var form = container.find('.ini-form')
form.find('input').each(function(e) {
$(this).prop('disabled', null);
});
var unchecked = form.find(':checkbox:not(:checked)');
unchecked.each(function() {$(this).val('off').prop('checked', true)});
var formValues = form.serializeArray();
unchecked.each(function() {$(this).prop('checked', false)});
var location = $(this).attr('data-location');
var err_col = form.closest('.ini-form-container').find('.ini-form-error-col');
var err_div = err_col.find('.ini-form-error-div');
$.ajax({
url: $(this).attr('data-url'),
type: 'POST',
data: formValues,
success: function(data){
if (data.data.msg === "success"){
err_col.addClass('hide');
fetch_settings();
load_apps();
load_config_editor();
load_card_editor();
if (location != "settings-editor"){
load_settings_editor();
}
M.toast({html: 'Configuration applied'});
} else {
err_div.empty();
err_col.removeClass('hide');
err_div.append(data.data.msg);
}
}
});
});
// SUBVARIABLES
$(".ini-form-subvariable-input-add-btn").off('click');
$(".ini-form-subvariable-input-add-btn").on('click', function(e) {
var row = $(this).closest('.ini-form-subvariable-set-row');
var card = row.find('.ini-form-subvariable-set-card').first();
card.clone().appendTo(row);
var new_id = Math.floor(Math.random() * 99999) + 10000
card.find('input').each(function(e) {
$(this).val('');
var sliced = $(this).attr('id').slice(0, -5);
$(this).closest('.input-field').find('label').attr('for', sliced + new_id)
$(this).attr('id', sliced + new_id);
$(this).attr('name', sliced + new_id);
M.updateTextFields();
});
init_ini_form(container);
});
$(".ini-form-subvariable-delete-btn").off('click');
$(".ini-form-subvariable-delete-btn").on('click', function(e) {
var row = $(this).closest('.ini-form-subvariable-set-row');
var x = 0
row.find('.ini-form-subvariable-set-card').each(function(e) {
x = x + 1
});
if (x > 1){
$(this).closest('.ini-form-subvariable-set-card').remove();
init_ini_form(container);
}
});
// TEMPLATE APPS
var template_autocomplete_options = {};
container.find('.card-editor-app-template-options').find('div').each(function(e) {
template_autocomplete_options[$(this).attr("data-name")] = $(this).attr("data-icon")
});
var template_searchbar = container.find('.card-editor-app-template-search')
template_searchbar.autocomplete({
data: template_autocomplete_options,
minLength: 0,
onAutocomplete: function () {
var template_info = $("#app-template-info-" + template_searchbar.val().replace(/ /g, "-"));
$("#ini-form-App-name").val(template_info.attr("data-name"));
$("#ini-form-App-prefix").val(template_info.attr("data-prefix"));
$("#ini-form-App-url").val(template_info.attr("data-url"));
$("#ini-form-App-sidebar_icon").val(template_info.attr("data-sidebar_icon"));
$("#ini-form-App-description").val(template_info.attr("data-description"));
$("#ini-form-App-open_in").val(template_info.attr("data-open_in"));
$("#ini-form-App-icon").val(template_info.attr("data-icon"));
template_searchbar.val('');
M.updateTextFields();
}
});
// DATA SOURCE SELECT
$("#ini-form-new-ds-selector").on('change', function(e) {
var form = $("#card-editor-data-sources-form-container")
var table = $("#card-editor-data-sources-table")
$.ajax({
url: $(this).attr('data-url'),
type: 'GET',
data: {platform: $(this).val()},
success: function(data){
after_ini_form_ajax_load(form, table, data);
}
});
});
init_select();
}
function after_ini_form_ajax_load(form, table, data) {
form.removeClass('hide');
table.addClass('hide');
form.empty();
form.append(data);
init_ini_form(form);
form.find(".ini-form-cancel-btn").on('click', function(e) {
table.removeClass('hide');
form.addClass('hide');
});
form.find('.ini-form-cancel-btn').removeClass('hide');
}

View File

@ -0,0 +1,77 @@
$( document ).ready(function() {
$(".tabs").tabs();
init_ini_form($("#settings-editor-settings-form-container"));
init_select();
initTCdrop('#images-tcdrop');
$(".settings-editor-ag-row").on('click', function(e) {
var table = $("#settings-editor-ag-table")
var form = $("#settings-editor-ag-form-container")
$.ajax({
url: $(this).attr('data-url'),
type: 'GET',
data: {ag_id: $(this).attr('data-id')},
success: function(data){
after_ini_form_ajax_load(form, table, data);
}
});
});
$(".settings-editor-user-row").on('click', function(e) {
var table = $("#settings-editor-user-table")
var form = $("#settings-editor-user-form-container")
$.ajax({
url: $(this).attr('data-url'),
type: 'GET',
data: {user_id: $(this).attr('data-id')},
success: function(data){
after_ini_form_ajax_load(form, table, data);
}
});
});
$("#save-images-btn").on('click', function(e) {
$("#add-images-input").val(tcdrop_files['images-tcdrop'].toString());
$.ajax({
url: $(this).attr('data-url'),
type: 'POST',
data: $("#add-images-form").serialize(),
success: function(data){
$("#add-images-form").trigger('reset');
$("#files-div").empty();
$("#files-div").append(data);
tcdropResetAll();
}
});
});
$(".settings-editor-add-ag-btn").on('click', function(e) {
var table = $("#settings-editor-ag-table")
var form = $("#settings-editor-ag-form-container")
$.ajax({
url: $(this).attr('data-url'),
type: 'GET',
data: {new: "True"},
success: function(data){
after_ini_form_ajax_load(form, table, data);
}
});
});
$(".settings-editor-add-user-btn").on('click', function(e) {
var table = $("#settings-editor-user-table")
var form = $("#settings-editor-user-form-container")
$.ajax({
url: $(this).attr('data-url'),
type: 'GET',
data: {new: "True"},
success: function(data){
after_ini_form_ajax_load(form, table, data);
}
});
});
});

View File

@ -1,84 +0,0 @@
var d = document.getElementById("settings-sidenav");
d.className += " active theme-primary";
$( document ).ready(function() {
$("#settings-readme table").addClass('responsive-table');
initTCdrop('#images-tcdrop');
$("#user-modal").modal({
onCloseEnd: function () {
$("#edit-user-form").trigger('reset');
}
});
$("#save-config-btn").on('click', function(e) {
$.ajax({
url: $(this).attr('data-url'),
type: 'POST',
data: $("#config-form").serialize(),
success: function(data){
if (data.data.msg === "success"){
M.toast({html: 'Config applied successfully'});
location.reload(true);
} else {
M.toast({html: data.data.msg, classes: "theme-failure"});
}
}
});
});
$("#save-images-btn").on('click', function(e) {
$("#add-images-input").val(tcdrop_files['images-tcdrop'].toString());
$.ajax({
url: $(this).attr('data-url'),
type: 'POST',
data: $("#add-images-form").serialize(),
success: function(data){
$("#add-images-form").trigger('reset');
$("#files-div").empty();
$("#files-div").append(data);
tcdropResetAll();
}
});
});
var template_apps = $("#templates-filter").attr("data-template_apps").split(',');
var autocomplete_data = {}
$.each(template_apps, function(i, e) {
autocomplete_data[e.split('&&')[0]] = e.split('&&')[1]
});
$("#templates-filter").autocomplete({
limit: 16,
data: autocomplete_data,
onAutocomplete: function () {
$.ajax({
url: $("#templates-filter").attr('data-url'),
type: 'GET',
data: {name: $("#templates-filter").val()},
success: function(data){
$("#template-div").empty();
$("#template-div").append(data);
}
});
}
});
$("#save-user-btn").on('click', function(e) {
$.ajax({
url: $(this).attr('data-url'),
type: 'POST',
data: $("#edit-user-form").serialize(),
success: function(data){
if (data.data.err !== 'success'){
M.toast({html: data.data.err, classes: 'theme-failure'});
} else {
$("#users-div").empty();
$("#users-div").append(data.data.html);
$("#user-modal").modal('close');
M.toast({html: 'User saved'});
}
}
});
});
});

View File

@ -0,0 +1,11 @@
/*!
* jQuery UI Touch Punch 0.2.3
*
* Copyright 20112014, Dave Furfero
* Dual licensed under the MIT or GPL Version 2 licenses.
*
* Depends:
* jquery.ui.widget.js
* jquery.ui.mouse.js
*/
!function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery);

View File

@ -0,0 +1,7 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8

View File

@ -0,0 +1,8 @@
*.txt text eol=lf
*.js text eol=lf
*.html text eol=lf
*.md text eol=lf
*.json text eol=lf
*.yml text eol=lf
*.css text eol=lf
*.svg text eol=lf

View File

@ -0,0 +1,14 @@
/node_modules
/demo
/doc
/test
/test*.html
/index.html
/mode/*/*test.js
/mode/*/*.html
/mode/index.html
.*
/bin/authors.sh
/bin/lint
/bin/release
/bin/upload-release.js

View File

@ -0,0 +1,5 @@
language: node_js
node_js:
- stable
sudo: false
cache: npm

View File

@ -0,0 +1,859 @@
List of CodeMirror contributors. Updated before every release.
4oo4
4r2r
Aaron Brooks
Abdelouahab
Abdussalam Abdurrahman
Abe Fettig
Abhishek Gahlot
Adam Ahmed
Adam King
Adam Particka
adanlobato
Adán Lobato
Aditya Toshniwal
Adrian Aichner
Adrian Heine
Adrien Bertrand
aeroson
Ahmad Amireh
Ahmad M. Zawawi
ahoward
Akeksandr Motsjonov
Alasdair Smith
AlbertHilb
Alberto González Palomo
Alberto Pose
Albert Xing
Alexander Pavlov
Alexander Schepanovski
Alexander Shvets
Alexander Solovyov
Alexandre Bique
alexey-k
Alex Piggott
Aliaksei Chapyzhenka
Allen Sarkisyan
Ami Fischman
Amin Shali
Amin Ullah Khan
amshali@google.com
Amsul
amuntean
Amy
Ananya Sen
anaran
AndersMad
Anders Nawroth
Anderson Mesquita
Anders Wåglund
Andrea G
Andreas Reischuck
Andres Taylor
Andre von Houck
Andrew Cheng
Andrew Dassonville
Andrey Fedorov
Andrey Klyuchnikov
Andrey Lushnikov
Andrey Shchekin
Andy Joslin
Andy Kimball
Andy Li
Angelo
angelozerr
angelo.zerr@gmail.com
Ankit
Ankit Ahuja
Ansel Santosa
Anthony Dugois
anthonygego
Anthony Gégo
Anthony Grimes
Anton Kovalyov
antosarho
Apollo Zhu
AQNOUCH Mohammed
Aram Shatakhtsyan
areos
Arnab Bose
Arnoud Buzing
Arsène von Wyss
Arthur Müller
Arun Narasani
as3boyan
asolove
atelierbram
AtomicPages LLC
Atul Bhouraskar
Aurelian Oancea
Axel Lewenhaupt
Baptiste Augrain
Barret Rennie
Bartosz Dziewoński
Basarat Ali Syed
Bastian Müller
belhaj
Bem Jones-Bey
benbro
Beni Cherniavsky-Paskin
Benjamin DeCoste
Benjamin Young
Ben Keen
Ben Miller
Ben Mosher
Bernhard Sirlinger
Bert Chang
Bharad
BigBlueHat
Billy Moon
binny
Bjorn Hansen
B Krishna Chaitanya
Blaine G
blukat29
Bo
boomyjee
Bo Peng
borawjm
Brad Metcalf
Brandon Frohs
Brandon Wamboldt
Bret Little
Brett Zamir
Brian Grinstead
Brian Sletten
brrd
Bruce Mitchener
Bruno Logerfo
Bryan Gin-ge Chen
Bryan Massoth
Caitlin Potter
Calin Barbat
callodacity
Camilo Roca
Casey Klebba
César González Íñiguez
Chad Jolly
Chandra Sekhar Pydi
Charles Skelton
Cheah Chu Yeow
Chhekur
Chris Colborne
Chris Coyier
Chris Ford
Chris Granger
Chris Houseknecht
Chris Lohfink
Chris Morgan
Chris Reeves
Chris Smith
Christian Gruen
Christian Oyarzun
Christian Petrov
christopherblaser
Christopher Brown
Christopher Kramer
Christopher Mitchell
Christopher Pfohl
Christopher Wallis
Chunliang Lyu
ciaranj
clone-it
clso
CodeAnimal
CodeBitt
coderaiser
Cole R Lawrence
ComFreek
Cristian Prieto
Curran Kelleher
Curtis Gagliardi
dagsta
daines
Dale Jung
Dan Bentley
Dan Heberden
Daniel, Dao Quang Minh
Daniele Di Sarli
Daniel Faust
Daniel Hanggi
Daniel Huigens
Daniel Kesler
Daniel KJ
Daniel Neel
Daniel Parnell
Daniel Thwaites
Danila Malyutin
Danny Yoo
darealshinji
Darius Roberts
databricks-david-lewis
Dave Brondsema
Dave MacLachlan
Dave Myers
David Barnett
David H. Bronke
David Mignot
David Pathakjee
David Rodrigues
David Santana
David Vázquez
David Whittington
deebugger
Deep Thought
Denis Ovsienko
Devin Abbott
Devon Carew
Dick Choi
Diego Fernandez
dignifiedquire
Dimage Sapelkin
dmaclach
Dmitry Kiselyov
domagoj412
Dominator008
Domizio Demichelis
Doug Blank
Doug Wikle
Drew Bratcher
Drew Hintz
Drew Khoury
Drini Cami
Dror BG
Duncan Lilley
duralog
dwelle
eborden
edoroshenko
edsharp
ekhaled
Elisée
elpnt
Emmanuel Schanzer
Enam Mijbah Noor
Eric Allam
Eric Bogard
Erik Demaine
Erik Welander
eustas
Evan Minsk
Fabien Dubosson
Fabien O'Carroll
Fabio Zendhi Nagao
Faiza Alsaied
Fauntleroy
fbuchinger
feizhang365
Felipe Lalanne
Felix Raab
ficristo
Filip Noetzel
Filip Stollár
Filype Pereira
finalfantasia
flack
Florian Felten
Forbes Lindesay
ForbesLindesay
Ford_Lawnmower
Forrest Oliphant
Franco Catena
Frank Seifferth
Frank Wiegand
fraxx001
Fredrik Borg
FUJI Goro (gfx)
Gabriel Gheorghian
Gabriel Horner
Gabriel Nahmias
galambalazs
Gary Sheng
Gautam Mehta
Gavin Douglas
gekkoe
Geordie Hall
George Stephanis
geowarin
Gerard Braad
Gergely Hegykozi
Germain Chazot
Giovanni Calò
Glebov Boris
Glenn Jorde
Glenn Ruehle
goldsmcb
Golevka
Google LLC
Gordon Smith
Grant Skinner
greengiant
Gregory Koberger
Grzegorz Mazur
Guang Li
Guan Gui
Guillaume Massé
Guillaume Massé
guraga
Gustavo Rodrigues
Hakan Tunc
Hanno Fellmann
Hans Engel
Hanzhao Deng
Harald Schilly
Hardest
Harshvardhan Gupta
Hasan Delibaş
Hasan Karahan
Heanes
Hector Oswaldo Caballero
Hein Htat
Hélio
Hendrik Wallbaum
Henrik Haugbølle
Herculano Campos
hidaiy
Hiroyuki Makino
hitsthings
Hocdoc
Hugues Malphettes
Ian Beck
Ian Davies
Ian Dickinson
Ian Rose
Ian Wehrman
Ian Wetherbee
Ice White
ICHIKAWA, Yuji
idleberg
Igor Petruk
ilvalle
Ilya Kharlamov
Ilya Zverev
Ingo Richter
Irakli Gozalishvili
Ivan Kurnosov
Ivoah
Jacob Lee
Jaimin
Jake Peyser
Jakob Miland
Jakub Vrana
Jakub Vrána
James Campos
James Cockshull
James Howard
James Thorne
Jamie Hill
Jamie Morris
Janice Leung
Jan Jongboom
jankeromnes
Jan Keromnes
Jan Odvarko
Jan Schär
Jan T. Sott
Jared Dean
Jared Forsyth
Jared Jacobs
Jason
Jason Barnabe
Jason Grout
Jason Heeris
Jason Johnston
Jason San Jose
Jason Siefken
Jayaprabhakar
Jay Contonio
Jaydeep Solanki
Jean Boussier
Jeff Blaisdell
Jeff Hanke
Jeff Jenkins
jeffkenton
Jeff Pickhardt
jem (graphite)
Jeremy Parmenter
Jim
Jim Avery
jkaplon
JobJob
jochenberger
Jochen Berger
Joel Einbinder
joelpinheiro
joewalsh
Johan Ask
Johannes
John Connor
John-David Dalton
John Engler
John Lees-Miller
John Ryan
John Snelson
John Van Der Loo
Jon Ander Peñalba
Jonas Döbertin
Jonas Helfer
Jonathan Dierksen
Jonathan Hart
Jonathan Malmaud
Jon Gacnik
jongalloway
Jon Malmaud
Jon Sangster
Joo
Joost-Wim Boekesteijn
Joseph Pecoraro
Josh Barnes
Josh Cohen
Josh Soref
Joshua Newman
Josh Watzman
jots
Joy Zhong
jsoojeon
ju1ius
Juan Benavides Romero
Jucovschi Constantin
Juho Vuori
Julien CROUZET
Julien Rebetez
Justin Andresen
Justin Hileman
jwallers@gmail.com
kaniga
karevn
Karol
Kayur Patel
Kazuhito Hokamura
kcwiakala
Kees de Kooter
Kenan Christian Dimas
Ken Newman
ken restivo
Ken Rockot
Kevin Earls
Kevin Kwok
Kevin Muret
Kevin Sawicki
Kevin Ushey
Kier Darby
Klaus Silveira
Koh Zi Han, Cliff
komakino
Konstantin Lopuhin
koops
Kris Ciccarello
ks-ifware
kubelsmieci
kvncp
KwanEsq
Kyle Kelley
KyleMcNutt
LaKing
Lanfei
Lanny
laobubu
Laszlo Vidacs
leaf
leaf corcoran
Lemmon
Leo Baschy
Leonid Khachaturov
Leon Sorokin
Leonya Khachaturov
Liam Newman
Libo Cannici
Lior Goldberg
Lior Shub
LloydMilligan
LM
lochel
Lonnie Abelbeck
Lorenzo Simionato
Lorenzo Stoakes
Louis Mauchet
Luca Fabbri
Luciano Longo
Luciano Santana
Lu Fangjian
Luke Browning
Luke Granger-Brown
Luke Stagner
lynschinzer
M1cha
Madhura Jayaratne
Maksim Lin
Maksym Taran
Malay Majithia
Manideep
Manuel Rego Casasnovas
Marat Dreizin
Marcel Gerber
Marcelo Camargo
Marco Aurélio
Marco Munizaga
Marcus Bointon
Marek Rudnicki
Marijn Haverbeke
Mário Gonçalves
Mario Pietsch
Mark Anderson
Mark Dalgleish
Mark Hamstra
Mark Lentczner
Marko Bonaci
Mark Peace
Markus Bordihn
Markus Olsson
Martin Balek
Martín Gaitán
Martin Hasoň
Martin Hunt
Martin Laine
Martin Zagora
Mason Malone
Mateusz Paprocki
Mathias Bynens
mats cronqvist
Matt Gaide
Matthew Bauer
Matthew Beale
matthewhayes
Matthew Rathbone
Matthew Suozzo
Matthias Bussonnier
Matthias BUSSONNIER
Mattia Astorino
Matt MacPherson
Matt McDonald
Matt Pass
Matt Sacks
mauricio
Maximilian Hils
Maxim Kraev
Max Kirsch
Max Schaefer
Max Wu
Max Xiantu
mbarkhau
McBrainy
mce2
melpon
meshuamam
Metatheos
Micah Dubinko
Michael
Michael Goderbauer
Michael Grey
Michael Kaminsky
Michael Lehenbauer
Michael Wadman
Michael Walker
Michael Zhou
Michal Čihař
Michal Dorner
Michal Kapiczynski
Mighty Guava
Miguel Castillo
mihailik
Mika Andrianarijaona
Mike
Mike Bostock
Mike Brevoort
Mike Diaz
Mike Ivanov
Mike Kadin
Mike Kobit
Milan Szekely
MinRK
Miraculix87
misfo
mkaminsky11
mloginov
Moritz Schubotz (physikerwelt)
Moritz Schwörer
Moshe Wajnberg
mps
ms
mtaran-google
Mu-An ✌️ Chiou
Mu-An Chiou
mzabuawala
Narciso Jaramillo
Nathan Williams
ndr
Neil Anderson
neon-dev
nerbert
NetworkNode
nextrevision
ngn
nguillaumin
Ng Zhi An
Nicholas Bollweg
Nicholas Bollweg (Nick)
NickKolok
Nick Kreeger
Nick Small
Nicolas Chevobbe
Nicolas Kick
Nicolò Ribaudo
Niels van Groningen
nightwing
Nikita Beloglazov
Nikita Vasilyev
Nikolaj Kappler
Nikolay Kostov
nilp0inter
Nils Knappmeier
Nisarg Jhaveri
nlwillia
noragrossman
Norman Rzepka
Nouzbe
Oleksandr Yakovenko
Olivia Ytterbrink
Opender Singh
opl-
Oreoluwa Onatemowo
oscar.lofwenhamn
Oskar Segersvärd
ossdev
overdodactyl
pablo
pabloferz
Pablo Zubieta
paddya
Page
paladox
Panupong Pasupat
paris
Paris
Paris Kasidiaris
Patil Arpith
Patrick Kettner
Patrick Stoica
Patrick Strawderman
Paul Garvin
Paul Ivanov
Paul Masson
Pavel
Pavel Feldman
Pavel Petržela
Pavel Strashkin
Paweł Bartkiewicz
peteguhl
peter
Peter Flynn
peterkroon
Peter Kroon
Philipp A
Philipp Markovics
Philip Stadermann
Pi Delport
Pierre Gerold
Pieter Ouwerkerk
Pontus Melke
prasanthj
Prasanth J
Prayag Verma
prendota
Prendota
Qiang Li
Radek Piórkowski
Rahul
Rahul Anand
ramwin1
Randall Mason
Randy Burden
Randy Edmunds
Randy Luecke
Raphael Amorim
Rasmus Erik Voel Jensen
Rasmus Schultz
raymondf
Raymond Hill
ray ratchup
Ray Ratchup
Remi Nyborg
Renaud Durlin
Reynold Xin
Richard Denton
Richard van der Meer
Richard Z.H. Wang
Rishi Goomar
Robert Brignull
Robert Crossfield
Robert Martin
Roberto Abdelkader Martínez Pérez
robertop23
Robert Plummer
Roman Janusz
Rrandom
Rrrandom
Ruslan Osmanov
rvalavicius
Ryan Pangrle
Ryan Petrello
Ryan Prior
ryu-sato
sabaca
Sam Lee
Sam Rawlins
Samuel Ainsworth
Sam Wilson
sandeepshetty
Sander AKA Redsandro
Sander Verweij
santec
Sarah McAlear and Wenlin Zhang
Sascha Peilicke
Sasha Varlamov
satamas
satchmorun
sathyamoorthi
Saul Costa
S. Chris Colbert
SCLINIC\jdecker
Scott Aikin
Scott Feeney
Scott Goodhew
Seb35
Sebastian Wilzbach
Sebastian Zaha
Seren D
Sergey Goder
Sergey Tselovalnikov
Se-Won Kim
Shane Liesegang
shaund
shaun gilchrist
Shawn A
Shea Bunge
sheopory
Shil S
Shiv Deepak
Shmuel Englard
Shubham Jain
Siamak Mokhtari
silverwind
Simon Edwards
sinkuu
snasa
soliton4
sonson
Sorab Bisht
spastorelli
srajanpaliwal
Stanislav Oaserele
stan-z
Stas Kobzar
Stefan Borsje
Steffen Beyer
Steffen Bruchmann
Steffen Kowalski
Stephane Moore
Stephen Lavelle
Steve Champagne
Steve Hoover
Steve O'Hara
stockiNail
stoskov
Stryder Crown
Stu Kennedy
Sungho Kim
sverweij
Taha Jahangir
takamori
Tako Schotanus
Takuji Shimokawa
Takuya Matsuyama
Tarmil
T. Brandon Ashley
TDaglis
Teja
tel
Tentone
tfjgeorge
Thaddee Tyl
thanasis
TheHowl
themrmax
think
Thomas Brouard
Thomas Dvornik
Thomas Kluyver
thomasmaclean
Thomas Schmid
Tim Alby
Tim Baumann
Timothy Farrell
Timothy Gu
Timothy Hatcher
Tobias Bertelsen
TobiasBg
Todd Berman
Todd Kennedy
Tomas-A
Tomas Varaneckas
Tom Erik Støwer
Tom Klancer
Tom MacWright
Tom McLaughlin
Tony Jian
tophf
Torgeir Thoresen
totalamd
Travis Heppe
Triangle717
Tristan Tarrant
TSUYUSATO Kitsune
Tugrul Elmas
twifkak
Tyler Long
Tyler Makaro
Vadim Dyachenko
Vadzim Ramanenka
Vaibhav Sagar
vamshi.revu
VapidWorx
Vestimir Markov
vf
Victor Bocharsky
Vincent Woo
Volker Mische
vtripolitakis
wdouglashall
Weiyan Shao
wenli
Wes Cossick
Wesley Wiser
Weston Ruter
Will Binns-Smith
Will Dean
William Desportes
William Jamieson
William Stein
Willy
Wojtek Ptak
wonderboyjon
Wu Cheng-Han
Xavier Mendez
Yang Guo
Yassin N. Hassan
YNH Webdev
yoongu
Yunchi Luo
Yuvi Panda
Yvonnick Esnault
Zac Anger
Zachary Dremann
Zeno Rocha
Zhang Hao
Ziv
zoobestik
zziuni
魏鹏刚

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,92 @@
# How to contribute
- [Getting help](#getting-help)
- [Submitting bug reports](#submitting-bug-reports)
- [Contributing code](#contributing-code)
## Getting help
Community discussion, questions, and informal bug reporting is done on the
[discuss.CodeMirror forum](http://discuss.codemirror.net).
## Submitting bug reports
The preferred way to report bugs is to use the
[GitHub issue tracker](http://github.com/codemirror/CodeMirror/issues). Before
reporting a bug, read these pointers.
**Note:** The issue tracker is for *bugs*, not requests for help. Questions
should be asked on the
[discuss.CodeMirror forum](http://discuss.codemirror.net) instead.
### Reporting bugs effectively
- CodeMirror is maintained by volunteers. They don't owe you anything, so be
polite. Reports with an indignant or belligerent tone tend to be moved to the
bottom of the pile.
- Include information about **the browser in which the problem occurred**. Even
if you tested several browsers, and the problem occurred in all of them,
mention this fact in the bug report. Also include browser version numbers and
the operating system that you're on.
- Mention which release of CodeMirror you're using. Preferably, try also with
the current development snapshot, to ensure the problem has not already been
fixed.
- Mention very precisely what went wrong. "X is broken" is not a good bug
report. What did you expect to happen? What happened instead? Describe the
exact steps a maintainer has to take to make the problem occur. We can not
fix something that we can not observe.
- If the problem can not be reproduced in any of the demos included in the
CodeMirror distribution, please provide an HTML document that demonstrates
the problem. The best way to do this is to go to
[jsbin.com](http://jsbin.com/ihunin/edit), enter it there, press save, and
include the resulting link in your bug report.
## Contributing code
Note that we are not accepting any new addons or modes into the main
distribution. If you've written such a module, please distribute it as
a separate NPM package.
- Make sure you have a [GitHub Account](https://github.com/signup/free)
- Fork [CodeMirror](https://github.com/codemirror/CodeMirror/)
([how to fork a repo](https://help.github.com/articles/fork-a-repo))
- Make your changes
- If your changes are easy to test or likely to regress, add tests.
Tests for the core go into `test/test.js`, some modes have their own
test suite under `mode/XXX/test.js`. Feel free to add new test
suites to modes that don't have one yet (be sure to link the new
tests into `test/index.html`).
- Follow the general code style of the rest of the project (see
below). Run `bin/lint` to verify that the linter is happy.
- Make sure all tests pass. Visit `test/index.html` in your browser to
run them.
- Submit a pull request
([how to create a pull request](https://help.github.com/articles/fork-a-repo)).
Don't put more than one feature/fix in a single pull request.
By contributing code to CodeMirror you
- agree to license the contributed code under CodeMirror's [MIT
license](https://codemirror.net/LICENSE).
- confirm that you have the right to contribute and license the code
in question. (Either you hold all rights on the code, or the rights
holder has explicitly granted the right to use it like this,
through a compatible open source license or through a direct
agreement with you.)
### Coding standards
- 2 spaces per indentation level, no tabs.
- Note that the linter (`bin/lint`) which is run after each commit
complains about unused variables and functions. Prefix their names
with an underscore to muffle it.
- CodeMirror does *not* follow JSHint or JSLint prescribed style.
Patches that try to 'fix' code to pass one of these linters will be
unceremoniously discarded.

View File

@ -0,0 +1,21 @@
MIT License
Copyright (C) 2017 by Marijn Haverbeke <marijnh@gmail.com> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,48 @@
# CodeMirror
[![Build Status](https://travis-ci.org/codemirror/CodeMirror.svg)](https://travis-ci.org/codemirror/CodeMirror)
[![NPM version](https://img.shields.io/npm/v/codemirror.svg)](https://www.npmjs.org/package/codemirror)
[![Join the chat at https://gitter.im/codemirror/CodeMirror](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/codemirror/CodeMirror)
CodeMirror is a versatile text editor implemented in JavaScript for
the browser. It is specialized for editing code, and comes with over
100 language modes and various addons that implement more advanced
editing functionality. Every language comes with fully-featured code
and syntax highlighting to help with reading and editing complex code.
A rich programming API and a CSS theming system are available for
customizing CodeMirror to fit your application, and extending it with
new functionality.
You can find more information (and the
[manual](https://codemirror.net/doc/manual.html)) on the [project
page](https://codemirror.net). For questions and discussion, use the
[discussion forum](https://discuss.codemirror.net/).
See
[CONTRIBUTING.md](https://github.com/codemirror/CodeMirror/blob/master/CONTRIBUTING.md)
for contributing guidelines.
The CodeMirror community aims to be welcoming to everybody. We use the
[Contributor Covenant
(1.1)](http://contributor-covenant.org/version/1/1/0/) as our code of
conduct.
### Installation
Either get the [zip file](https://codemirror.net/codemirror.zip) with
the latest version, or make sure you have [Node](https://nodejs.org/)
installed and run:
npm install codemirror
**NOTE**: This is the source repository for the library, and not the
distribution channel. Cloning it is not the recommended way to install
the library, and will in fact not work unless you also run the build
step.
### Quickstart
To build the project, make sure you have Node.js installed (at least version 6)
and then `npm install`. To run, just open `index.html` in your
browser (you don't need to run a webserver). Run the tests with `npm test`.

View File

@ -0,0 +1,209 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
var noOptions = {};
var nonWS = /[^\s\u00a0]/;
var Pos = CodeMirror.Pos;
function firstNonWS(str) {
var found = str.search(nonWS);
return found == -1 ? 0 : found;
}
CodeMirror.commands.toggleComment = function(cm) {
cm.toggleComment();
};
CodeMirror.defineExtension("toggleComment", function(options) {
if (!options) options = noOptions;
var cm = this;
var minLine = Infinity, ranges = this.listSelections(), mode = null;
for (var i = ranges.length - 1; i >= 0; i--) {
var from = ranges[i].from(), to = ranges[i].to();
if (from.line >= minLine) continue;
if (to.line >= minLine) to = Pos(minLine, 0);
minLine = from.line;
if (mode == null) {
if (cm.uncomment(from, to, options)) mode = "un";
else { cm.lineComment(from, to, options); mode = "line"; }
} else if (mode == "un") {
cm.uncomment(from, to, options);
} else {
cm.lineComment(from, to, options);
}
}
});
// Rough heuristic to try and detect lines that are part of multi-line string
function probablyInsideString(cm, pos, line) {
return /\bstring\b/.test(cm.getTokenTypeAt(Pos(pos.line, 0))) && !/^[\'\"\`]/.test(line)
}
function getMode(cm, pos) {
var mode = cm.getMode()
return mode.useInnerComments === false || !mode.innerMode ? mode : cm.getModeAt(pos)
}
CodeMirror.defineExtension("lineComment", function(from, to, options) {
if (!options) options = noOptions;
var self = this, mode = getMode(self, from);
var firstLine = self.getLine(from.line);
if (firstLine == null || probablyInsideString(self, from, firstLine)) return;
var commentString = options.lineComment || mode.lineComment;
if (!commentString) {
if (options.blockCommentStart || mode.blockCommentStart) {
options.fullLines = true;
self.blockComment(from, to, options);
}
return;
}
var end = Math.min(to.ch != 0 || to.line == from.line ? to.line + 1 : to.line, self.lastLine() + 1);
var pad = options.padding == null ? " " : options.padding;
var blankLines = options.commentBlankLines || from.line == to.line;
self.operation(function() {
if (options.indent) {
var baseString = null;
for (var i = from.line; i < end; ++i) {
var line = self.getLine(i);
var whitespace = line.slice(0, firstNonWS(line));
if (baseString == null || baseString.length > whitespace.length) {
baseString = whitespace;
}
}
for (var i = from.line; i < end; ++i) {
var line = self.getLine(i), cut = baseString.length;
if (!blankLines && !nonWS.test(line)) continue;
if (line.slice(0, cut) != baseString) cut = firstNonWS(line);
self.replaceRange(baseString + commentString + pad, Pos(i, 0), Pos(i, cut));
}
} else {
for (var i = from.line; i < end; ++i) {
if (blankLines || nonWS.test(self.getLine(i)))
self.replaceRange(commentString + pad, Pos(i, 0));
}
}
});
});
CodeMirror.defineExtension("blockComment", function(from, to, options) {
if (!options) options = noOptions;
var self = this, mode = getMode(self, from);
var startString = options.blockCommentStart || mode.blockCommentStart;
var endString = options.blockCommentEnd || mode.blockCommentEnd;
if (!startString || !endString) {
if ((options.lineComment || mode.lineComment) && options.fullLines != false)
self.lineComment(from, to, options);
return;
}
if (/\bcomment\b/.test(self.getTokenTypeAt(Pos(from.line, 0)))) return
var end = Math.min(to.line, self.lastLine());
if (end != from.line && to.ch == 0 && nonWS.test(self.getLine(end))) --end;
var pad = options.padding == null ? " " : options.padding;
if (from.line > end) return;
self.operation(function() {
if (options.fullLines != false) {
var lastLineHasText = nonWS.test(self.getLine(end));
self.replaceRange(pad + endString, Pos(end));
self.replaceRange(startString + pad, Pos(from.line, 0));
var lead = options.blockCommentLead || mode.blockCommentLead;
if (lead != null) for (var i = from.line + 1; i <= end; ++i)
if (i != end || lastLineHasText)
self.replaceRange(lead + pad, Pos(i, 0));
} else {
self.replaceRange(endString, to);
self.replaceRange(startString, from);
}
});
});
CodeMirror.defineExtension("uncomment", function(from, to, options) {
if (!options) options = noOptions;
var self = this, mode = getMode(self, from);
var end = Math.min(to.ch != 0 || to.line == from.line ? to.line : to.line - 1, self.lastLine()), start = Math.min(from.line, end);
// Try finding line comments
var lineString = options.lineComment || mode.lineComment, lines = [];
var pad = options.padding == null ? " " : options.padding, didSomething;
lineComment: {
if (!lineString) break lineComment;
for (var i = start; i <= end; ++i) {
var line = self.getLine(i);
var found = line.indexOf(lineString);
if (found > -1 && !/comment/.test(self.getTokenTypeAt(Pos(i, found + 1)))) found = -1;
if (found == -1 && nonWS.test(line)) break lineComment;
if (found > -1 && nonWS.test(line.slice(0, found))) break lineComment;
lines.push(line);
}
self.operation(function() {
for (var i = start; i <= end; ++i) {
var line = lines[i - start];
var pos = line.indexOf(lineString), endPos = pos + lineString.length;
if (pos < 0) continue;
if (line.slice(endPos, endPos + pad.length) == pad) endPos += pad.length;
didSomething = true;
self.replaceRange("", Pos(i, pos), Pos(i, endPos));
}
});
if (didSomething) return true;
}
// Try block comments
var startString = options.blockCommentStart || mode.blockCommentStart;
var endString = options.blockCommentEnd || mode.blockCommentEnd;
if (!startString || !endString) return false;
var lead = options.blockCommentLead || mode.blockCommentLead;
var startLine = self.getLine(start), open = startLine.indexOf(startString)
if (open == -1) return false
var endLine = end == start ? startLine : self.getLine(end)
var close = endLine.indexOf(endString, end == start ? open + startString.length : 0);
var insideStart = Pos(start, open + 1), insideEnd = Pos(end, close + 1)
if (close == -1 ||
!/comment/.test(self.getTokenTypeAt(insideStart)) ||
!/comment/.test(self.getTokenTypeAt(insideEnd)) ||
self.getRange(insideStart, insideEnd, "\n").indexOf(endString) > -1)
return false;
// Avoid killing block comments completely outside the selection.
// Positions of the last startString before the start of the selection, and the first endString after it.
var lastStart = startLine.lastIndexOf(startString, from.ch);
var firstEnd = lastStart == -1 ? -1 : startLine.slice(0, from.ch).indexOf(endString, lastStart + startString.length);
if (lastStart != -1 && firstEnd != -1 && firstEnd + endString.length != from.ch) return false;
// Positions of the first endString after the end of the selection, and the last startString before it.
firstEnd = endLine.indexOf(endString, to.ch);
var almostLastStart = endLine.slice(to.ch).lastIndexOf(startString, firstEnd - to.ch);
lastStart = (firstEnd == -1 || almostLastStart == -1) ? -1 : to.ch + almostLastStart;
if (firstEnd != -1 && lastStart != -1 && lastStart != to.ch) return false;
self.operation(function() {
self.replaceRange("", Pos(end, close - (pad && endLine.slice(close - pad.length, close) == pad ? pad.length : 0)),
Pos(end, close + endString.length));
var openEnd = open + startString.length;
if (pad && startLine.slice(openEnd, openEnd + pad.length) == pad) openEnd += pad.length;
self.replaceRange("", Pos(start, open), Pos(start, openEnd));
if (lead) for (var i = start + 1; i <= end; ++i) {
var line = self.getLine(i), found = line.indexOf(lead);
if (found == -1 || nonWS.test(line.slice(0, found))) continue;
var foundEnd = found + lead.length;
if (pad && line.slice(foundEnd, foundEnd + pad.length) == pad) foundEnd += pad.length;
self.replaceRange("", Pos(i, found), Pos(i, foundEnd));
}
});
return true;
});
});

View File

@ -0,0 +1,114 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
var nonspace = /\S/g;
var repeat = String.prototype.repeat || function (n) { return Array(n + 1).join(this); };
function continueComment(cm) {
if (cm.getOption("disableInput")) return CodeMirror.Pass;
var ranges = cm.listSelections(), mode, inserts = [];
for (var i = 0; i < ranges.length; i++) {
var pos = ranges[i].head
if (!/\bcomment\b/.test(cm.getTokenTypeAt(pos))) return CodeMirror.Pass;
var modeHere = cm.getModeAt(pos)
if (!mode) mode = modeHere;
else if (mode != modeHere) return CodeMirror.Pass;
var insert = null, line, found;
var blockStart = mode.blockCommentStart, lineCmt = mode.lineComment;
if (blockStart && mode.blockCommentContinue) {
line = cm.getLine(pos.line);
var end = line.lastIndexOf(mode.blockCommentEnd, pos.ch - mode.blockCommentEnd.length);
// 1. if this block comment ended
// 2. if this is actually inside a line comment
if (end != -1 && end == pos.ch - mode.blockCommentEnd.length ||
lineCmt && (found = line.lastIndexOf(lineCmt, pos.ch - 1)) > -1 &&
/\bcomment\b/.test(cm.getTokenTypeAt({line: pos.line, ch: found + 1}))) {
// ...then don't continue it
} else if (pos.ch >= blockStart.length &&
(found = line.lastIndexOf(blockStart, pos.ch - blockStart.length)) > -1 &&
found > end) {
// reuse the existing leading spaces/tabs/mixed
// or build the correct indent using CM's tab/indent options
if (nonspaceAfter(0, line) >= found) {
insert = line.slice(0, found);
} else {
var tabSize = cm.options.tabSize, numTabs;
found = CodeMirror.countColumn(line, found, tabSize);
insert = !cm.options.indentWithTabs ? repeat.call(" ", found) :
repeat.call("\t", (numTabs = Math.floor(found / tabSize))) +
repeat.call(" ", found - tabSize * numTabs);
}
} else if ((found = line.indexOf(mode.blockCommentContinue)) > -1 &&
found <= pos.ch &&
found <= nonspaceAfter(0, line)) {
insert = line.slice(0, found);
}
if (insert != null) insert += mode.blockCommentContinue
}
if (insert == null && lineCmt && continueLineCommentEnabled(cm)) {
if (line == null) line = cm.getLine(pos.line);
found = line.indexOf(lineCmt);
// cursor at pos 0, line comment also at pos 0 => shift it down, don't continue
if (!pos.ch && !found) insert = "";
// continue only if the line starts with an optional space + line comment
else if (found > -1 && nonspaceAfter(0, line) >= found) {
// don't continue if there's only space(s) after cursor or the end of the line
insert = nonspaceAfter(pos.ch, line) > -1;
// but always continue if the next line starts with a line comment too
if (!insert) {
var next = cm.getLine(pos.line + 1) || '',
nextFound = next.indexOf(lineCmt);
insert = nextFound > -1 && nonspaceAfter(0, next) >= nextFound || null;
}
if (insert) {
insert = line.slice(0, found) + lineCmt +
line.slice(found + lineCmt.length).match(/^\s*/)[0];
}
}
}
if (insert == null) return CodeMirror.Pass;
inserts[i] = "\n" + insert;
}
cm.operation(function() {
for (var i = ranges.length - 1; i >= 0; i--)
cm.replaceRange(inserts[i], ranges[i].from(), ranges[i].to(), "+insert");
});
}
function nonspaceAfter(ch, str) {
nonspace.lastIndex = ch;
var m = nonspace.exec(str);
return m ? m.index : -1;
}
function continueLineCommentEnabled(cm) {
var opt = cm.getOption("continueComments");
if (opt && typeof opt == "object")
return opt.continueLineComment !== false;
return true;
}
CodeMirror.defineOption("continueComments", null, function(cm, val, prev) {
if (prev && prev != CodeMirror.Init)
cm.removeKeyMap("continueComment");
if (val) {
var key = "Enter";
if (typeof val == "string")
key = val;
else if (typeof val == "object" && val.key)
key = val.key;
var map = {name: "continueComment"};
map[key] = continueComment;
cm.addKeyMap(map);
}
});
});

View File

@ -0,0 +1,32 @@
.CodeMirror-dialog {
position: absolute;
left: 0; right: 0;
background: inherit;
z-index: 15;
padding: .1em .8em;
overflow: hidden;
color: inherit;
}
.CodeMirror-dialog-top {
border-bottom: 1px solid #eee;
top: 0;
}
.CodeMirror-dialog-bottom {
border-top: 1px solid #eee;
bottom: 0;
}
.CodeMirror-dialog input {
border: none;
outline: none;
background: transparent;
width: 20em;
color: inherit;
font-family: monospace;
}
.CodeMirror-dialog button {
font-size: 70%;
}

View File

@ -0,0 +1,161 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
// Open simple dialogs on top of an editor. Relies on dialog.css.
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
function dialogDiv(cm, template, bottom) {
var wrap = cm.getWrapperElement();
var dialog;
dialog = wrap.appendChild(document.createElement("div"));
if (bottom)
dialog.className = "CodeMirror-dialog CodeMirror-dialog-bottom";
else
dialog.className = "CodeMirror-dialog CodeMirror-dialog-top";
if (typeof template == "string") {
dialog.innerHTML = template;
} else { // Assuming it's a detached DOM element.
dialog.appendChild(template);
}
CodeMirror.addClass(wrap, 'dialog-opened');
return dialog;
}
function closeNotification(cm, newVal) {
if (cm.state.currentNotificationClose)
cm.state.currentNotificationClose();
cm.state.currentNotificationClose = newVal;
}
CodeMirror.defineExtension("openDialog", function(template, callback, options) {
if (!options) options = {};
closeNotification(this, null);
var dialog = dialogDiv(this, template, options.bottom);
var closed = false, me = this;
function close(newVal) {
if (typeof newVal == 'string') {
inp.value = newVal;
} else {
if (closed) return;
closed = true;
CodeMirror.rmClass(dialog.parentNode, 'dialog-opened');
dialog.parentNode.removeChild(dialog);
me.focus();
if (options.onClose) options.onClose(dialog);
}
}
var inp = dialog.getElementsByTagName("input")[0], button;
if (inp) {
inp.focus();
if (options.value) {
inp.value = options.value;
if (options.selectValueOnOpen !== false) {
inp.select();
}
}
if (options.onInput)
CodeMirror.on(inp, "input", function(e) { options.onInput(e, inp.value, close);});
if (options.onKeyUp)
CodeMirror.on(inp, "keyup", function(e) {options.onKeyUp(e, inp.value, close);});
CodeMirror.on(inp, "keydown", function(e) {
if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; }
if (e.keyCode == 27 || (options.closeOnEnter !== false && e.keyCode == 13)) {
inp.blur();
CodeMirror.e_stop(e);
close();
}
if (e.keyCode == 13) callback(inp.value, e);
});
if (options.closeOnBlur !== false) CodeMirror.on(inp, "blur", close);
} else if (button = dialog.getElementsByTagName("button")[0]) {
CodeMirror.on(button, "click", function() {
close();
me.focus();
});
if (options.closeOnBlur !== false) CodeMirror.on(button, "blur", close);
button.focus();
}
return close;
});
CodeMirror.defineExtension("openConfirm", function(template, callbacks, options) {
closeNotification(this, null);
var dialog = dialogDiv(this, template, options && options.bottom);
var buttons = dialog.getElementsByTagName("button");
var closed = false, me = this, blurring = 1;
function close() {
if (closed) return;
closed = true;
CodeMirror.rmClass(dialog.parentNode, 'dialog-opened');
dialog.parentNode.removeChild(dialog);
me.focus();
}
buttons[0].focus();
for (var i = 0; i < buttons.length; ++i) {
var b = buttons[i];
(function(callback) {
CodeMirror.on(b, "click", function(e) {
CodeMirror.e_preventDefault(e);
close();
if (callback) callback(me);
});
})(callbacks[i]);
CodeMirror.on(b, "blur", function() {
--blurring;
setTimeout(function() { if (blurring <= 0) close(); }, 200);
});
CodeMirror.on(b, "focus", function() { ++blurring; });
}
});
/*
* openNotification
* Opens a notification, that can be closed with an optional timer
* (default 5000ms timer) and always closes on click.
*
* If a notification is opened while another is opened, it will close the
* currently opened one and open the new one immediately.
*/
CodeMirror.defineExtension("openNotification", function(template, options) {
closeNotification(this, close);
var dialog = dialogDiv(this, template, options && options.bottom);
var closed = false, doneTimer;
var duration = options && typeof options.duration !== "undefined" ? options.duration : 5000;
function close() {
if (closed) return;
closed = true;
clearTimeout(doneTimer);
CodeMirror.rmClass(dialog.parentNode, 'dialog-opened');
dialog.parentNode.removeChild(dialog);
}
CodeMirror.on(dialog, 'click', function(e) {
CodeMirror.e_preventDefault(e);
close();
});
if (duration)
doneTimer = setTimeout(close, duration);
return close;
});
});

View File

@ -0,0 +1,47 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"))
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod)
else // Plain browser env
mod(CodeMirror)
})(function(CodeMirror) {
"use strict"
CodeMirror.defineOption("autoRefresh", false, function(cm, val) {
if (cm.state.autoRefresh) {
stopListening(cm, cm.state.autoRefresh)
cm.state.autoRefresh = null
}
if (val && cm.display.wrapper.offsetHeight == 0)
startListening(cm, cm.state.autoRefresh = {delay: val.delay || 250})
})
function startListening(cm, state) {
function check() {
if (cm.display.wrapper.offsetHeight) {
stopListening(cm, state)
if (cm.display.lastWrapHeight != cm.display.wrapper.clientHeight)
cm.refresh()
} else {
state.timeout = setTimeout(check, state.delay)
}
}
state.timeout = setTimeout(check, state.delay)
state.hurry = function() {
clearTimeout(state.timeout)
state.timeout = setTimeout(check, 50)
}
CodeMirror.on(window, "mouseup", state.hurry)
CodeMirror.on(window, "keyup", state.hurry)
}
function stopListening(_cm, state) {
clearTimeout(state.timeout)
CodeMirror.off(window, "mouseup", state.hurry)
CodeMirror.off(window, "keyup", state.hurry)
}
});

View File

@ -0,0 +1,6 @@
.CodeMirror-fullscreen {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
height: auto;
z-index: 9;
}

Some files were not shown because too many files have changed in this diff Show More