Compare commits

..

9 Commits
v0.6 ... master

Author SHA1 Message Date
sportivaman
be50c2e763
Update README.md 2020-09-22 07:42:23 -04:00
Ross Mountjoy
ef23aec3f0 (MANUALLY ADDING PR FROM reedhaffner ) Add Plex as a platform 2020-05-30 09:20:08 -04:00
Ross Mountjoy
322612761d (MANUALLY ADDING PR FROM Thlb 0.6) Add Docker platform and template app 2020-05-30 08:59:02 -04:00
Ross Mountjoy
8358cc4749 (MANUALLY ADDING PR FROM Thlb) Add Lidarr platforms 2020-05-30 08:50:39 -04:00
Ross Mountjoy
519e123f77 (MANUALLY ADDING PR FROM Thlb) Add Sonarr,Radarr,Tautulli,Healthchecks platforms 2020-05-30 07:59:13 -04:00
Ross Mountjoy
f196180f06 fixing secret key issue 2020-05-30 07:48:34 -04:00
Ross Mountjoy
c9027a69b2 - update version 2020-05-11 06:25:19 -04:00
Ross Mountjoy
a9864ad3e5 Merge remote-tracking branch 'origin/master' 2020-05-11 06:18:24 -04:00
Ross Mountjoy
f93b77d89b - fixes #163 2020-05-11 06:18:16 -04:00
696 changed files with 4193 additions and 112306 deletions

4
.gitignore vendored
View File

@ -117,9 +117,7 @@ scheduler.db
scheduler.db
.idea/
.vscode/
dashmachine/user_data/
dashmachine/static/images/icons
dashmachine/static/images/backgrounds
dashmachine/platform/custom_*
dashmachine/static/images/backgrounds

View File

@ -16,4 +16,4 @@ COPY [ ".", "/dashmachine/" ]
ENV PRODUCTION=true
EXPOSE 5000
VOLUME /dashmachine/dashmachine/user_data
CMD [ "gunicorn", "--bind", '-t 300', "0.0.0.0:5000", "wsgi:app" ]
CMD [ "gunicorn", "--bind", "0.0.0.0:5000", "wsgi:app" ]

120
README.md
View File

