Plugin menu entry hook, posm-gcpi plugin skeleton, mount points
|
@ -1,2 +1,4 @@
|
|||
from .plugin_base import PluginBase
|
||||
from .menu import Menu
|
||||
from .mountpoint import MountPoint
|
||||
from .functions import *
|
||||
|
|
|
@ -23,10 +23,18 @@ def get_url_patterns():
|
|||
"""
|
||||
url_patterns = []
|
||||
for plugin in get_active_plugins():
|
||||
for mount_point in plugin.mount_points():
|
||||
url_patterns.append(url('^plugins/{}/{}'.format(plugin.get_name(), mount_point.url),
|
||||
mount_point.view,
|
||||
*mount_point.args,
|
||||
**mount_point.kwargs))
|
||||
|
||||
if plugin.has_public_path():
|
||||
url_patterns.append(url('^plugins/{}/(.*)'.format(plugin.get_name()),
|
||||
django.views.static.serve,
|
||||
{'document_root': plugin.get_path("public")}))
|
||||
|
||||
|
||||
return url_patterns
|
||||
|
||||
plugins = None
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
class Menu:
|
||||
def __init__(self, label, link = "javascript:void(0)", css_icon = 'fa fa-caret-right fa-fw', submenu = []):
|
||||
"""
|
||||
Create a menu
|
||||
:param label: text shown in entry
|
||||
:param css_icon: class used for showing an icon (for example, "fa fa-wrench")
|
||||
:param link: link of entry (use "#" or "javascript:void(0);" for no action)
|
||||
:param submenu: list of Menu items
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.label = label
|
||||
self.css_icon = css_icon
|
||||
self.link = link
|
||||
self.submenu = submenu
|
||||
|
||||
if (self.has_submenu()):
|
||||
self.link = "#"
|
||||
|
||||
|
||||
def has_submenu(self):
|
||||
return len(self.submenu) > 0
|
|
@ -0,0 +1,17 @@
|
|||
import re
|
||||
|
||||
class MountPoint:
|
||||
def __init__(self, url, view, *args, **kwargs):
|
||||
"""
|
||||
|
||||
:param url: path to mount this view to, relative to plugins directory
|
||||
:param view: Django view
|
||||
:param args: extra args to pass to url()
|
||||
:param kwargs: extra kwargs to pass to url()
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.url = re.sub(r'^/+', '', url) # remove leading slashes
|
||||
self.view = view
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
|
@ -24,10 +24,13 @@ class PluginBase(ABC):
|
|||
return self.__class__.__module__
|
||||
|
||||
def get_include_js_urls(self):
|
||||
return ["/plugins/{}/{}".format(self.get_name(), js_file) for js_file in self.include_js_files()]
|
||||
return [self.url(js_file) for js_file in self.include_js_files()]
|
||||
|
||||
def get_include_css_urls(self):
|
||||
return ["/plugins/{}/{}".format(self.get_name(), css_file) for css_file in self.include_css_files()]
|
||||
return [self.url(css_file) for css_file in self.include_css_files()]
|
||||
|
||||
def url(self, path):
|
||||
return "/plugins/{}/{}".format(self.get_name(), path)
|
||||
|
||||
def has_public_path(self):
|
||||
return os.path.isdir(self.get_path("public"))
|
||||
|
@ -48,5 +51,21 @@ class PluginBase(ABC):
|
|||
"""
|
||||
return []
|
||||
|
||||
def main_menu(self):
|
||||
"""
|
||||
Should be overriden by plugins that want to add
|
||||
items to the side menu.
|
||||
:return: [] of Menu objects
|
||||
"""
|
||||
return []
|
||||
|
||||
def mount_points(self):
|
||||
"""
|
||||
Should be overriden by plugins that want to connect
|
||||
custom Django views
|
||||
:return: [] of MountPoint objects
|
||||
"""
|
||||
return []
|
||||
|
||||
def __str__(self):
|
||||
return "[{}]".format(self.get_module_name())
|
|
@ -1,329 +0,0 @@
|
|||
const env = {};
|
||||
(function (environment) {
|
||||
|
||||
/**
|
||||
* List of existings modules
|
||||
* @type {Object}
|
||||
*/
|
||||
var modules = {};
|
||||
|
||||
/**
|
||||
* Array of waiting modules
|
||||
* @type {Array}
|
||||
*/
|
||||
var waitingModules = [];
|
||||
|
||||
/**
|
||||
* Count created script for control
|
||||
* @type {Number}
|
||||
*/
|
||||
var scriptCounter = 1;
|
||||
|
||||
/**
|
||||
* Base element check for IE 6-8
|
||||
* @type {Node}
|
||||
*/
|
||||
var baseElement = document.getElementsByTagName('base')[0];
|
||||
|
||||
/**
|
||||
* Head element
|
||||
* @type {Node}
|
||||
*/
|
||||
var head = document.getElementsByTagName('head')[0];
|
||||
|
||||
/**
|
||||
* @param {String} name the name of the module
|
||||
* @param {Array} deps dependencies of the module
|
||||
* @param {Function} module module definition
|
||||
* @param {String} dir relative dir path from which to load files
|
||||
*/
|
||||
function Include (name, deps, module, dir) {
|
||||
var self = this;
|
||||
|
||||
if (typeof name !== "string") {
|
||||
module = deps;
|
||||
deps = name;
|
||||
name = null;
|
||||
}
|
||||
|
||||
if (deps.constructor !== [].constructor) {
|
||||
module = deps;
|
||||
deps = [];
|
||||
}
|
||||
|
||||
waitingModules.unshift([name, deps, module]);
|
||||
|
||||
/**
|
||||
* Uid for script differentiation
|
||||
* @type {String}
|
||||
*/
|
||||
self.uid = Math.random().toString(36).replace(/[^a-z0-9]+/g, '').substr(0, 10);
|
||||
|
||||
self.checkModuleLoaded();
|
||||
|
||||
if (deps.length) {
|
||||
self.each(deps, self.parseFiles);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Loop trougth an array of element with the given function
|
||||
* @param {Array|NodeList} array array to loop
|
||||
* @param {Function} callback function to execute with each element
|
||||
*/
|
||||
Include.prototype.each = function (array, callback) {
|
||||
var self = this,
|
||||
i;
|
||||
|
||||
for (i = 0; i < array.length; i++) {
|
||||
if (array[i] !== undefined && callback.call(self, array[i], i, array) === false) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get element data id
|
||||
* @param {String} name
|
||||
* @param {Boolean} clean only clean the name
|
||||
* @return {String}
|
||||
*/
|
||||
Include.prototype.getId = function (name, clean) {
|
||||
return (clean ? '' : this.uid + '-') + name.replace(/[^a-z0-9]+/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a module is loaded
|
||||
*/
|
||||
Include.prototype.checkModuleLoaded = function () {
|
||||
var self = this;
|
||||
|
||||
self.each(waitingModules, function (module, i) {
|
||||
var name = module[0],
|
||||
dependencies = module[1],
|
||||
exec = module[2],
|
||||
args = [];
|
||||
|
||||
self.each(dependencies, function (dependencie, n, t) {
|
||||
n = dependencie.push ? dependencie[0] : dependencie;
|
||||
t = document.querySelector('[data-id*="' + self.getId(n, 1) + '"]');
|
||||
|
||||
if (t && t.nodeName == "LINK") {
|
||||
args.push(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (modules[n] !== undefined) {
|
||||
args.push(modules[n]);
|
||||
}
|
||||
});
|
||||
|
||||
if (dependencies.length === args.length || dependencies.length === 0) {
|
||||
if (name === null && i+1 === waitingModules.length) {
|
||||
waitingModules = [];
|
||||
scriptCounter = 1;
|
||||
}
|
||||
|
||||
exec = typeof exec == 'function' ? exec.apply(this, args) : exec;
|
||||
modules[name] = exec;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* onModuleLoaded
|
||||
* @param {String} name name of the module
|
||||
* @param {Number} index index of the module
|
||||
*/
|
||||
Include.prototype.onModuleLoaded = function (name, index) {
|
||||
var self = this;
|
||||
|
||||
// Is this script add a waiting module ? If not, that's a "normal" script file
|
||||
if (index > waitingModules.length) {
|
||||
scriptCounter--;
|
||||
modules[name] = modules[name] || scriptCounter;
|
||||
} else if (waitingModules[0][0] === null) {
|
||||
waitingModules[0][0] = name;
|
||||
}
|
||||
|
||||
self.checkModuleLoaded();
|
||||
}
|
||||
|
||||
/**
|
||||
* On Load event
|
||||
* @param {Event} event event of the load
|
||||
* @param {Function} caller
|
||||
*/
|
||||
Include.prototype.onLoad = function (event, caller) {
|
||||
var self = this,
|
||||
target = (event.currentTarget || event.srcElement);
|
||||
|
||||
//Check if the script is realy loaded and executed
|
||||
if (event.type !== "load" && target.readyState != "complete") {
|
||||
return;
|
||||
}
|
||||
|
||||
target.setAttribute('data-loaded', true);
|
||||
self.onModuleLoaded(target.getAttribute('data-module'), target.getAttribute('data-count'));
|
||||
|
||||
// Old browser need to use the detachEvent method
|
||||
if (target.attachEvent) {
|
||||
target.detachEvent('onreadystatechange', caller);
|
||||
} else {
|
||||
target.removeEventListener('load', caller);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch for css load
|
||||
* @param {Element} elem elem to check loading
|
||||
*/
|
||||
Include.prototype.watchCss = function (elem) {
|
||||
var self = this,
|
||||
sheets = document.styleSheets,
|
||||
i = sheets.length,
|
||||
href = elem.href.split('//').pop();
|
||||
|
||||
// loop on document stylesheets to check if media is loaded
|
||||
while (i--) {
|
||||
if (sheets[i].href.indexOf(href) != -1) {
|
||||
elem.setAttribute('data-loaded', true);
|
||||
self.onModuleLoaded(elem.getAttribute('data-module'), elem.getAttribute('data-count'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
self.watchCss.call(self, elem);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach events to an element
|
||||
* @param {Element} elem elem to attach event
|
||||
* @param {Boolean} isJs is elem a script
|
||||
*/
|
||||
Include.prototype.attachEvents = function (elem, isJs) {
|
||||
var self = this,
|
||||
cb = function () {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
args.push(cb);
|
||||
|
||||
self.onLoad.apply(self, args);
|
||||
};
|
||||
|
||||
|
||||
if (isJs) {
|
||||
if (elem.attachEvent) {
|
||||
elem.attachEvent('onreadystatechange', cb);
|
||||
} else {
|
||||
elem.addEventListener('load', cb, true);
|
||||
}
|
||||
} else {
|
||||
self.watchCss(elem);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a script already load
|
||||
* @param {String} moduleName module to load
|
||||
* @param {String} isJs type of file
|
||||
*/
|
||||
Include.prototype.checkExists = function (moduleName, isJs) {
|
||||
var exists = false;
|
||||
|
||||
this.each(document.getElementsByTagName(isJs ? 'script' : 'link'), function (elem) {
|
||||
if (elem.getAttribute('data-module') && elem.getAttribute('data-module') === moduleName) {
|
||||
exists = elem;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a script element to load asked module
|
||||
* @param {String} moduleName name of the module
|
||||
* @param {String} moduleFile file to include
|
||||
* @param {String} isJs type of file
|
||||
*/
|
||||
Include.prototype.create = function (moduleName, moduleFile, isJs) {
|
||||
var self = this;
|
||||
|
||||
//SetTimeout prevent the element create browser rush
|
||||
setTimeout(function(){
|
||||
var elem = self.checkExists.call(self, moduleName, isJs);
|
||||
|
||||
if (elem) {
|
||||
return;
|
||||
}
|
||||
|
||||
scriptCounter++;
|
||||
|
||||
elem = document.createElement(isJs ? 'script' : 'link');
|
||||
|
||||
if (isJs) {
|
||||
elem.async = true;
|
||||
elem.type = "text/javascript";
|
||||
elem.src = moduleFile;
|
||||
} else {
|
||||
elem.media = "all";
|
||||
elem.href = moduleFile;
|
||||
elem.rel = "stylesheet"
|
||||
}
|
||||
|
||||
elem.setAttribute('data-id', self.getId(moduleName));
|
||||
elem.setAttribute('data-module', moduleName);
|
||||
elem.setAttribute('data-count', scriptCounter);
|
||||
elem.setAttribute('data-loaded', false);
|
||||
|
||||
if (baseElement) {
|
||||
//prevent IE 6-8 bug (script executed before appenchild execution.
|
||||
baseElement.parentNode.insertBefore(elem, baseElement);
|
||||
} else {
|
||||
head.appendChild(elem);
|
||||
}
|
||||
|
||||
self.attachEvents.call(self, elem, isJs);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a file to include
|
||||
* @param {String} file file to parse
|
||||
*/
|
||||
Include.prototype.parseFiles = function (file) {
|
||||
var moduleName = file.push ? file[0] : file,
|
||||
moduleFile = file.push ? file[1] : file,
|
||||
ext;
|
||||
|
||||
//Don't load module already loaded
|
||||
if (modules[moduleName]) {
|
||||
this.checkModuleLoaded();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (moduleFile.indexOf('//') == -1 && !/\.js/.test(moduleFile) && !/^http/.test(moduleFile)) {
|
||||
moduleFile = moduleFile.replace(/\./g, '/');
|
||||
moduleFile = moduleFile + '.js';
|
||||
}
|
||||
|
||||
ext = moduleFile.split('.').pop() == 'js';
|
||||
|
||||
this.create.call(this, moduleName, moduleFile, ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} name the name of the module
|
||||
* @param {Array} deps dependencies of the module
|
||||
* @param {Function} module module definition
|
||||
*/
|
||||
environment['include'] = environment['require'] = environment['define'] = function (name, deps, module) {
|
||||
return new Include(name, deps, module);
|
||||
};
|
||||
|
||||
})(env);
|
||||
|
||||
export default env;
|
|
@ -226,11 +226,27 @@
|
|||
<!--<li>
|
||||
<a href="#"><i class="fa fa-plane fa-fw"></i> Mission Planner</a>
|
||||
</li> -->
|
||||
{% load processingnode_extras %}
|
||||
{% load processingnode_extras plugins %}
|
||||
{% can_view_processing_nodes as view_nodes %}
|
||||
{% can_add_processing_nodes as add_nodes %}
|
||||
{% get_visible_processing_nodes as nodes %}
|
||||
|
||||
{% get_plugins_main_menus as plugin_menus %}
|
||||
{% for menu in plugin_menus %}
|
||||
<li>
|
||||
<a href="{{menu.link}}"><i class="{{menu.css_icon}}"></i> {{menu.label}}{% if menu.has_submenu %}<span class="fa arrow"></span>{% endif %}</a>
|
||||
|
||||
{% if menu.has_submenu %}
|
||||
<ul class="nav nav-second-level">
|
||||
{% for menu in menu.submenu %}
|
||||
<li>
|
||||
<a href="{{menu.link}}"><i class="{{menu.css_icon}}"></i> {{menu.label}}{% if menu.has_submenu %}<span class="fa arrow"></span>{% endif %}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
{% if view_nodes %}
|
||||
<li>
|
||||
|
|
|
@ -15,3 +15,8 @@ def get_plugins_css_includes():
|
|||
# Flatten all urls for all plugins
|
||||
css_urls = list(itertools.chain(*[plugin.get_include_css_urls() for plugin in get_active_plugins()]))
|
||||
return "\n".join(map(lambda url: "<link href='{}' rel='stylesheet' type='text/css'>".format(url), css_urls))
|
||||
|
||||
@register.assignment_tag()
|
||||
def get_plugins_main_menus():
|
||||
# Flatten list of menus
|
||||
return list(itertools.chain(*[plugin.main_menu() for plugin in get_active_plugins()]))
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
from django.test import Client
|
||||
from rest_framework import status
|
||||
|
||||
from .classes import BootTestCase
|
||||
|
||||
class TestPlugins(BootTestCase):
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_core_plugins(self):
|
||||
client = Client()
|
||||
|
||||
# We can access public files core plugins (without auth)
|
||||
res = client.get('/plugins/measure/leaflet-measure.css')
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
|
||||
# TODO:
|
||||
# test API endpoints
|
||||
# test python hooks
|
|
@ -6,7 +6,7 @@ from nodeodm.models import ProcessingNode
|
|||
from .classes import BootTestCase
|
||||
from .utils import start_processing_node
|
||||
|
||||
class TestWelcome(BootTestCase):
|
||||
class TestWorker(BootTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
|
|
|
@ -16,10 +16,10 @@ urlpatterns = [
|
|||
url(r'^3d/project/(?P<project_pk>[^/.]+)/task/(?P<task_pk>[^/.]+)/$', app_views.model_display, name='model_display'),
|
||||
|
||||
url(r'^public/task/(?P<task_pk>[^/.]+)/map/$', public_views.map, name='public_map'),
|
||||
url(r'^public/task/(?P<task_pk>[^/.]+)/iframe/map/$', public_views.map_iframe, name='public_map'),
|
||||
url(r'^public/task/(?P<task_pk>[^/.]+)/3d/$', public_views.model_display, name='public_map'),
|
||||
url(r'^public/task/(?P<task_pk>[^/.]+)/iframe/3d/$', public_views.model_display_iframe, name='public_map'),
|
||||
url(r'^public/task/(?P<task_pk>[^/.]+)/json/$', public_views.task_json, name='public_map'),
|
||||
url(r'^public/task/(?P<task_pk>[^/.]+)/iframe/map/$', public_views.map_iframe, name='public_iframe_map'),
|
||||
url(r'^public/task/(?P<task_pk>[^/.]+)/3d/$', public_views.model_display, name='public_3d'),
|
||||
url(r'^public/task/(?P<task_pk>[^/.]+)/iframe/3d/$', public_views.model_display_iframe, name='public_iframe_3d'),
|
||||
url(r'^public/task/(?P<task_pk>[^/.]+)/json/$', public_views.task_json, name='public_json'),
|
||||
|
||||
url(r'^processingnode/([\d]+)/$', app_views.processing_node, name='processing_node'),
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
from .plugin import *
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "POSM GCP Interface",
|
||||
"webodmMinVersion": "0.5.0",
|
||||
"description": "A plugin to create GCP files from images",
|
||||
"version": "0.1.0",
|
||||
"author": "Piero Toffanin",
|
||||
"email": "pt@masseranolabs.com",
|
||||
"repository": "https://github.com/OpenDroneMap/WebODM",
|
||||
"tags": ["gcp", "posm"],
|
||||
"homepage": "https://github.com/OpenDroneMap/WebODM",
|
||||
"experimental": true,
|
||||
"deprecated": false
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
from app.plugins import PluginBase, Menu, MountPoint
|
||||
from django.shortcuts import render
|
||||
|
||||
class Plugin(PluginBase):
|
||||
|
||||
def register(self):
|
||||
pass
|
||||
|
||||
def main_menu(self):
|
||||
return [Menu("GCP Editor", self.url("index.html"), "fa fa-map-marker fa-fw")]
|
||||
|
||||
def mount_points(self):
|
||||
return [
|
||||
MountPoint("/test", test)
|
||||
]
|
||||
|
||||
|
||||
def test(request):
|
||||
return render(request, 'app/dashboard.html', {'title': 'PLUGIN!!'})
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"main.css": "static/css/main.0b2dd337.css",
|
||||
"main.css.map": "static/css/main.0b2dd337.css.map",
|
||||
"main.js": "static/js/main.df429876.js",
|
||||
"main.js.map": "static/js/main.df429876.js.map",
|
||||
"static/media/add.png": "static/media/add.5a2714f3.png",
|
||||
"static/media/add@2x.png": "static/media/add@2x.b53b9f2d.png",
|
||||
"static/media/add_point.png": "static/media/add_point.e65f1d0c.png",
|
||||
"static/media/add_point@2x.png": "static/media/add_point@2x.bf317640.png",
|
||||
"static/media/add_point_green.png": "static/media/add_point_green.013c6b67.png",
|
||||
"static/media/add_point_green@2x.png": "static/media/add_point_green@2x.1dd546dd.png",
|
||||
"static/media/add_point_yellow.png": "static/media/add_point_yellow.a6d933c3.png",
|
||||
"static/media/add_point_yellow@2x.png": "static/media/add_point_yellow@2x.5b290820.png",
|
||||
"static/media/close.png": "static/media/close.729ab67b.png",
|
||||
"static/media/close@2x.png": "static/media/close@2x.c65c9577.png",
|
||||
"static/media/fit_markers.png": "static/media/fit_markers.be9754ad.png",
|
||||
"static/media/fit_markers@2x.png": "static/media/fit_markers@2x.cf8c8fad.png",
|
||||
"static/media/gcp-green.png": "static/media/gcp-green.cfc5c722.png",
|
||||
"static/media/gcp-yellow.png": "static/media/gcp-yellow.3793065e.png",
|
||||
"static/media/gcp.png": "static/media/gcp.44ed9ab1.png",
|
||||
"static/media/layers-2x.png": "static/media/layers-2x.4f0283c6.png",
|
||||
"static/media/layers.png": "static/media/layers.a6137456.png",
|
||||
"static/media/loading.gif": "static/media/loading.e56d6770.gif",
|
||||
"static/media/loading@2x.gif": "static/media/loading@2x.0ab4b1d1.gif",
|
||||
"static/media/logo.png": "static/media/logo.b38a9426.png",
|
||||
"static/media/marker-icon.png": "static/media/marker-icon.2273e3d8.png",
|
||||
"static/media/point_icon.png": "static/media/point_icon.e206131a.png",
|
||||
"static/media/point_icon@2x.png": "static/media/point_icon@2x.dd1da9a3.png",
|
||||
"static/media/polygon_icon.png": "static/media/polygon_icon.83cffeed.png",
|
||||
"static/media/polygon_icon@2x.png": "static/media/polygon_icon@2x.53277be6.png",
|
||||
"static/media/providers.png": "static/media/providers.ad5af2f5.png",
|
||||
"static/media/providers@2x.png": "static/media/providers@2x.51ed570c.png",
|
||||
"static/media/search.png": "static/media/search.57a8b421.png",
|
||||
"static/media/search@2x.png": "static/media/search@2x.44cf1bbe.png"
|
||||
}
|
Po Szerokość: | Wysokość: | Rozmiar: 1.1 KiB |
|
@ -0,0 +1 @@
|
|||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="shortcut icon" href="/favicon.ico"><title>GCPi</title><link href="/static/css/main.0b2dd337.css" rel="stylesheet"></head><body><div id="root"></div><script type="text/javascript" src="/static/js/main.df429876.js"></script></body></html>
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"sources":[],"names":[],"mappings":"","file":"static/css/main.0b2dd337.css","sourceRoot":""}
|
Po Szerokość: | Wysokość: | Rozmiar: 400 B |
Po Szerokość: | Wysokość: | Rozmiar: 630 B |
Po Szerokość: | Wysokość: | Rozmiar: 564 B |
Po Szerokość: | Wysokość: | Rozmiar: 1003 B |
Po Szerokość: | Wysokość: | Rozmiar: 627 B |
Po Szerokość: | Wysokość: | Rozmiar: 1.0 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 581 B |
Po Szerokość: | Wysokość: | Rozmiar: 990 B |
Po Szerokość: | Wysokość: | Rozmiar: 482 B |
Po Szerokość: | Wysokość: | Rozmiar: 717 B |
Po Szerokość: | Wysokość: | Rozmiar: 569 B |
Po Szerokość: | Wysokość: | Rozmiar: 985 B |
Po Szerokość: | Wysokość: | Rozmiar: 1.1 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 936 B |
Po Szerokość: | Wysokość: | Rozmiar: 958 B |
Po Szerokość: | Wysokość: | Rozmiar: 1.2 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 696 B |
Po Szerokość: | Wysokość: | Rozmiar: 3.9 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 8.0 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 3.7 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 1.4 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 298 B |
Po Szerokość: | Wysokość: | Rozmiar: 549 B |
Po Szerokość: | Wysokość: | Rozmiar: 250 B |
Po Szerokość: | Wysokość: | Rozmiar: 522 B |
Po Szerokość: | Wysokość: | Rozmiar: 378 B |
Po Szerokość: | Wysokość: | Rozmiar: 626 B |
Po Szerokość: | Wysokość: | Rozmiar: 268 B |
Po Szerokość: | Wysokość: | Rozmiar: 359 B |
Po Szerokość: | Wysokość: | Rozmiar: 524 B |
Po Szerokość: | Wysokość: | Rozmiar: 639 B |