@ -1,45 +1,99 @@
# DashMachine
##### Another web application bookmark dashboard, with fun features.
### Another web application bookmark dashboard, with fun features.
![Subreddit subscribers](https://img.shields.io/reddit/subreddit-subscribers/dashmachine?style=social)
### Demo
* [Go to live demo](#)
![GitHub last commit](https://img.shields.io/github/last-commit/rmountjoy92/dashmachine)
![Docker Cloud Build Status](https://img.shields.io/docker/cloud/build/rmountjoy/dashmachine)
### Features
* creates a dashboard to view web pages
* uses a single .ini file for configuration
* dark mode/light mode and accent colors
* custom backgrounds and icons
* web interface to edit the config file and add image files
* ability to open web pages in current tab, new tab or iframe
* hideable sidebar with dragable reveal button
* user login system
* 'app templates' which are sample config entries for popular self hosted apps
* powerful plugin system for adding data from various sources to display on cards
* multiple card types including collections and custom cards
* multiple users, access groups, access settings
* tagging system
![Docker Pulls](https://img.shields.io/docker/pulls/rmountjoy/dashmachine)
![GitHub Repo stars](https://img.shields.io/github/stars/rmountjoy92/dashmachine?style=social)
![GitHub repo size](https://img.shields.io/github/repo-size/rmountjoy92/dashmachine)
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/rmountjoy/dashmachine/latest?label=Docker%20Image%20Size)
![Lines of code](https://img.shields.io/tokei/lines/github/rmountjoy92/dashmachine)
[![GPLv3 License](https://img.shields.io/badge/License-GPL%20v3-yellow.svg)](https://opensource.org/licenses/)
[![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/sindresorhus/awesome)
[![Donate](https://img.shields.io/badge/$-support-ff69b4.svg?style=flat)](https://liberapay.com/rmountjoy)
![Bountysource](https://img.shields.io/bountysource/team/dashmachine/activity)
Want a feature added now? [Open a bounty](https://www.bountysource.com/teams/dashmachine-app)
## Screenshots
![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)
### 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)
## 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
```
### Subreddit
* [Go to subreddit](https://www.reddit.com/r/DashMachine)
## Updating
For python, use git. For docker, just pull the latest image and recreate the container.
### Want to buy me a coffee?
* [Librepay](https://liberapay.com/rmountjoy/donate)
* [Bountysource](https://www.bountysource.com/teams/dashmachine-app)
## 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'.
### 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)
### 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.
### Tech used
* Flask
* SQLalchemy w/ SQLite
* Jinja2
## Want to contribute?
Please use the pull request template at:
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
## Tech used
* Flask (Python 3)
* SQLalchemy w/ SQLite database
* HTML5/Jinja2
* Materialize css
* JavaScript/jQuery/jQueryUI
* .ini (for configuration)
## 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,14 +42,12 @@ 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

@ -1,524 +0,0 @@
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

@ -1,239 +0,0 @@
### 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

@ -1,39 +0,0 @@
### 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

@ -1,8 +0,0 @@
## 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

@ -1,56 +0,0 @@
# 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

@ -1,23 +0,0 @@
### 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

@ -1,94 +0,0 @@
import os
from flask import render_template, Blueprint, redirect
from flask_login import current_user
from dashmachine.paths import root_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,
)
from dashmachine.main.utils import get_apps_and_tags, get_access_group
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,
)

View File

@ -1,58 +0,0 @@
import os
from markdown2 import markdown
from flask import render_template_string, url_for
from dashmachine.paths import docs_folder
from dashmachine.docs_system.core_docs import (
base_md_string,
doc_toc_string,
apps_doc_dict,
custom_card_doc_dict,
collections_doc_dict,
)
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

View File

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

View File

@ -6,12 +6,6 @@ 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")),
)
class Files(db.Model):
id = db.Column(db.Integer, primary_key=True)
@ -32,6 +26,8 @@ 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())
@ -55,11 +51,14 @@ 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"),
)

View File

@ -1,91 +0,0 @@
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):
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,9 +1,8 @@
import os
import json
import socket
from configparser import ConfigParser
from dashmachine.main.models import Apps, DataSources, DataSourcesArgs, Tags
from dashmachine.user_system.models import User, AccessGroups
from dashmachine.main.models import Apps, Groups, DataSources, DataSourcesArgs, Tags
from dashmachine.user_system.models import User
from dashmachine.user_system.utils import (
hash_and_cache_password,
get_cached_password,
@ -11,19 +10,9 @@ 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:
@ -32,199 +21,75 @@ def row2dict(row):
return d
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
def read_config():
config = ConfigParser(interpolation=None)
try:
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"))
config.read(os.path.join(user_data_folder, "config.ini"))
except Exception as 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()
return {"msg": f"Invalid Config: {e}."}
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()
AccessGroups.query.delete()
Groups.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, error = create_settings(config)
if error:
return error
# Settings creation
if section == "Settings":
settings = Settings()
error = create_access_groups(config)
if error:
return error
settings.theme = config["Settings"].get("theme", "light")
error = create_users(config)
if error:
return error
settings.accent = config["Settings"].get("accent", "orange")
error = create_data_sources(config)
if error:
return error
settings.background = config["Settings"].get("background", "None")
error = create_cards(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"
# APPLY TAG SETTINGS
tags_settings = config["Settings"].get("tags", "None")
if tags_settings and tags_settings != "None":
tags_settings = tags_settings.replace("},{", "}%,%{").split("%,%")
settings.home_access_groups = config["Settings"].get(
"home_access_groups", "admin_only"
)
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()
settings.settings_access_groups = config["Settings"].get(
"settings_access_groups", "admin_only"
)
# 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)}
settings.custom_app_title = config["Settings"].get(
"custom_app_title", "DashMachine"
)
settings.sidebar_default = config["Settings"].get("sidebar_default", "open")
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.tags_expanded = config["Settings"].get("tags_expanded", "True")
settings.custom_app_title = config["Settings"].get(
"custom_app_title", "DashMachine"
)
db.session.add(settings)
db.session.commit()
settings.tags_expanded = config["Settings"].get("tags_expanded", "True")
settings.tags = config["Settings"].get("tags", None)
error = validate_json_csv(settings.tags)
if error:
return (
None,
{
"msg": f"{config_restored_msg} Invalid Json for settings - tags: {error}."
},
)
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}."
},
)
db.session.add(settings)
db.session.commit()
return settings, None
def create_users(config):
# LOOP CONFIG SECTIONS
for section in config.sections():
if "role" in config[section] and section != "Settings":
# User creation
elif "role" in config[section]:
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.background = config[section].get("background", None)
user.tags_expanded = config[section].get("tags_expanded", "False")
user.tags_expanded = config[section].get("tags_expanded", None)
user.password = ""
if not User.query.filter_by(role="admin").first() and user.role != "admin":
print(
@ -254,45 +119,17 @@ def create_users(config):
config.set(section, "password", "")
config.set(section, "confirm_password", "")
config.write(open(os.path.join(user_data_folder, "config.ini"), "w"))
return None
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()
# Groups creation
elif "roles" in config[section]:
group = Groups()
group.name = section
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")
group.roles = config[section]["roles"]
db.session.add(group)
db.session.commit()
return None
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 creation
elif "platform" in config[section]:
data_source = DataSources()
data_source.name = section
data_source.platform = config[section]["platform"]
@ -306,80 +143,45 @@ def create_data_sources(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:
# START CREATE APPS
# App creation
app = Apps()
app.name = section
app.type = config[section].get("type", "app")
app.prefix = config[section].get("prefix", "https://")
app.prefix = config[section].get("prefix", None)
if app.type == "app" and not app.prefix:
return {
"msg": f"{config_restored_msg} Invalid Config: {section} does not contain prefix."
}
return {"msg": f"Invalid Config: {section} does not contain prefix."}
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)
app.url = config[section].get("url", None)
if app.type == "app" and not app.url:
return {
"msg": f"{config_restored_msg} Invalid Config: {section} does not contain url."
}
return {"msg": f"Invalid Config: {section} does not contain url."}
app.icon = config[section].get("icon", "static/images/apps/default.png")
app.icon = config[section].get("icon", None)
app.sidebar_icon = config[section].get(
"sidebar_icon", "static/images/apps/default.png"
)
app.sidebar_icon = config[section].get("sidebar_icon", None)
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}."
}
# CREATE TAGS (DURING CREATE APPS)
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
if "tags" in config[section]:
for tag in config[section]["tags"].split(","):
app.tags = config[section]["tags"]
for tag in app.tags.split(","):
tag = tag.strip()
if not Tags.query.filter_by(name=tag).first():
tag_db = Tags(name=tag)
@ -388,23 +190,16 @@ def create_cards(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", sort_pos=1)
tag_db = Tags(name="Untagged")
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()
@ -414,27 +209,43 @@ def create_cards(config):
db.session.commit()
else:
return {
"msg": f"{config_restored_msg} Invalid Config: {section} has a data_source variable that doesn't exist."
"msg": f"Invalid Config: {section} has a data_source variable that doesn't exist."
}
# 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()
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 '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)
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)
db.session.commit()
# END CREATE APP
return None
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)}

View File

@ -1,43 +1,19 @@
import os
import glob
import json
from secrets import token_hex
from htmlmin.main import minify
from flask import (
render_template,
url_for,
redirect,
request,
Blueprint,
jsonify,
render_template_string,
)
from configparser import ConfigParser
from flask import render_template, url_for, redirect, request, Blueprint, jsonify
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.main.modify_config import modify_config
from dashmachine.user_system.models import User
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
@ -65,140 +41,20 @@ def response_minify(response):
@main.route("/")
@main.route("/home", methods=["GET"])
def home():
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("%,%")
]
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
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")
@main.route("/app_view?<app_id>", methods=["GET"])
def app_view(app_id):
access_group, redirect_url = get_access_group(current_user)
apps, tags = get_apps_and_tags(access_group)
if redirect_url:
return redirect(redirect_url)
settings = Settings.query.first()
if not check_groups(settings.home_access_groups, current_user):
return redirect(url_for("user_system.login"))
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,
access_group=access_group,
apps=apps,
tags=tags,
"main/app-view.html", url=f"{app_db.prefix}{app_db.url}", title=app_db.name
)
@ -215,200 +71,6 @@ 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)
# ------------------------------------------------------------------------------
# TCDROP routes
# ------------------------------------------------------------------------------
@ -451,3 +113,28 @@ 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,22 +1,16 @@
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 Tags
from dashmachine.main.models import Groups
from dashmachine.main.read_config import read_config
from dashmachine.user_system.models import AccessGroups
from dashmachine.version import version as dashmachine_version
from dashmachine import db
@ -41,6 +35,8 @@ 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")
@ -62,78 +58,34 @@ 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)
# 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)
# 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()
def get_access_group(user, page=None):
access_groups = []
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)
def check_groups(groups, current_user):
if current_user.is_anonymous:
current_user.role = "public_user"
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
else:
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:
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
if current_user.role == "admin":
return True
else:
return False
def get_data_source(data_source):
data_source_args = {}
for arg in data_source.args:
arg = row2dict(arg)
if arg["value"] != "None":
data_source_args[arg.get("key")] = arg.get("value")
data_source_args[arg.get("key")] = arg.get("value")
data_source = row2dict(data_source)
module = importlib.import_module(
f"dashmachine.platform.{data_source['platform']}", "."
@ -151,25 +103,6 @@ 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:
@ -200,41 +133,3 @@ 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())

View File

@ -17,12 +17,8 @@ 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")
auth_cache = os.path.join(user_data_folder, "auth_cache")
if not os.path.isdir(auth_cache):

View File

@ -1,69 +1,47 @@
"""
##### 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 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():

View File

@ -1,69 +1,49 @@
"""
##### 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.",
"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
@ -74,19 +54,16 @@ data_sources = deluge
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,394 @@
"""
##### Docker
Display information from Docker API. Informations can be displayed on a custom card or on an app card (e.g. Portainer App)
```ini
[variable_name]
platform = docker
prefix = http://
host = localhost
port = 2375
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. | docker |
| prefix | No | The prefix for the app's url. | web prefix, e.g. http:// or https:// |
| host | Yes | Docker Host | url,ip |
| port | No | Docker Port | port, usually 2375 (Insecure) or 2376 (TLS) |
| api_version | No | Docker API version to use (Default : platform will try to find latest version) | 1.40 |
| tls_mode | No | TLS verification mode, default is None | Server, Client, Both, None |
| tls_ca | No | Requierd for tls_mode=Both or tls_mode=Server, default is None | /path/to/ca, None |
| tls_cert | No | Requierd for tls_mode=Both or tls_mode=Client, default is None | /path/to/cert, None |
| tls_key | No | Requierd for tls_mode=Both or tls_mode=Client, default is None | /path/to/key, None|
| card_type | No | Set to Custom if you want to display informations in a custom card. Default is App | Custom, App|
| value_template | Yes | Jinja template for how the returned data from API is displayed. | jinja template |
<br />
###### **Available fields for value_template**
* version
* max_api_version
* name
* containers
* containers_running
* containers_paused
* containers_stopped
* images
* driver
* cpu
* memory
* warnings
* error (for debug)
> **Working example (using un-encrypted connection, on Portainer card):**
>```ini
> [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
>```
"""
import json
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 __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,199 @@
"""
##### Healthchecks
Display information from Healthchecks API
```ini
[variable_name]
platform = healthchecks
prefix = http://
host = localhost
port = 8080
api_key = {{ Healthchecks project API Key }}
project = {{ Healthchecks project name }}
verify = true
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. | healthchecks |
| prefix | No | The prefix for the app's url. | web prefix, e.g. http:// or https:// |
| host | Yes | Healthchecks Host | url,ip |
| port | No | Healthchecks Port | port |
| api_key | Yes | ApiKey | api key |
| project | No | Healthchecks project name | project |
| verify | No | Turn TLS verification on or off, default is true | true,false |
| value_template | Yes | Jinja template for how the returned data from API is displayed. | jinja template |
<br />
###### **Available fields for value_template**
* status
* count_checks
* count_up
* count_down
* count_grace
* count_paused
* error (for debug)
> **Working 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
>```
"""
import json
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 __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,90 +1,54 @@
"""
##### 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():

View File

@ -0,0 +1,290 @@
"""
##### Lidarr
Display information from Lidarr API
```ini
[variable_name]
platform = lidarr
prefix = http://
host = localhost
port = 8686
api_key = my_api_key
verify = true
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. | lidarr |
| prefix | No | The prefix for the app's url. | web prefix, e.g. http:// or https:// |
| host | Yes | Lidarr Host | url,ip |
| port | No | Lidarr Port | port |
| api_key | Yes | ApiKey | api key |
| api_version | No | API version (default : v1) | v1 |
| verify | No | Turn TLS verification on or off, default is true | true,false |
| value_template | Yes | Jinja template for how the returned data from API is displayed. | jinja template |
<br />
###### **Available fields for value_template**
* version
* wanted_missing
* wanted_cutoff
* queue
* diskspace[x]['path']
* diskspace[x]['total']
* diskspace[x]['used']
* diskspace[x]['free']
* error (for debug)
> **Working 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
>```
"""
import json
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 __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,3 +1,58 @@
"""
##### 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
@ -187,85 +242,12 @@ 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,53 +1,28 @@
"""
##### 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,95 @@
"""
##### 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 __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"):
print(
"Please add a token\nSee https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/ to find it."
)
exit(1)
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,268 @@
"""
##### Radarr
Display information from Radarr API
```ini
[variable_name]
platform = radarr
prefix = http://
host = localhost
port = 7878
api_key = my_api_key
verify = true
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. | radarr |
| prefix | No | The prefix for the app's url. | web prefix, e.g. http:// or https:// |
| host | Yes | Radarr Host | url,ip |
| port | No | Radarr Port | port |
| api_key | Yes | ApiKey | api key |
| verify | No | Turn TLS verification on or off, default is true | true,false |
| value_template | Yes | Jinja template for how the returned data from API is displayed. | jinja template |
<br />
###### **Available fields for value_template**
* version
* movies
* queue
* diskspace[x]['path']
* diskspace[x]['total']
* diskspace[x]['used']
* diskspace[x]['free']
* error (for debug)
> **Working 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
>```
"""
import json
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 __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,3 +1,54 @@
"""
##### 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
@ -5,102 +56,6 @@ 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():
@ -109,8 +64,6 @@ data_sources = test
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

@ -1,130 +0,0 @@
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,268 @@
"""
##### Sonarr
Display information from Sonarr API
```ini
[variable_name]
platform = sonarr
prefix = http://
host = localhost
port = 8989
api_key = {{ Sonarr API Key }}
verify = true
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. | sonarr |
| prefix | No | The prefix for the app's url. | web prefix, e.g. http:// or https:// |
| host | Yes | Sonarr Host | url,ip |
| port | No | Sonarr Port | port |
| api_key | Yes | ApiKey | api key |
| verify | No | Turn TLS verification on or off, default is true | true,false |
| value_template | Yes | Jinja template for how the returned data from API is displayed. | jinja template |
<br />
###### **Available fields for value_template**
* version
* wanted_missing
* queue
* diskspace[x]['path']
* diskspace[x]['total']
* diskspace[x]['used']
* diskspace[x]['free']
* error (for debug)
> **Working 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
>```
"""
import json
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 __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,224 @@
"""
##### Tautulli
Display information from Tautulli API
```ini
[variable_name]
platform = tautulli
prefix = http://
host = localhost
port = 8181
api_key = {{ Tautulli API Key }}
verify = true
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. | tautulli |
| prefix | No | The prefix for the app's url. | web prefix, e.g. http:// or https:// |
| host | Yes | Tautulli Host | url,ip |
| port | No | Tautulli Port | port |
| api_key | Yes | ApiKey | api key |
| verify | No | Turn TLS verification on or off, default is true | true,false |
| value_template | Yes | Jinja template for how the returned data from API is displayed. | jinja template |
<br />
###### **Available fields for value_template**
* stream_count
* stream_count_direct_play
* stream_count_direct_stream
* stream_count_transcode
* total_bandwidth
* wan_bandwidth
* update_available
* update_message
* error (for debug)
> **Working 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
>```
"""
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 __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,89 +1,67 @@
"""
##### 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():
@ -94,11 +72,12 @@ data_sources = transmission-data
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,74 +1,46 @@
"""
##### 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():
@ -82,14 +54,14 @@ data_sources = variable_name
if not hasattr(self, "wind_speed_unit"):
self.wind_speed_unit = "kph"
if not hasattr(self, "air_pressure_unit"):
self.air_pressure_unit = "mbar"
self.air_pressure_unit = "x"
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;</span>
<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>
</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

@ -7,7 +7,8 @@ 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,33 +1,61 @@
import os
from shutil import move
from flask import (
request,
Blueprint,
jsonify,
render_template_string,
)
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 dashmachine.main.read_config import read_config
from dashmachine.main.models import Files
from dashmachine.settings_system.utils import load_files_html
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.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__)
@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() }}
"""
@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,
)
return html
@settings_system.route("/settings/save_config", methods=["POST"])
@ -35,8 +63,6 @@ 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,7 +4,9 @@ import random
from jsmin import jsmin
from flask_login import current_user
from dashmachine import app
from dashmachine.main.utils import get_update_message_html, row2dict
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.settings_system.models import Settings
from dashmachine.paths import static_folder, backgrounds_images_folder
from dashmachine.cssmin import cssmin
@ -25,9 +27,8 @@ def process_js_sources(process_bundle=None, src=None, app_global=False):
elif app_global is True:
process_bundle = [
"main/dashmachine.js",
"main/ini-form.js",
"main/tcdrop.js",
"global/dashmachine.js",
"global/tcdrop.js",
]
html = ""
@ -81,45 +82,49 @@ 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.selected_background = None
settings.background = None
else:
settings.selected_background = (
settings.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,
user=user,
action_providers=action_providers,
tags=tags,
tags_form=tags_form,
update_message=update_message,
)

View File

@ -1,147 +0,0 @@
#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,8 +5,7 @@
--theme-surface-rgb: 255, 255, 255;
--theme-surface-1: #fcfcfc;
--theme-surface-2: #e0e0e0;
--theme-almost-transparent: rgba(255, 255, 255, 0.2);
--theme-primary: #ff9800;
--theme-primary: #FF9966;
--theme-secondary: #9e9e9e;
--theme-accent: #3399FF;
--theme-color-font: #2c2f3a;
@ -23,10 +22,8 @@
--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"] {
@ -152,9 +149,6 @@
.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,20 +33,17 @@
}
.scrollbar {
overflow-y: auto !important;
overflow-y: scroll !important;
overflow-x: hidden !important;
}
.scrollbar-x {
overflow-y: auto !important;
overflow-x: auto !important;
overflow-y: scroll !important;
overflow-x: scroll !important;
}
/* ELEMENT STLYES */
body {
background-position: center;
background-size: cover;
background-attachment: fixed;
overflow: auto;
overflow: scroll;
overflow-x: hidden !important;
min-height: 100%;
color: var(--theme-color-font);
@ -145,8 +142,7 @@ textarea {
}
input:disabled {
color: var(--theme-secondary) !important;
border-bottom-color: var(--theme-secondary) !important;
color: var(--theme-secondary);
}
/* label color */
@ -202,18 +198,6 @@ 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;
@ -301,85 +285,25 @@ input:disabled {
/* END FORM STYLES */
/* SIDENAV*/
#main-sidenav {
height: 100vh;
top: 0;
overflow-y: scroll;
}
@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;
margin-left: 1rem;
bottom: 2px;
}
#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 {
#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);
}
#sidenav-toggle-svg-container {
z-index: 8000;
#show-sidenav .material-icons-outlined {
font-size: 32px;
position: relative;
left: 15px;
top: 5px;
}
.drag-target {
width: 0;
height: 0;
}
#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-main .sidenav-collapsible {
border-radius: 0px;
background-color: var(--theme-surface);
}
.sidenav li > a > i.material-icons-outlined {
@ -419,6 +343,12 @@ 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;
}
@ -432,7 +362,7 @@ input:disabled {
/* MODALS AND CARDS */
.modal {
background-color: var(--theme-surface);
border-radius: 10px !important;
border-radius: 12px !important;
position: fixed;
}
@ -454,13 +384,6 @@ 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%;
@ -670,254 +593,3 @@ 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%;
}
@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;
}
#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: 0px;
padding-left: 64px;
}
footer
{

View File

@ -20,4 +20,43 @@
/*width: calc(100vw - 45px) !important;*/
/*margin-left: 15px !important;*/
}
}
.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 {
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;
}

View File

@ -0,0 +1,66 @@
@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;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

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.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,9 +0,0 @@
$(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,345 @@
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

@ -1,92 +0,0 @@
// 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-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');
});
});
$(".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

@ -1,30 +0,0 @@
sleep(500).then(() => {
var config_textarea_codemirror = CodeMirror.fromTextArea(document.getElementById("config-textarea"), {
lineNumbers: true,
mode: 'properties',
theme: 'dashmachine',
scrollbarStyle: null,
});
$("#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);
}
}
});
});
});

View File

@ -1,609 +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 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 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);
}
});
}
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');
});
// 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,3 +1,6 @@
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');
@ -13,7 +16,46 @@ function get_data_source(el){
});
}
function init_home_cards(){
$( 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');
}
});
});
$(".data-source-container").each(function(e) {
get_data_source($(this));
});
@ -25,21 +67,66 @@ function init_home_cards(){
});
});
$(".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).closest('.tag-group').attr("data-tag") == tag_name){
toggle_tag_expand($(this));
$("#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
$(".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');
}
});
$( document ).ready(function() {
$(".tooltipped").tooltip();
$("#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();
}
});
}
});
init_home_cards();
if ($("#settings-tags_expanded").val() == "False" || $("#user-tags_expanded").val() == "False"){
$(".toggle-tag-expand-btn").each(function(e) {
$(this)[0].click();
});
if ($("#user-tags_expanded").val() == "True"){
$(".toggle-tag-expand-btn").each(function(e) {
$(this)[0].click();
});
}
}
});

View File

@ -1,163 +0,0 @@
// 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

@ -1,77 +0,0 @@
$( 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

@ -0,0 +1,84 @@
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

@ -1,11 +0,0 @@
/*!
* 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

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

View File

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

View File

@ -1,8 +0,0 @@
*.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

@ -1,14 +0,0 @@
/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

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

View File

@ -1,859 +0,0 @@
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

@ -1,92 +0,0 @@
# 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

@ -1,21 +0,0 @@
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

@ -1,48 +0,0 @@
# 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

@ -1,209 +0,0 @@
// 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

@ -1,114 +0,0 @@
// 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

@ -1,32 +0,0 @@
.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

@ -1,161 +0,0 @@
// 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

@ -1,47 +0,0 @@
// 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

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

View File

@ -1,41 +0,0 @@
// 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("fullScreen", false, function(cm, val, old) {
if (old == CodeMirror.Init) old = false;
if (!old == !val) return;
if (val) setFullscreen(cm);
else setNormal(cm);
});
function setFullscreen(cm) {
var wrap = cm.getWrapperElement();
cm.state.fullScreenRestore = {scrollTop: window.pageYOffset, scrollLeft: window.pageXOffset,
width: wrap.style.width, height: wrap.style.height};
wrap.style.width = "";
wrap.style.height = "auto";
wrap.className += " CodeMirror-fullscreen";
document.documentElement.style.overflow = "hidden";
cm.refresh();
}
function setNormal(cm) {
var wrap = cm.getWrapperElement();
wrap.className = wrap.className.replace(/\s*CodeMirror-fullscreen\b/, "");
document.documentElement.style.overflow = "";
var info = cm.state.fullScreenRestore;
wrap.style.width = info.width; wrap.style.height = info.height;
window.scrollTo(info.scrollLeft, info.scrollTop);
cm.refresh();
}
});

View File

@ -1,129 +0,0 @@
// 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) {
CodeMirror.defineExtension("addPanel", function (node, options) {
options = options || {};
if (!this.state.panels) initPanels(this);
var info = this.state.panels;
var wrapper = info.wrapper;
var cmWrapper = this.getWrapperElement();
var replace = options.replace instanceof Panel && !options.replace.cleared;
if (options.after instanceof Panel && !options.after.cleared) {
wrapper.insertBefore(node, options.before.node.nextSibling);
} else if (options.before instanceof Panel && !options.before.cleared) {
wrapper.insertBefore(node, options.before.node);
} else if (replace) {
wrapper.insertBefore(node, options.replace.node);
options.replace.clear(true);
} else if (options.position == "bottom") {
wrapper.appendChild(node);
} else if (options.position == "before-bottom") {
wrapper.insertBefore(node, cmWrapper.nextSibling);
} else if (options.position == "after-top") {
wrapper.insertBefore(node, cmWrapper);
} else {
wrapper.insertBefore(node, wrapper.firstChild);
}
var height = (options && options.height) || node.offsetHeight;
var panel = new Panel(this, node, options, height);
info.panels.push(panel);
this.setSize();
if (options.stable && isAtTop(this, node))
this.scrollTo(null, this.getScrollInfo().top + height);
return panel;
});
function Panel(cm, node, options, height) {
this.cm = cm;
this.node = node;
this.options = options;
this.height = height;
this.cleared = false;
}
/* when skipRemove is true, clear() was called from addPanel().
* Thus removePanels() should not be called (issue 5518) */
Panel.prototype.clear = function (skipRemove) {
if (this.cleared) return;
this.cleared = true;
var info = this.cm.state.panels;
info.panels.splice(info.panels.indexOf(this), 1);
this.cm.setSize();
if (this.options.stable && isAtTop(this.cm, this.node))
this.cm.scrollTo(null, this.cm.getScrollInfo().top - this.height)
info.wrapper.removeChild(this.node);
if (info.panels.length == 0 && !skipRemove) removePanels(this.cm);
};
Panel.prototype.changed = function () {
this.height = this.node.getBoundingClientRect().height;
this.cm.setSize();
};
function initPanels(cm) {
var wrap = cm.getWrapperElement();
var style = window.getComputedStyle ? window.getComputedStyle(wrap) : wrap.currentStyle;
var height = parseInt(style.height);
var info = cm.state.panels = {
setHeight: wrap.style.height,
panels: [],
wrapper: document.createElement("div")
};
wrap.parentNode.insertBefore(info.wrapper, wrap);
var hasFocus = cm.hasFocus();
info.wrapper.appendChild(wrap);
if (hasFocus) cm.focus();
cm._setSize = cm.setSize;
if (height != null) cm.setSize = function (width, newHeight) {
if (!newHeight) newHeight = info.wrapper.offsetHeight;
info.setHeight = newHeight;
if (typeof newHeight != "number") {
var px = /^(\d+\.?\d*)px$/.exec(newHeight);
if (px) {
newHeight = Number(px[1]);
} else {
info.wrapper.style.height = newHeight;
newHeight = info.wrapper.offsetHeight;
}
}
var editorheight = newHeight - info.panels
.map(function (p) { return p.node.getBoundingClientRect().height; })
.reduce(function (a, b) { return a + b; }, 0);
cm._setSize(width, editorheight);
height = newHeight;
};
}
function removePanels(cm) {
var info = cm.state.panels;
cm.state.panels = null;
var wrap = cm.getWrapperElement();
info.wrapper.parentNode.replaceChild(wrap, info.wrapper);
wrap.style.height = info.setHeight;
cm.setSize = cm._setSize;
cm.setSize();
}
function isAtTop(cm, dom) {
for (var sibling = dom.nextSibling; sibling; sibling = sibling.nextSibling)
if (sibling == cm.getWrapperElement()) return true
return false
}
});

View File

@ -1,63 +0,0 @@
// 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) {
CodeMirror.defineOption("placeholder", "", function(cm, val, old) {
var prev = old && old != CodeMirror.Init;
if (val && !prev) {
cm.on("blur", onBlur);
cm.on("change", onChange);
cm.on("swapDoc", onChange);
onChange(cm);
} else if (!val && prev) {
cm.off("blur", onBlur);
cm.off("change", onChange);
cm.off("swapDoc", onChange);
clearPlaceholder(cm);
var wrapper = cm.getWrapperElement();
wrapper.className = wrapper.className.replace(" CodeMirror-empty", "");
}
if (val && !cm.hasFocus()) onBlur(cm);
});
function clearPlaceholder(cm) {
if (cm.state.placeholder) {
cm.state.placeholder.parentNode.removeChild(cm.state.placeholder);
cm.state.placeholder = null;
}
}
function setPlaceholder(cm) {
clearPlaceholder(cm);
var elt = cm.state.placeholder = document.createElement("pre");
elt.style.cssText = "height: 0; overflow: visible";
elt.style.direction = cm.getOption("direction");
elt.className = "CodeMirror-placeholder CodeMirror-line-like";
var placeHolder = cm.getOption("placeholder")
if (typeof placeHolder == "string") placeHolder = document.createTextNode(placeHolder)
elt.appendChild(placeHolder)
cm.display.lineSpace.insertBefore(elt, cm.display.lineSpace.firstChild);
}
function onBlur(cm) {
if (isEmpty(cm)) setPlaceholder(cm);
}
function onChange(cm) {
var wrapper = cm.getWrapperElement(), empty = isEmpty(cm);
wrapper.className = wrapper.className.replace(" CodeMirror-empty", "") + (empty ? " CodeMirror-empty" : "");
if (empty) setPlaceholder(cm);
else clearPlaceholder(cm);
}
function isEmpty(cm) {
return (cm.lineCount() === 1) && (cm.getLine(0) === "");
}
});

View File

@ -1,51 +0,0 @@
// 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("rulers", false, function(cm, val) {
if (cm.state.rulerDiv) {
cm.state.rulerDiv.parentElement.removeChild(cm.state.rulerDiv)
cm.state.rulerDiv = null
cm.off("refresh", drawRulers)
}
if (val && val.length) {
cm.state.rulerDiv = cm.display.lineSpace.parentElement.insertBefore(document.createElement("div"), cm.display.lineSpace)
cm.state.rulerDiv.className = "CodeMirror-rulers"
drawRulers(cm)
cm.on("refresh", drawRulers)
}
});
function drawRulers(cm) {
cm.state.rulerDiv.textContent = ""
var val = cm.getOption("rulers");
var cw = cm.defaultCharWidth();
var left = cm.charCoords(CodeMirror.Pos(cm.firstLine(), 0), "div").left;
cm.state.rulerDiv.style.minHeight = (cm.display.scroller.offsetHeight + 30) + "px";
for (var i = 0; i < val.length; i++) {
var elt = document.createElement("div");
elt.className = "CodeMirror-ruler";
var col, conf = val[i];
if (typeof conf == "number") {
col = conf;
} else {
col = conf.column;
if (conf.className) elt.className += " " + conf.className;
if (conf.color) elt.style.borderColor = conf.color;
if (conf.lineStyle) elt.style.borderLeftStyle = conf.lineStyle;
if (conf.width) elt.style.borderLeftWidth = conf.width;
}
elt.style.left = (left + col * cw) + "px";
cm.state.rulerDiv.appendChild(elt)
}
}
});

View File

@ -1,191 +0,0 @@
// 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 defaults = {
pairs: "()[]{}''\"\"",
closeBefore: ")]}'\":;>",
triples: "",
explode: "[]{}"
};
var Pos = CodeMirror.Pos;
CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) {
if (old && old != CodeMirror.Init) {
cm.removeKeyMap(keyMap);
cm.state.closeBrackets = null;
}
if (val) {
ensureBound(getOption(val, "pairs"))
cm.state.closeBrackets = val;
cm.addKeyMap(keyMap);
}
});
function getOption(conf, name) {
if (name == "pairs" && typeof conf == "string") return conf;
if (typeof conf == "object" && conf[name] != null) return conf[name];
return defaults[name];
}
var keyMap = {Backspace: handleBackspace, Enter: handleEnter};
function ensureBound(chars) {
for (var i = 0; i < chars.length; i++) {
var ch = chars.charAt(i), key = "'" + ch + "'"
if (!keyMap[key]) keyMap[key] = handler(ch)
}
}
ensureBound(defaults.pairs + "`")
function handler(ch) {
return function(cm) { return handleChar(cm, ch); };
}
function getConfig(cm) {
var deflt = cm.state.closeBrackets;
if (!deflt || deflt.override) return deflt;
var mode = cm.getModeAt(cm.getCursor());
return mode.closeBrackets || deflt;
}
function handleBackspace(cm) {
var conf = getConfig(cm);
if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
var pairs = getOption(conf, "pairs");
var ranges = cm.listSelections();
for (var i = 0; i < ranges.length; i++) {
if (!ranges[i].empty()) return CodeMirror.Pass;
var around = charsAround(cm, ranges[i].head);
if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass;
}
for (var i = ranges.length - 1; i >= 0; i--) {
var cur = ranges[i].head;
cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1), "+delete");
}
}
function handleEnter(cm) {
var conf = getConfig(cm);
var explode = conf && getOption(conf, "explode");
if (!explode || cm.getOption("disableInput")) return CodeMirror.Pass;
var ranges = cm.listSelections();
for (var i = 0; i < ranges.length; i++) {
if (!ranges[i].empty()) return CodeMirror.Pass;
var around = charsAround(cm, ranges[i].head);
if (!around || explode.indexOf(around) % 2 != 0) return CodeMirror.Pass;
}
cm.operation(function() {
var linesep = cm.lineSeparator() || "\n";
cm.replaceSelection(linesep + linesep, null);
cm.execCommand("goCharLeft");
ranges = cm.listSelections();
for (var i = 0; i < ranges.length; i++) {
var line = ranges[i].head.line;
cm.indentLine(line, null, true);
cm.indentLine(line + 1, null, true);
}
});
}
function contractSelection(sel) {
var inverted = CodeMirror.cmpPos(sel.anchor, sel.head) > 0;
return {anchor: new Pos(sel.anchor.line, sel.anchor.ch + (inverted ? -1 : 1)),
head: new Pos(sel.head.line, sel.head.ch + (inverted ? 1 : -1))};
}
function handleChar(cm, ch) {
var conf = getConfig(cm);
if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
var pairs = getOption(conf, "pairs");
var pos = pairs.indexOf(ch);
if (pos == -1) return CodeMirror.Pass;
var closeBefore = getOption(conf,"closeBefore");
var triples = getOption(conf, "triples");
var identical = pairs.charAt(pos + 1) == ch;
var ranges = cm.listSelections();
var opening = pos % 2 == 0;
var type;
for (var i = 0; i < ranges.length; i++) {
var range = ranges[i], cur = range.head, curType;
var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1));
if (opening && !range.empty()) {
curType = "surround";
} else if ((identical || !opening) && next == ch) {
if (identical && stringStartsAfter(cm, cur))
curType = "both";
else if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch)
curType = "skipThree";
else
curType = "skip";
} else if (identical && cur.ch > 1 && triples.indexOf(ch) >= 0 &&
cm.getRange(Pos(cur.line, cur.ch - 2), cur) == ch + ch) {
if (cur.ch > 2 && /\bstring/.test(cm.getTokenTypeAt(Pos(cur.line, cur.ch - 2)))) return CodeMirror.Pass;
curType = "addFour";
} else if (identical) {
var prev = cur.ch == 0 ? " " : cm.getRange(Pos(cur.line, cur.ch - 1), cur)
if (!CodeMirror.isWordChar(next) && prev != ch && !CodeMirror.isWordChar(prev)) curType = "both";
else return CodeMirror.Pass;
} else if (opening && (next.length === 0 || /\s/.test(next) || closeBefore.indexOf(next) > -1)) {
curType = "both";
} else {
return CodeMirror.Pass;
}
if (!type) type = curType;
else if (type != curType) return CodeMirror.Pass;
}
var left = pos % 2 ? pairs.charAt(pos - 1) : ch;
var right = pos % 2 ? ch : pairs.charAt(pos + 1);
cm.operation(function() {
if (type == "skip") {
cm.execCommand("goCharRight");
} else if (type == "skipThree") {
for (var i = 0; i < 3; i++)
cm.execCommand("goCharRight");
} else if (type == "surround") {
var sels = cm.getSelections();
for (var i = 0; i < sels.length; i++)
sels[i] = left + sels[i] + right;
cm.replaceSelections(sels, "around");
sels = cm.listSelections().slice();
for (var i = 0; i < sels.length; i++)
sels[i] = contractSelection(sels[i]);
cm.setSelections(sels);
} else if (type == "both") {
cm.replaceSelection(left + right, null);
cm.triggerElectric(left + right);
cm.execCommand("goCharLeft");
} else if (type == "addFour") {
cm.replaceSelection(left + left + left + left, "before");
cm.execCommand("goCharRight");
}
});
}
function charsAround(cm, pos) {
var str = cm.getRange(Pos(pos.line, pos.ch - 1),
Pos(pos.line, pos.ch + 1));
return str.length == 2 ? str : null;
}
function stringStartsAfter(cm, pos) {
var token = cm.getTokenAt(Pos(pos.line, pos.ch + 1))
return /\bstring/.test(token.type) && token.start == pos.ch &&
(pos.ch == 0 || !/\bstring/.test(cm.getTokenTypeAt(pos)))
}
});

View File

@ -1,184 +0,0 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
/**
* Tag-closer extension for CodeMirror.
*
* This extension adds an "autoCloseTags" option that can be set to
* either true to get the default behavior, or an object to further
* configure its behavior.
*
* These are supported options:
*
* `whenClosing` (default true)
* Whether to autoclose when the '/' of a closing tag is typed.
* `whenOpening` (default true)
* Whether to autoclose the tag when the final '>' of an opening
* tag is typed.
* `dontCloseTags` (default is empty tags for HTML, none for XML)
* An array of tag names that should not be autoclosed.
* `indentTags` (default is block tags for HTML, none for XML)
* An array of tag names that should, when opened, cause a
* blank line to be added inside the tag, and the blank line and
* closing line to be indented.
* `emptyTags` (default is none)
* An array of XML tag names that should be autoclosed with '/>'.
*
* See demos/closetag.html for a usage example.
*/
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"), require("../fold/xml-fold"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror", "../fold/xml-fold"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
CodeMirror.defineOption("autoCloseTags", false, function(cm, val, old) {
if (old != CodeMirror.Init && old)
cm.removeKeyMap("autoCloseTags");
if (!val) return;
var map = {name: "autoCloseTags"};
if (typeof val != "object" || val.whenClosing)
map["'/'"] = function(cm) { return autoCloseSlash(cm); };
if (typeof val != "object" || val.whenOpening)
map["'>'"] = function(cm) { return autoCloseGT(cm); };
cm.addKeyMap(map);
});
var htmlDontClose = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param",
"source", "track", "wbr"];
var htmlIndent = ["applet", "blockquote", "body", "button", "div", "dl", "fieldset", "form", "frameset", "h1", "h2", "h3", "h4",
"h5", "h6", "head", "html", "iframe", "layer", "legend", "object", "ol", "p", "select", "table", "ul"];
function autoCloseGT(cm) {
if (cm.getOption("disableInput")) return CodeMirror.Pass;
var ranges = cm.listSelections(), replacements = [];
var opt = cm.getOption("autoCloseTags");
for (var i = 0; i < ranges.length; i++) {
if (!ranges[i].empty()) return CodeMirror.Pass;
var pos = ranges[i].head, tok = cm.getTokenAt(pos);
var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state;
var tagInfo = inner.mode.xmlCurrentTag && inner.mode.xmlCurrentTag(state)
var tagName = tagInfo && tagInfo.name
if (!tagName) return CodeMirror.Pass
var html = inner.mode.configuration == "html";
var dontCloseTags = (typeof opt == "object" && opt.dontCloseTags) || (html && htmlDontClose);
var indentTags = (typeof opt == "object" && opt.indentTags) || (html && htmlIndent);
if (tok.end > pos.ch) tagName = tagName.slice(0, tagName.length - tok.end + pos.ch);
var lowerTagName = tagName.toLowerCase();
// Don't process the '>' at the end of an end-tag or self-closing tag
if (!tagName ||
tok.type == "string" && (tok.end != pos.ch || !/[\"\']/.test(tok.string.charAt(tok.string.length - 1)) || tok.string.length == 1) ||
tok.type == "tag" && tagInfo.close ||
tok.string.indexOf("/") == (pos.ch - tok.start - 1) || // match something like <someTagName />
dontCloseTags && indexOf(dontCloseTags, lowerTagName) > -1 ||
closingTagExists(cm, inner.mode.xmlCurrentContext && inner.mode.xmlCurrentContext(state) || [], tagName, pos, true))
return CodeMirror.Pass;
var emptyTags = typeof opt == "object" && opt.emptyTags;
if (emptyTags && indexOf(emptyTags, tagName) > -1) {
replacements[i] = { text: "/>", newPos: CodeMirror.Pos(pos.line, pos.ch + 2) };
continue;
}
var indent = indentTags && indexOf(indentTags, lowerTagName) > -1;
replacements[i] = {indent: indent,
text: ">" + (indent ? "\n\n" : "") + "</" + tagName + ">",
newPos: indent ? CodeMirror.Pos(pos.line + 1, 0) : CodeMirror.Pos(pos.line, pos.ch + 1)};
}
var dontIndentOnAutoClose = (typeof opt == "object" && opt.dontIndentOnAutoClose);
for (var i = ranges.length - 1; i >= 0; i--) {
var info = replacements[i];
cm.replaceRange(info.text, ranges[i].head, ranges[i].anchor, "+insert");
var sel = cm.listSelections().slice(0);
sel[i] = {head: info.newPos, anchor: info.newPos};
cm.setSelections(sel);
if (!dontIndentOnAutoClose && info.indent) {
cm.indentLine(info.newPos.line, null, true);
cm.indentLine(info.newPos.line + 1, null, true);
}
}
}
function autoCloseCurrent(cm, typingSlash) {
var ranges = cm.listSelections(), replacements = [];
var head = typingSlash ? "/" : "</";
var opt = cm.getOption("autoCloseTags");
var dontIndentOnAutoClose = (typeof opt == "object" && opt.dontIndentOnSlash);
for (var i = 0; i < ranges.length; i++) {
if (!ranges[i].empty()) return CodeMirror.Pass;
var pos = ranges[i].head, tok = cm.getTokenAt(pos);
var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state;
if (typingSlash && (tok.type == "string" || tok.string.charAt(0) != "<" ||
tok.start != pos.ch - 1))
return CodeMirror.Pass;
// Kludge to get around the fact that we are not in XML mode
// when completing in JS/CSS snippet in htmlmixed mode. Does not
// work for other XML embedded languages (there is no general
// way to go from a mixed mode to its current XML state).
var replacement, mixed = inner.mode.name != "xml" && cm.getMode().name == "htmlmixed"
if (mixed && inner.mode.name == "javascript") {
replacement = head + "script";
} else if (mixed && inner.mode.name == "css") {
replacement = head + "style";
} else {
var context = inner.mode.xmlCurrentContext && inner.mode.xmlCurrentContext(state)
if (!context || (context.length && closingTagExists(cm, context, context[context.length - 1], pos)))
return CodeMirror.Pass;
replacement = head + context[context.length - 1]
}
if (cm.getLine(pos.line).charAt(tok.end) != ">") replacement += ">";
replacements[i] = replacement;
}
cm.replaceSelections(replacements);
ranges = cm.listSelections();
if (!dontIndentOnAutoClose) {
for (var i = 0; i < ranges.length; i++)
if (i == ranges.length - 1 || ranges[i].head.line < ranges[i + 1].head.line)
cm.indentLine(ranges[i].head.line);
}
}
function autoCloseSlash(cm) {
if (cm.getOption("disableInput")) return CodeMirror.Pass;
return autoCloseCurrent(cm, true);
}
CodeMirror.commands.closeTag = function(cm) { return autoCloseCurrent(cm); };
function indexOf(collection, elt) {
if (collection.indexOf) return collection.indexOf(elt);
for (var i = 0, e = collection.length; i < e; ++i)
if (collection[i] == elt) return i;
return -1;
}
// If xml-fold is loaded, we use its functionality to try and verify
// whether a given tag is actually unclosed.
function closingTagExists(cm, context, tagName, pos, newTag) {
if (!CodeMirror.scanForClosingTag) return false;
var end = Math.min(cm.lastLine() + 1, pos.line + 500);
var nextClose = CodeMirror.scanForClosingTag(cm, pos, null, end);
if (!nextClose || nextClose.tag != tagName) return false;
// If the immediate wrapping context contains onCx instances of
// the same tag, a closing tag only exists if there are at least
// that many closing tags of that type following.
var onCx = newTag ? 1 : 0
for (var i = context.length - 1; i >= 0; i--) {
if (context[i] == tagName) ++onCx
else break
}
pos = nextClose.to;
for (var i = 1; i < onCx; i++) {
var next = CodeMirror.scanForClosingTag(cm, pos, null, end);
if (!next || next.tag != tagName) return false;
pos = next.to;
}
return true;
}
});

View File

@ -1,101 +0,0 @@
// 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 listRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/,
emptyListRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/,
unorderedListRE = /[*+-]\s/;
CodeMirror.commands.newlineAndIndentContinueMarkdownList = function(cm) {
if (cm.getOption("disableInput")) return CodeMirror.Pass;
var ranges = cm.listSelections(), replacements = [];
for (var i = 0; i < ranges.length; i++) {
var pos = ranges[i].head;
// If we're not in Markdown mode, fall back to normal newlineAndIndent
var eolState = cm.getStateAfter(pos.line);
var inner = CodeMirror.innerMode(cm.getMode(), eolState);
if (inner.mode.name !== "markdown") {
cm.execCommand("newlineAndIndent");
return;
} else {
eolState = inner.state;
}
var inList = eolState.list !== false;
var inQuote = eolState.quote !== 0;
var line = cm.getLine(pos.line), match = listRE.exec(line);
var cursorBeforeBullet = /^\s*$/.test(line.slice(0, pos.ch));
if (!ranges[i].empty() || (!inList && !inQuote) || !match || cursorBeforeBullet) {
cm.execCommand("newlineAndIndent");
return;
}
if (emptyListRE.test(line)) {
var endOfQuote = inQuote && />\s*$/.test(line)
var endOfList = !/>\s*$/.test(line)
if (endOfQuote || endOfList) cm.replaceRange("", {
line: pos.line, ch: 0
}, {
line: pos.line, ch: pos.ch + 1
});
replacements[i] = "\n";
} else {
var indent = match[1], after = match[5];
var numbered = !(unorderedListRE.test(match[2]) || match[2].indexOf(">") >= 0);
var bullet = numbered ? (parseInt(match[3], 10) + 1) + match[4] : match[2].replace("x", " ");
replacements[i] = "\n" + indent + bullet + after;
if (numbered) incrementRemainingMarkdownListNumbers(cm, pos);
}
}
cm.replaceSelections(replacements);
};
// Auto-updating Markdown list numbers when a new item is added to the
// middle of a list
function incrementRemainingMarkdownListNumbers(cm, pos) {
var startLine = pos.line, lookAhead = 0, skipCount = 0;
var startItem = listRE.exec(cm.getLine(startLine)), startIndent = startItem[1];
do {
lookAhead += 1;
var nextLineNumber = startLine + lookAhead;
var nextLine = cm.getLine(nextLineNumber), nextItem = listRE.exec(nextLine);
if (nextItem) {
var nextIndent = nextItem[1];
var newNumber = (parseInt(startItem[3], 10) + lookAhead - skipCount);
var nextNumber = (parseInt(nextItem[3], 10)), itemNumber = nextNumber;
if (startIndent === nextIndent && !isNaN(nextNumber)) {
if (newNumber === nextNumber) itemNumber = nextNumber + 1;
if (newNumber > nextNumber) itemNumber = newNumber + 1;
cm.replaceRange(
nextLine.replace(listRE, nextIndent + itemNumber + nextItem[4] + nextItem[5]),
{
line: nextLineNumber, ch: 0
}, {
line: nextLineNumber, ch: nextLine.length
});
} else {
if (startIndent.length > nextIndent.length) return;
// This doesn't run if the next line immediatley indents, as it is
// not clear of the users intention (new indented item or same level)
if ((startIndent.length < nextIndent.length) && (lookAhead === 1)) return;
skipCount += 1;
}
}
} while (nextItem);
}
});

View File

@ -1,150 +0,0 @@
// 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 ie_lt8 = /MSIE \d/.test(navigator.userAgent) &&
(document.documentMode == null || document.documentMode < 8);
var Pos = CodeMirror.Pos;
var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<", "<": ">>", ">": "<<"};
function bracketRegex(config) {
return config && config.bracketRegex || /[(){}[\]]/
}
function findMatchingBracket(cm, where, config) {
var line = cm.getLineHandle(where.line), pos = where.ch - 1;
var afterCursor = config && config.afterCursor
if (afterCursor == null)
afterCursor = /(^| )cm-fat-cursor($| )/.test(cm.getWrapperElement().className)
var re = bracketRegex(config)
// A cursor is defined as between two characters, but in in vim command mode
// (i.e. not insert mode), the cursor is visually represented as a
// highlighted box on top of the 2nd character. Otherwise, we allow matches
// from before or after the cursor.
var match = (!afterCursor && pos >= 0 && re.test(line.text.charAt(pos)) && matching[line.text.charAt(pos)]) ||
re.test(line.text.charAt(pos + 1)) && matching[line.text.charAt(++pos)];
if (!match) return null;
var dir = match.charAt(1) == ">" ? 1 : -1;
if (config && config.strict && (dir > 0) != (pos == where.ch)) return null;
var style = cm.getTokenTypeAt(Pos(where.line, pos + 1));
var found = scanForBracket(cm, Pos(where.line, pos + (dir > 0 ? 1 : 0)), dir, style || null, config);
if (found == null) return null;
return {from: Pos(where.line, pos), to: found && found.pos,
match: found && found.ch == match.charAt(0), forward: dir > 0};
}
// bracketRegex is used to specify which type of bracket to scan
// should be a regexp, e.g. /[[\]]/
//
// Note: If "where" is on an open bracket, then this bracket is ignored.
//
// Returns false when no bracket was found, null when it reached
// maxScanLines and gave up
function scanForBracket(cm, where, dir, style, config) {
var maxScanLen = (config && config.maxScanLineLength) || 10000;
var maxScanLines = (config && config.maxScanLines) || 1000;
var stack = [];
var re = bracketRegex(config)
var lineEnd = dir > 0 ? Math.min(where.line + maxScanLines, cm.lastLine() + 1)
: Math.max(cm.firstLine() - 1, where.line - maxScanLines);
for (var lineNo = where.line; lineNo != lineEnd; lineNo += dir) {
var line = cm.getLine(lineNo);
if (!line) continue;
var pos = dir > 0 ? 0 : line.length - 1, end = dir > 0 ? line.length : -1;
if (line.length > maxScanLen) continue;
if (lineNo == where.line) pos = where.ch - (dir < 0 ? 1 : 0);
for (; pos != end; pos += dir) {
var ch = line.charAt(pos);
if (re.test(ch) && (style === undefined || cm.getTokenTypeAt(Pos(lineNo, pos + 1)) == style)) {
var match = matching[ch];
if (match && (match.charAt(1) == ">") == (dir > 0)) stack.push(ch);
else if (!stack.length) return {pos: Pos(lineNo, pos), ch: ch};
else stack.pop();
}
}
}
return lineNo - dir == (dir > 0 ? cm.lastLine() : cm.firstLine()) ? false : null;
}
function matchBrackets(cm, autoclear, config) {
// Disable brace matching in long lines, since it'll cause hugely slow updates
var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000;
var marks = [], ranges = cm.listSelections();
for (var i = 0; i < ranges.length; i++) {
var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, config);
if (match && cm.getLine(match.from.line).length <= maxHighlightLen) {
var style = match.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket";
marks.push(cm.markText(match.from, Pos(match.from.line, match.from.ch + 1), {className: style}));
if (match.to && cm.getLine(match.to.line).length <= maxHighlightLen)
marks.push(cm.markText(match.to, Pos(match.to.line, match.to.ch + 1), {className: style}));
}
}
if (marks.length) {
// Kludge to work around the IE bug from issue #1193, where text
// input stops going to the textare whever this fires.
if (ie_lt8 && cm.state.focused) cm.focus();
var clear = function() {
cm.operation(function() {
for (var i = 0; i < marks.length; i++) marks[i].clear();
});
};
if (autoclear) setTimeout(clear, 800);
else return clear;
}
}
function doMatchBrackets(cm) {
cm.operation(function() {
if (cm.state.matchBrackets.currentlyHighlighted) {
cm.state.matchBrackets.currentlyHighlighted();
cm.state.matchBrackets.currentlyHighlighted = null;
}
cm.state.matchBrackets.currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets);
});
}
CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) {
if (old && old != CodeMirror.Init) {
cm.off("cursorActivity", doMatchBrackets);
if (cm.state.matchBrackets && cm.state.matchBrackets.currentlyHighlighted) {
cm.state.matchBrackets.currentlyHighlighted();
cm.state.matchBrackets.currentlyHighlighted = null;
}
}
if (val) {
cm.state.matchBrackets = typeof val == "object" ? val : {};
cm.on("cursorActivity", doMatchBrackets);
}
});
CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);});
CodeMirror.defineExtension("findMatchingBracket", function(pos, config, oldConfig){
// Backwards-compatibility kludge
if (oldConfig || typeof config == "boolean") {
if (!oldConfig) {
config = config ? {strict: true} : null
} else {
oldConfig.strict = config
config = oldConfig
}
}
return findMatchingBracket(this, pos, config)
});
CodeMirror.defineExtension("scanForBracket", function(pos, dir, style, config){
return scanForBracket(this, pos, dir, style, config);
});
});

View File

@ -1,66 +0,0 @@
// 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"), require("../fold/xml-fold"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror", "../fold/xml-fold"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
CodeMirror.defineOption("matchTags", false, function(cm, val, old) {
if (old && old != CodeMirror.Init) {
cm.off("cursorActivity", doMatchTags);
cm.off("viewportChange", maybeUpdateMatch);
clear(cm);
}
if (val) {
cm.state.matchBothTags = typeof val == "object" && val.bothTags;
cm.on("cursorActivity", doMatchTags);
cm.on("viewportChange", maybeUpdateMatch);
doMatchTags(cm);
}
});
function clear(cm) {
if (cm.state.tagHit) cm.state.tagHit.clear();
if (cm.state.tagOther) cm.state.tagOther.clear();
cm.state.tagHit = cm.state.tagOther = null;
}
function doMatchTags(cm) {
cm.state.failedTagMatch = false;
cm.operation(function() {
clear(cm);
if (cm.somethingSelected()) return;
var cur = cm.getCursor(), range = cm.getViewport();
range.from = Math.min(range.from, cur.line); range.to = Math.max(cur.line + 1, range.to);
var match = CodeMirror.findMatchingTag(cm, cur, range);
if (!match) return;
if (cm.state.matchBothTags) {
var hit = match.at == "open" ? match.open : match.close;
if (hit) cm.state.tagHit = cm.markText(hit.from, hit.to, {className: "CodeMirror-matchingtag"});
}
var other = match.at == "close" ? match.open : match.close;
if (other)
cm.state.tagOther = cm.markText(other.from, other.to, {className: "CodeMirror-matchingtag"});
else
cm.state.failedTagMatch = true;
});
}
function maybeUpdateMatch(cm) {
if (cm.state.failedTagMatch) doMatchTags(cm);
}
CodeMirror.commands.toMatchingTag = function(cm) {
var found = CodeMirror.findMatchingTag(cm, cm.getCursor());
if (found) {
var other = found.at == "close" ? found.open : found.close;
if (other) cm.extendSelection(other.to, other.from);
}
};
});

View File

@ -1,27 +0,0 @@
// 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) {
CodeMirror.defineOption("showTrailingSpace", false, function(cm, val, prev) {
if (prev == CodeMirror.Init) prev = false;
if (prev && !val)
cm.removeOverlay("trailingspace");
else if (!prev && val)
cm.addOverlay({
token: function(stream) {
for (var l = stream.string.length, i = l; i && /\s/.test(stream.string.charAt(i - 1)); --i) {}
if (i > stream.pos) { stream.pos = i; return null; }
stream.pos = l;
return "trailingspace";
},
name: "trailingspace"
});
});
});

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