initial upload of new Javascript Frontend based on ExtJS (by Johannes)
- contains ExtJS Library 4.1.1a, together with css and images - is related to the module 93_DbLog.pm, which holds some functions used by the frontend git-svn-id: https://svn.fhem.de/fhem/trunk@2767 2b470e98-0d58-463d-a4d8-8e2adae1ed80
@ -1,4 +1,5 @@
|
||||
- SVN
|
||||
- feature: added new Javascript Frontend based on ExtJS (by Johannes)
|
||||
- feature: new Modules 30_HUEDevice and 31_HUEBridge for phillips hue and
|
||||
smartlink devices (by justme1968)
|
||||
- change: SYSSTAT: allow remote monitoring by ssh
|
||||
@ -94,7 +95,7 @@
|
||||
- feature: devicepair in 10_CUL_HM.pm supports unset
|
||||
- feature: devicepair for single Button in 10_CUL_HM.pm (by MartinP)
|
||||
- feature: new Modules 75_MSG.pm, 76_MSGFile.pm and 76_MSGMail.pm (by
|
||||
Rüdiger)
|
||||
R<EFBFBD>diger)
|
||||
- feature: new Module 59_Twilight.pm to calculate current daylight
|
||||
- feature: internal NotifyOrderPrefix: 98_average.pm is more straightforward
|
||||
- feature: the usb command tries to flash unflashed CULs on linux
|
||||
|
@ -519,3 +519,6 @@
|
||||
|
||||
- Sat Aug 11 2012 (M. Fischer)
|
||||
- Added new module IPCAM
|
||||
|
||||
- Tue Feb 19 2013 (Johannes)
|
||||
- added new Javascript Frontend based on ExtJS (by Johannes)
|
||||
|
10
fhem/www/frontend/README.txt
Normal file
@ -0,0 +1,10 @@
|
||||
This is the readme of the new Webfrontend, based on ExtJS.
|
||||
As there is no full documentation available at the moment,
|
||||
please refer to this thread on the forums of FHEM to get help, ask questions or get updates:
|
||||
|
||||
http://forum.fhem.de/index.php?t=msg&th=10439&start=0&rid=0
|
||||
|
||||
The ExtJS Library as well as the application itself are available under the GPLv3 License.
|
||||
See the license.txt in the lib folder for details
|
||||
|
||||
@author J. Weskamm <jweskamm at gmx.net>
|
57
fhem/www/frontend/app/app.js
Normal file
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Setup the application
|
||||
*/
|
||||
|
||||
Ext.Loader.setConfig({
|
||||
enabled: true,
|
||||
disableCaching: false,
|
||||
paths: {
|
||||
'FHEM': 'app'
|
||||
}
|
||||
});
|
||||
|
||||
Ext.application({
|
||||
name: 'FHEM Frontend',
|
||||
requires: [
|
||||
'FHEM.view.Viewport'
|
||||
],
|
||||
|
||||
controllers: [
|
||||
'FHEM.controller.MainController',
|
||||
'FHEM.controller.ChartController'
|
||||
],
|
||||
|
||||
launch: function() {
|
||||
|
||||
// Gather information from FHEM to display status, devices, etc.
|
||||
var me = this,
|
||||
url = '../../../fhem?cmd=jsonlist&XHR=1';
|
||||
|
||||
Ext.Ajax.request({
|
||||
method: 'GET',
|
||||
async: false,
|
||||
disableCaching: false,
|
||||
url: url,
|
||||
success: function(response){
|
||||
var json = Ext.decode(response.responseText);
|
||||
FHEM.version = json.Results[0].devices[0].ATTR.version;
|
||||
|
||||
Ext.each(json.Results, function(result) {
|
||||
//TODO: get more specific here...
|
||||
if (result.list === "DbLog" && result.devices[0].NAME) {
|
||||
FHEM.dblogname = result.devices[0].NAME;
|
||||
}
|
||||
});
|
||||
if (!FHEM.dblogname && Ext.isEmpty(FHEM.dblogname)) {
|
||||
Ext.Msg.alert("Error", "Could not find a DbLog Configuration. Do you have DbLog already running?");
|
||||
} else {
|
||||
Ext.create("FHEM.view.Viewport");
|
||||
}
|
||||
},
|
||||
failure: function() {
|
||||
Ext.Msg.alert("Error", "The connection to FHEM could not be established");
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
});
|
380
fhem/www/frontend/app/controller/ChartController.js
Normal file
@ -0,0 +1,380 @@
|
||||
/**
|
||||
* Controller handling the charts
|
||||
*/
|
||||
Ext.define('FHEM.controller.ChartController', {
|
||||
extend: 'Ext.app.Controller',
|
||||
|
||||
refs: [
|
||||
{
|
||||
selector: 'datefield[name=starttimepicker]',
|
||||
ref: 'starttimepicker' //this.getStarttimepicker()
|
||||
},
|
||||
{
|
||||
selector: 'datefield[name=endtimepicker]',
|
||||
ref: 'endtimepicker' //this.getEndtimepicker()
|
||||
},
|
||||
{
|
||||
selector: 'button[name=requestchartdata]',
|
||||
ref: 'requestchartdatabtn' //this.getRequestchartdatabtn()
|
||||
},
|
||||
{
|
||||
selector: 'button[name=savechartdata]',
|
||||
ref: 'savechartdatabtn' //this.getSavechartdatabtn()
|
||||
},
|
||||
{
|
||||
selector: 'combobox[name=devicecombo]',
|
||||
ref: 'devicecombo' //this.getDevicecombo()
|
||||
},
|
||||
{
|
||||
selector: 'combobox[name=xaxiscombo]',
|
||||
ref: 'xaxiscombo' //this.getXaxiscombo()
|
||||
},
|
||||
{
|
||||
selector: 'combobox[name=yaxiscombo]',
|
||||
ref: 'yaxiscombo' //this.getYaxiscombo()
|
||||
},
|
||||
{
|
||||
selector: 'linechartview',
|
||||
ref: 'linechartview' //this.getLinechartview()
|
||||
},
|
||||
{
|
||||
selector: 'linechartpanel',
|
||||
ref: 'linechartpanel' //this.getLinechartpanel()
|
||||
},
|
||||
{
|
||||
selector: 'linechartpanel toolbar',
|
||||
ref: 'linecharttoolbar' //this.getLinecharttoolbar()
|
||||
},
|
||||
{
|
||||
selector: 'grid[name=savedchartsgrid]',
|
||||
ref: 'savedchartsgrid' //this.getSavedchartsgrid()
|
||||
}
|
||||
|
||||
|
||||
],
|
||||
|
||||
/**
|
||||
* init function to register listeners
|
||||
*/
|
||||
init: function() {
|
||||
this.control({
|
||||
'combobox[name=devicecombo]': {
|
||||
select: this.deviceSelected
|
||||
},
|
||||
'button[name=requestchartdata]': {
|
||||
click: this.requestChartData
|
||||
},
|
||||
'button[name=savechartdata]': {
|
||||
click: this.saveChartData
|
||||
},
|
||||
'button[name=stepback]': {
|
||||
click: this.stepchange
|
||||
},
|
||||
'button[name=stepforward]': {
|
||||
click: this.stepchange
|
||||
},
|
||||
'linechartview': {
|
||||
afterrender: this.enableZoomInChart
|
||||
},
|
||||
'grid[name=savedchartsgrid]': {
|
||||
cellclick: this.loadsavedchart
|
||||
},
|
||||
'actioncolumn[name=savedchartsactioncolumn]': {
|
||||
click: this.deletechart
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* loads data for the readingsstore after device has been selected
|
||||
*/
|
||||
deviceSelected: function(combo){
|
||||
|
||||
var device = combo.getValue(),
|
||||
store = this.getYaxiscombo().getStore(),
|
||||
proxy = store.getProxy();
|
||||
|
||||
if (proxy) {
|
||||
proxy.url = '../../../fhem?cmd=get+' + FHEM.dblogname + '+-+webchart+""+""+' + device + '+getreadings&XHR=1';
|
||||
store.load();
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Triggers a request to FHEM Module to get the data from Database
|
||||
*/
|
||||
requestChartData: function() {
|
||||
|
||||
var me = this;
|
||||
//getting the necessary values
|
||||
var device = me.getDevicecombo().getValue(),
|
||||
xaxis = me.getXaxiscombo().getValue(),
|
||||
yaxis = me.getYaxiscombo().getValue(),
|
||||
starttime = me.getStarttimepicker().getValue(),
|
||||
dbstarttime = Ext.Date.format(starttime, 'Y-m-d_H:i:s'),
|
||||
endtime = me.getEndtimepicker().getValue(),
|
||||
dbendtime = Ext.Date.format(endtime, 'Y-m-d_H:i:s'),
|
||||
view = me.getLinechartview(),
|
||||
store = me.getLinechartview().getStore(),
|
||||
proxy = store.getProxy();
|
||||
|
||||
//register store listeners
|
||||
store.on("beforeload", function() {
|
||||
me.getLinechartview().setLoading(true);
|
||||
});
|
||||
store.on("load", function() {
|
||||
me.getLinechartview().setLoading(false);
|
||||
});
|
||||
|
||||
if (proxy) {
|
||||
var url = '../../../fhem?cmd=get+' + FHEM.dblogname + '+-+webchart+' + dbstarttime + '+' + dbendtime + '+';
|
||||
url +=device + '+timerange+' + xaxis + '+' + yaxis + '&XHR=1';
|
||||
proxy.url = url;
|
||||
store.load();
|
||||
}
|
||||
|
||||
//remove the old max values of y axis to get a dynamic range
|
||||
delete view.axes.get(0).maximum;
|
||||
|
||||
view.axes.get(0).setTitle(yaxis);
|
||||
view.axes.get(1).setTitle(xaxis);
|
||||
|
||||
// set the x axis range dependent on user given timerange
|
||||
view.axes.get(1).fromDate = starttime;
|
||||
view.axes.get(1).toDate = endtime;
|
||||
view.axes.get(1).processView();
|
||||
|
||||
me.getLinechartview().redraw();
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* perpare zooming
|
||||
*/
|
||||
enableZoomInChart: function() {
|
||||
var view = this.getLinechartview();
|
||||
view.mon(view.getEl(), 'mousewheel', this.zoomInChart, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* zoom in chart with mousewheel
|
||||
*/
|
||||
zoomInChart: function(e) {
|
||||
var wheeldelta = e.getWheelDelta(),
|
||||
view = this.getLinechartview(),
|
||||
currentmax = view.axes.get(0).prevMax,
|
||||
newmax;
|
||||
|
||||
if (wheeldelta == 1) { //zoomin case:
|
||||
if (currentmax > 1) {
|
||||
newmax = currentmax - 1;
|
||||
view.axes.get(0).maximum = newmax;
|
||||
view.redraw();
|
||||
}
|
||||
} else if (wheeldelta == -1) { //zoomout case
|
||||
newmax = currentmax + 1;
|
||||
view.axes.get(0).maximum = newmax;
|
||||
view.redraw();
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* jump one step back / forward in timerange
|
||||
*/
|
||||
stepchange: function(btn) {
|
||||
var me = this;
|
||||
|
||||
var starttime = me.getStarttimepicker().getValue(),
|
||||
dbstarttime = Ext.Date.format(starttime, 'Y-m-d H:i:s'),
|
||||
endtime = me.getEndtimepicker().getValue(),
|
||||
dbendtime = Ext.Date.format(endtime, 'Y-m-d H:i:s');
|
||||
|
||||
if(!Ext.isEmpty(starttime) && !Ext.isEmpty(endtime)) {
|
||||
var timediff = Ext.Date.getElapsed(starttime, endtime);
|
||||
if(btn.name === "stepback") {
|
||||
me.getEndtimepicker().setValue(starttime);
|
||||
var newstarttime = Ext.Date.add(starttime, Ext.Date.MILLI, -timediff);
|
||||
me.getStarttimepicker().setValue(newstarttime);
|
||||
me.requestChartData();
|
||||
|
||||
} else if (btn.name === "stepforward") {
|
||||
me.getStarttimepicker().setValue(endtime);
|
||||
var newendtime = Ext.Date.add(endtime, Ext.Date.MILLI, timediff);
|
||||
me.getEndtimepicker().setValue(newendtime);
|
||||
me.requestChartData();
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* save the current chart to database
|
||||
*/
|
||||
saveChartData: function() {
|
||||
|
||||
var me = this;
|
||||
Ext.Msg.prompt("Select a name", "Enter a name to save the Chart", function(action, savename) {
|
||||
if (action === "ok" && !Ext.isEmpty(savename)) {
|
||||
|
||||
//getting all devices, check for same name
|
||||
var store = me.getSavedchartsgrid().getStore(),
|
||||
storednames = [];
|
||||
|
||||
store.each(function(record){
|
||||
var name = record.get('VALUE');
|
||||
storednames.push(name);
|
||||
});
|
||||
|
||||
if (Ext.Array.contains(storednames, savename)) {
|
||||
Ext.Msg.alert("Error", "There already is a chart with the name: " + savename);
|
||||
} else {
|
||||
|
||||
var device = this.getDevicecombo().getValue(),
|
||||
xaxis = this.getXaxiscombo().getValue(),
|
||||
yaxis = this.getYaxiscombo().getValue(),
|
||||
starttime = this.getStarttimepicker().getValue(),
|
||||
dbstarttime = Ext.Date.format(starttime, 'Y-m-d_H:i:s'),
|
||||
endtime = this.getEndtimepicker().getValue(),
|
||||
dbendtime = Ext.Date.format(endtime, 'Y-m-d_H:i:s'),
|
||||
view = this.getLinechartview();
|
||||
|
||||
var url = '../../../fhem?cmd=get+' + FHEM.dblogname + '+-+webchart+' + dbstarttime + '+' + dbendtime + '+';
|
||||
url +=device + '+savechart+' + xaxis + '+' + yaxis + '+' + savename + '&XHR=1';
|
||||
|
||||
view.setLoading(true);
|
||||
|
||||
Ext.Ajax.request({
|
||||
method: 'GET',
|
||||
disableCaching: false,
|
||||
url: url,
|
||||
success: function(response){
|
||||
view.setLoading(false);
|
||||
var json = Ext.decode(response.responseText);
|
||||
if (json.success === true) {
|
||||
me.getSavedchartsgrid().getStore().load();
|
||||
Ext.Msg.alert("Success", "Chart successfully saved!");
|
||||
} else if (json.msg) {
|
||||
Ext.Msg.alert("Error", "The Chart could not be saved, error Message is:<br><br>" + json.msg);
|
||||
} else {
|
||||
Ext.Msg.alert("Error", "The Chart could not be saved!");
|
||||
}
|
||||
},
|
||||
failure: function() {
|
||||
view.setLoading(false);
|
||||
if (json && json.msg) {
|
||||
Ext.Msg.alert("Error", "The Chart could not be saved, error Message is:<br><br>" + json.msg);
|
||||
} else {
|
||||
Ext.Msg.alert("Error", "The Chart could not be saved!");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* loading saved chart data and trigger the load of the chart
|
||||
*/
|
||||
loadsavedchart: function(grid, td, cellIndex, record) {
|
||||
|
||||
if (cellIndex === 0) {
|
||||
var name = record.get('VALUE');
|
||||
var chartdata = Ext.decode(record.get('EVENT'))[0];
|
||||
|
||||
if (chartdata && !Ext.isEmpty(chartdata)) {
|
||||
this.getDevicecombo().setValue(chartdata.device);
|
||||
// load storedata for readings after device has been selected
|
||||
this.deviceSelected(this.getDevicecombo());
|
||||
|
||||
this.getXaxiscombo().setValue(chartdata.x);
|
||||
this.getYaxiscombo().setValue(chartdata.y);
|
||||
this.getStarttimepicker().setValue(chartdata.starttime);
|
||||
this.getEndtimepicker().setValue(chartdata.endtime);
|
||||
|
||||
this.requestChartData();
|
||||
this.getLinechartpanel().setTitle(name);
|
||||
} else {
|
||||
Ext.Msg.alert("Error", "The Chart could not be loaded!");
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a chart by its name from the database
|
||||
*/
|
||||
deletechart: function(grid, td, cellIndex, par, evt, record) {
|
||||
|
||||
var me = this,
|
||||
chartname = record.get('VALUE'),
|
||||
view = this.getLinechartview();
|
||||
|
||||
if (Ext.isDefined(chartname) && chartname !== "") {
|
||||
|
||||
Ext.create('Ext.window.Window', {
|
||||
width: 250,
|
||||
layout: 'fit',
|
||||
title:'Delete?',
|
||||
modal: true,
|
||||
items: [
|
||||
{
|
||||
xtype: 'displayfield',
|
||||
value: 'Do you really want to delete this chart?'
|
||||
}
|
||||
],
|
||||
buttons: [{
|
||||
text: "Ok",
|
||||
handler: function(btn){
|
||||
|
||||
var url = '../../../fhem?cmd=get+' + FHEM.dblogname + '+-+webchart+""+""+""+deletechart+""+""+' + chartname + '&XHR=1';
|
||||
|
||||
view.setLoading(true);
|
||||
|
||||
Ext.Ajax.request({
|
||||
method: 'GET',
|
||||
disableCaching: false,
|
||||
url: url,
|
||||
success: function(response){
|
||||
view.setLoading(false);
|
||||
var json = Ext.decode(response.responseText);
|
||||
if (json && json.success === true) {
|
||||
me.getSavedchartsgrid().getStore().load();
|
||||
Ext.Msg.alert("Success", "Chart successfully deleted!");
|
||||
} else if (json && json.msg) {
|
||||
Ext.Msg.alert("Error", "The Chart could not be deleted, error Message is:<br><br>" + json.msg);
|
||||
} else {
|
||||
Ext.Msg.alert("Error", "The Chart could not be deleted!");
|
||||
}
|
||||
btn.up().up().destroy();
|
||||
},
|
||||
failure: function() {
|
||||
view.setLoading(false);
|
||||
if (json && json.msg) {
|
||||
Ext.Msg.alert("Error", "The Chart could not be deleted, error Message is:<br><br>" + json.msg);
|
||||
} else {
|
||||
Ext.Msg.alert("Error", "The Chart could not be deleted!");
|
||||
}
|
||||
btn.up().up().destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
text: "Cancel",
|
||||
handler: function(btn){
|
||||
btn.up().up().destroy();
|
||||
}
|
||||
}]
|
||||
}).show();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
80
fhem/www/frontend/app/controller/MainController.js
Normal file
@ -0,0 +1,80 @@
|
||||
/**
|
||||
* The Main Controller handling Main Application Logic
|
||||
*/
|
||||
Ext.define('FHEM.controller.MainController', {
|
||||
extend: 'Ext.app.Controller',
|
||||
|
||||
refs: [
|
||||
{
|
||||
selector: 'viewport[name=mainviewport]',
|
||||
ref: 'mainviewport' //this.getMainviewport()
|
||||
},
|
||||
{
|
||||
selector: 'text[name=statustextfield]',
|
||||
ref: 'statustextfield' //this.getStatustextfield()
|
||||
},
|
||||
{
|
||||
selector: 'panel[name=culpanel]',
|
||||
ref: 'culpanel' //this.getCulpanel()
|
||||
}
|
||||
|
||||
],
|
||||
|
||||
/**
|
||||
* init function to register listeners
|
||||
*/
|
||||
init: function() {
|
||||
this.control({
|
||||
'viewport[name=mainviewport]': {
|
||||
afterrender: this.viewportRendered
|
||||
},
|
||||
'panel[name=linechartaccordionpanel]': {
|
||||
expand: this.showLineChartPanel
|
||||
},
|
||||
'panel[name=tabledataaccordionpanel]': {
|
||||
expand: this.showDatabaseTablePanel
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* load the FHEM devices and state on viewport render completion
|
||||
*/
|
||||
viewportRendered: function(){
|
||||
|
||||
if (Ext.isDefined(FHEM.version)) {
|
||||
var sp = this.getStatustextfield();
|
||||
sp.setText(FHEM.version);
|
||||
}
|
||||
|
||||
// var cp = me.getCulpanel();
|
||||
// if (result.list === "CUL") {
|
||||
// var culname = result.devices[0].NAME;
|
||||
// cp.add(
|
||||
// {
|
||||
// xtype: 'text',
|
||||
// text: culname
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
showLineChartPanel: function() {
|
||||
Ext.ComponentQuery.query('panel[name=tabledatagridpanel]')[0].hide();
|
||||
Ext.ComponentQuery.query('panel[name=linechartpanel]')[0].show();
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
showDatabaseTablePanel: function() {
|
||||
//TODO: use this when new dblog module is deployed
|
||||
//Ext.ComponentQuery.query('panel[name=linechartpanel]')[0].hide();
|
||||
//Ext.ComponentQuery.query('panel[name=tabledatagridpanel]')[0].show();
|
||||
}
|
||||
|
||||
});
|
16
fhem/www/frontend/app/model/ChartModel.js
Normal file
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Model for Charts
|
||||
*/
|
||||
Ext.define('FHEM.model.ChartModel', {
|
||||
extend: 'Ext.data.Model',
|
||||
fields: [
|
||||
{
|
||||
name: 'TIMESTAMP',
|
||||
type: 'date',
|
||||
dateFormat: "Y-m-d H:i:s"
|
||||
},{
|
||||
name: 'VALUE',
|
||||
type: 'float'
|
||||
}
|
||||
]
|
||||
});
|
12
fhem/www/frontend/app/model/DeviceModel.js
Normal file
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Model for Devices
|
||||
*/
|
||||
Ext.define('FHEM.model.DeviceModel', {
|
||||
extend: 'Ext.data.Model',
|
||||
fields: [
|
||||
{
|
||||
name: 'DEVICE',
|
||||
type: 'text'
|
||||
}
|
||||
]
|
||||
});
|
12
fhem/www/frontend/app/model/ReadingsModel.js
Normal file
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Model for Readings
|
||||
*/
|
||||
Ext.define('FHEM.model.ReadingsModel', {
|
||||
extend: 'Ext.data.Model',
|
||||
fields: [
|
||||
{
|
||||
name: 'READING',
|
||||
type: 'text'
|
||||
}
|
||||
]
|
||||
});
|
15
fhem/www/frontend/app/model/SavedChartsModel.js
Normal file
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Model for saved Charts
|
||||
*/
|
||||
Ext.define('FHEM.model.SavedChartsModel', {
|
||||
extend: 'Ext.data.Model',
|
||||
fields: [
|
||||
{
|
||||
name: 'VALUE',
|
||||
type: 'text'
|
||||
},{
|
||||
name: 'EVENT',
|
||||
type: 'text'
|
||||
}
|
||||
]
|
||||
});
|
37
fhem/www/frontend/app/model/TableDataModel.js
Normal file
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Model for DatabaseTables
|
||||
*/
|
||||
Ext.define('FHEM.model.TableDataModel', {
|
||||
extend: 'Ext.data.Model',
|
||||
fields: [
|
||||
{
|
||||
name: 'TIMESTAMP',
|
||||
type: 'date',
|
||||
dateFormat: "Y-m-d H:i:s"
|
||||
},
|
||||
{
|
||||
name: 'DEVICE',
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
name: 'TYPE',
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
name: 'EVENT',
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
name: 'READING',
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
name: 'VALUE',
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
name: 'UNIT',
|
||||
type: 'text'
|
||||
}
|
||||
]
|
||||
});
|
18
fhem/www/frontend/app/store/ChartStore.js
Normal file
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Store for the Charts
|
||||
*/
|
||||
Ext.define('FHEM.store.ChartStore', {
|
||||
extend: 'Ext.data.Store',
|
||||
model: 'FHEM.model.ChartModel',
|
||||
proxy: {
|
||||
type: 'ajax',
|
||||
method: 'POST',
|
||||
url: '', //gets set by controller
|
||||
reader: {
|
||||
type: 'json',
|
||||
root: 'data',
|
||||
totalProperty: 'totalCount'
|
||||
}
|
||||
},
|
||||
autoLoad: false
|
||||
});
|
18
fhem/www/frontend/app/store/DeviceStore.js
Normal file
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Store for the Devices
|
||||
*/
|
||||
Ext.define('FHEM.store.DeviceStore', {
|
||||
extend: 'Ext.data.Store',
|
||||
model: 'FHEM.model.DeviceModel',
|
||||
proxy: {
|
||||
type: 'ajax',
|
||||
method: 'POST',
|
||||
url: '../../../fhem?cmd=get+' + FHEM.dblogname + '+-+webchart+""+""+""+getdevices&XHR=1',
|
||||
reader: {
|
||||
type: 'json',
|
||||
root: 'data',
|
||||
totalProperty: 'totalCount'
|
||||
}
|
||||
},
|
||||
autoLoad: false
|
||||
});
|
18
fhem/www/frontend/app/store/ReadingsStore.js
Normal file
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Store for the Readings
|
||||
*/
|
||||
Ext.define('FHEM.store.ReadingsStore', {
|
||||
extend: 'Ext.data.Store',
|
||||
model: 'FHEM.model.ReadingsModel',
|
||||
proxy: {
|
||||
type: 'ajax',
|
||||
method: 'POST',
|
||||
url: '', //gets set by controller after device has been selected
|
||||
reader: {
|
||||
type: 'json',
|
||||
root: 'data',
|
||||
totalProperty: 'totalCount'
|
||||
}
|
||||
},
|
||||
autoLoad: false
|
||||
});
|
18
fhem/www/frontend/app/store/SavedChartsStore.js
Normal file
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Store for the saved Charts
|
||||
*/
|
||||
Ext.define('FHEM.store.SavedChartsStore', {
|
||||
extend: 'Ext.data.Store',
|
||||
model: 'FHEM.model.SavedChartsModel',
|
||||
proxy: {
|
||||
type: 'ajax',
|
||||
method: 'POST',
|
||||
url: '../../../fhem?cmd=get+' + FHEM.dblogname + '+-+webchart+""+""+""+getcharts&XHR=1',
|
||||
reader: {
|
||||
type: 'json',
|
||||
root: 'data',
|
||||
totalProperty: 'totalCount'
|
||||
}
|
||||
},
|
||||
autoLoad: true
|
||||
});
|
30
fhem/www/frontend/app/store/TableDataStore.js
Normal file
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Store for the TableData from Database
|
||||
*/
|
||||
Ext.define('FHEM.store.TableDataStore', {
|
||||
extend: 'Ext.data.Store',
|
||||
model: 'FHEM.model.TableDataModel',
|
||||
buffered: true,
|
||||
trailingBufferZone: 200,
|
||||
leadingBufferZone: 200,
|
||||
//remoteGroup: true,
|
||||
pageSize: 200,
|
||||
proxy: {
|
||||
type: 'ajax',
|
||||
method: 'POST',
|
||||
url: '../../../fhem?cmd=get+' + FHEM.dblogname + '+-+webchart+""+""+""+getTableData+""+""+""+0+100&XHR=1',
|
||||
reader: {
|
||||
type: 'json',
|
||||
root: 'data',
|
||||
totalProperty: 'totalCount'
|
||||
}
|
||||
},
|
||||
autoLoad: true,
|
||||
listeners: {
|
||||
beforeprefetch: function(store, operation) {
|
||||
//override stores url to contain start and limit params in our needed notation
|
||||
store.proxy.url = '../../../fhem?cmd=get+' + FHEM.dblogname + '+-+webchart+""+""+""';
|
||||
store.proxy.url += '+getTableData+""+""+""+' + operation.start +'+' + operation.limit +'&XHR=1';
|
||||
}
|
||||
}
|
||||
});
|
127
fhem/www/frontend/app/view/LineChartPanel.js
Normal file
@ -0,0 +1,127 @@
|
||||
/**
|
||||
* The Panel containing the Line Charts
|
||||
*/
|
||||
Ext.define('FHEM.view.LineChartPanel', {
|
||||
extend: 'Ext.panel.Panel',
|
||||
alias : 'widget.linechartpanel',
|
||||
xtype : 'chart',
|
||||
requires: [
|
||||
'FHEM.view.LineChartView',
|
||||
'FHEM.store.ChartStore'
|
||||
],
|
||||
|
||||
title: 'Line Chart',
|
||||
|
||||
/**
|
||||
* init function
|
||||
*/
|
||||
initComponent: function() {
|
||||
|
||||
var me = this;
|
||||
|
||||
// set up the local db columnname store
|
||||
// as these columns are fixed, we dont have to request them
|
||||
me.comboAxesStore = Ext.create('Ext.data.Store', {
|
||||
fields: ['name'],
|
||||
data : [
|
||||
{'name':'TIMESTAMP'},
|
||||
{'name':'DEVICE'},
|
||||
{'name':'TYPE'},
|
||||
{'name':'EVENT'},
|
||||
{'name':'READING'},
|
||||
{'name':'VALUE'},
|
||||
{'name':'UNIT'}
|
||||
]
|
||||
});
|
||||
|
||||
me.comboDeviceStore = Ext.create('FHEM.store.DeviceStore');
|
||||
me.comboDeviceStore.on("load", function(store, e, success) {
|
||||
if(!success) {
|
||||
Ext.Msg.alert("Error", "Connection to database failed! Check your configuration.");
|
||||
}
|
||||
});
|
||||
|
||||
me.comboReadingsStore = Ext.create('FHEM.store.ReadingsStore');
|
||||
|
||||
me.dockedItems = [{
|
||||
xtype: 'toolbar',
|
||||
dock: 'top',
|
||||
layout: 'column',
|
||||
minheight: 60,
|
||||
maxHeight: 90,
|
||||
items: [
|
||||
{
|
||||
xtype: 'combobox',
|
||||
name: 'devicecombo',
|
||||
fieldLabel: 'Select Device',
|
||||
store: me.comboDeviceStore,
|
||||
displayField: 'DEVICE',
|
||||
valueField: 'DEVICE'
|
||||
},
|
||||
{
|
||||
xtype: 'combobox',
|
||||
name: 'xaxiscombo',
|
||||
fieldLabel: 'Select X Axis',
|
||||
store: me.comboAxesStore,
|
||||
displayField: 'name',
|
||||
valueField: 'name'
|
||||
},
|
||||
{
|
||||
xtype: 'combobox',
|
||||
name: 'yaxiscombo',
|
||||
fieldLabel: 'Select Y Axis',
|
||||
store: me.comboReadingsStore,
|
||||
displayField: 'READING',
|
||||
valueField: 'READING'
|
||||
},
|
||||
{
|
||||
xtype: 'datefield',
|
||||
name: 'starttimepicker',
|
||||
format: 'Y-m-d H:i:s',
|
||||
fieldLabel: 'Select Starttime'
|
||||
},
|
||||
{
|
||||
xtype: 'datefield',
|
||||
name: 'endtimepicker',
|
||||
format: 'Y-m-d H:i:s',
|
||||
fieldLabel: 'Select Endtime'
|
||||
},
|
||||
{
|
||||
xtype: 'button',
|
||||
width: 100,
|
||||
text: 'Show Chart',
|
||||
name: 'requestchartdata'
|
||||
},
|
||||
{
|
||||
xtype: 'button',
|
||||
width: 100,
|
||||
text: 'Save Chart',
|
||||
name: 'savechartdata'
|
||||
},
|
||||
{
|
||||
xtype: 'button',
|
||||
width: 100,
|
||||
text: 'Step back',
|
||||
name: 'stepback'
|
||||
},
|
||||
{
|
||||
xtype: 'button',
|
||||
width: 100,
|
||||
text: 'Step forward',
|
||||
name: 'stepforward'
|
||||
}
|
||||
]
|
||||
}];
|
||||
|
||||
me.items = [
|
||||
{
|
||||
xtype: 'linechartview',
|
||||
width: '100%'
|
||||
}
|
||||
];
|
||||
|
||||
me.callParent(arguments);
|
||||
|
||||
}
|
||||
|
||||
});
|
65
fhem/www/frontend/app/view/LineChartView.js
Normal file
@ -0,0 +1,65 @@
|
||||
/**
|
||||
* The View for the Line Charts
|
||||
*/
|
||||
Ext.define('FHEM.view.LineChartView', {
|
||||
extend : 'Ext.chart.Chart',
|
||||
alias : 'widget.linechartview',
|
||||
xtype : 'chart',
|
||||
requires : [ 'FHEM.store.ChartStore' ],
|
||||
style : 'background:#fff',
|
||||
animate : true,
|
||||
shadow : true,
|
||||
theme : 'Category1',
|
||||
|
||||
initComponent : function() {
|
||||
var me = this;
|
||||
me.store = Ext.create('FHEM.store.ChartStore');
|
||||
|
||||
me.axes = [ {
|
||||
type : 'Numeric',
|
||||
name : 'yaxe',
|
||||
position : 'left',
|
||||
fields : [ 'VALUE' ],
|
||||
title : 'kW / h',
|
||||
grid : {
|
||||
odd : {
|
||||
opacity : 1,
|
||||
fill : '#ddd',
|
||||
stroke : '#bbb',
|
||||
'stroke-width' : 0.5
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type : 'Time',
|
||||
name : 'xaxe',
|
||||
position : 'bottom',
|
||||
fields : [ 'TIMESTAMP' ],
|
||||
dateFormat : "Y-m-d H:i:s",
|
||||
minorTickSteps : 12,
|
||||
title : 'Time'
|
||||
} ];
|
||||
|
||||
me.series = [ {
|
||||
type : 'line',
|
||||
axis : 'left',
|
||||
xField : 'TIMESTAMP',
|
||||
yField : 'VALUE',
|
||||
smooth: 2,
|
||||
fill: true,
|
||||
highlight: true,
|
||||
tips : {
|
||||
trackMouse : true,
|
||||
width : 140,
|
||||
height : 100,
|
||||
renderer : function(storeItem, item) {
|
||||
this.setTitle(' Value: : ' + storeItem.get('VALUE') +
|
||||
'<br> Time: ' + storeItem.get('TIMESTAMP'));
|
||||
}
|
||||
}
|
||||
} ];
|
||||
|
||||
me.callParent(arguments);
|
||||
|
||||
}
|
||||
|
||||
});
|
73
fhem/www/frontend/app/view/TableDataGridPanel.js
Normal file
@ -0,0 +1,73 @@
|
||||
/**
|
||||
* The GridPanel containing a table with rawdata from Database
|
||||
*/
|
||||
Ext.define('FHEM.view.TableDataGridPanel', {
|
||||
extend: 'Ext.panel.Panel',
|
||||
alias : 'widget.tabledatagridpanel',
|
||||
//xtype : 'gridpanel',
|
||||
requires: [
|
||||
'FHEM.store.TableDataStore'
|
||||
],
|
||||
|
||||
title: 'Table Data',
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
initComponent: function() {
|
||||
|
||||
var me = this;
|
||||
|
||||
var tablestore = Ext.create('FHEM.store.TableDataStore');
|
||||
|
||||
me.items = [
|
||||
{
|
||||
xtype: 'panel',
|
||||
items: [
|
||||
{
|
||||
xtype: 'fieldset',
|
||||
title: 'Configure Database Query',
|
||||
items: [
|
||||
{
|
||||
xtype: 'displayfield',
|
||||
value: 'The configuration of the Databasequery will follow here...'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
xtype: 'gridpanel',
|
||||
height: 400,
|
||||
collapsible: true,
|
||||
store: tablestore,
|
||||
width: '100%',
|
||||
loadMask: true,
|
||||
selModel: {
|
||||
pruneRemoved: false
|
||||
},
|
||||
multiSelect: true,
|
||||
viewConfig: {
|
||||
trackOver: false
|
||||
},
|
||||
verticalScroller:{
|
||||
//trailingBufferZone: 20, // Keep 200 records buffered in memory behind scroll
|
||||
//leadingBufferZone: 50 // Keep 5000 records buffered in memory ahead of scroll
|
||||
},
|
||||
columns: [
|
||||
{ text: 'TIMESTAMP', dataIndex: 'TIMESTAMP' },
|
||||
{ text: 'DEVICE', dataIndex: 'DEVICE' },
|
||||
{ text: 'TYPE', dataIndex: 'TYPE' },
|
||||
{ text: 'EVENT', dataIndex: 'EVENT' },
|
||||
{ text: 'READING', dataIndex: 'READING' },
|
||||
{ text: 'VALUE', dataIndex: 'VALUE' },
|
||||
{ text: 'UNIT', dataIndex: 'UNIT' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
me.callParent(arguments);
|
||||
|
||||
}
|
||||
|
||||
});
|
168
fhem/www/frontend/app/view/Viewport.js
Normal file
@ -0,0 +1,168 @@
|
||||
/**
|
||||
* The main application viewport, which displays the whole application
|
||||
* @extends Ext.Viewport
|
||||
*/
|
||||
Ext.define('FHEM.view.Viewport', {
|
||||
extend: 'Ext.Viewport',
|
||||
name: 'mainviewport',
|
||||
layout: 'border',
|
||||
requires: [
|
||||
'FHEM.view.LineChartPanel',
|
||||
'FHEM.view.TableDataGridPanel',
|
||||
'FHEM.controller.ChartController'
|
||||
],
|
||||
|
||||
initComponent: function() {
|
||||
var me = this;
|
||||
|
||||
Ext.apply(me, {
|
||||
items: [
|
||||
{
|
||||
region: 'north',
|
||||
html: '<p align="center"><img align="center" src="../../fhem/images/default/fhemicon.png" height="70px"</></p><h1 class="x-panel-header" align="center">Frontend</h1>',
|
||||
height: 85
|
||||
}, {
|
||||
region: 'west',
|
||||
title: 'Navigation',
|
||||
width: 200,
|
||||
xtype: 'panel',
|
||||
layout: 'accordion',
|
||||
items: [
|
||||
{
|
||||
xtype: 'panel',
|
||||
name: 'culpanel',
|
||||
title: 'CUL'
|
||||
},
|
||||
{
|
||||
xtype: 'panel',
|
||||
title: 'LineChart',
|
||||
name: 'linechartaccordionpanel',
|
||||
layout: 'fit',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{
|
||||
xtype: 'grid',
|
||||
columns: [
|
||||
{
|
||||
header: 'Saved Charts',
|
||||
dataIndex: 'VALUE',
|
||||
width: '80%'
|
||||
},
|
||||
{
|
||||
xtype:'actioncolumn',
|
||||
name: 'savedchartsactioncolumn',
|
||||
width:'15%',
|
||||
items: [{
|
||||
icon: 'lib/ext-4.1.1a/images/gray/dd/drop-no.gif',
|
||||
tooltip: 'Delete'
|
||||
}]
|
||||
}
|
||||
],
|
||||
store: Ext.create('FHEM.store.SavedChartsStore', {}),
|
||||
name: 'savedchartsgrid'
|
||||
|
||||
}
|
||||
]
|
||||
},
|
||||
// {
|
||||
// xtype: 'panel',
|
||||
// title: 'BarChart',
|
||||
// name: 'barchartpanel',
|
||||
// layout: 'fit',
|
||||
// collapsed: false,
|
||||
// items: [
|
||||
// {
|
||||
// xtype: 'grid',
|
||||
// columns: [
|
||||
// {
|
||||
// header: 'Saved Charts',
|
||||
// dataIndex: 'VALUE',
|
||||
// width: '80%'
|
||||
// },
|
||||
// {
|
||||
// xtype:'actioncolumn',
|
||||
// name: 'savedchartsactioncolumn',
|
||||
// width:'15%',
|
||||
// items: [{
|
||||
// icon: 'lib/ext-4.1.1a/images/gray/dd/drop-no.gif',
|
||||
// tooltip: 'Delete'
|
||||
// }]
|
||||
// }
|
||||
// ],
|
||||
// store: Ext.create('FHEM.store.SavedChartsStore', {}),
|
||||
// name: 'savedchartsgrid'
|
||||
//
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
{
|
||||
xtype: 'panel',
|
||||
title: 'Database Tables',
|
||||
name: 'tabledataaccordionpanel'
|
||||
},
|
||||
{
|
||||
xtype: 'panel',
|
||||
title: 'Unsorted'
|
||||
},
|
||||
{
|
||||
xtype: 'panel',
|
||||
title: 'Everything'
|
||||
},
|
||||
{
|
||||
xtype: 'panel',
|
||||
title: 'Wiki'
|
||||
},
|
||||
{
|
||||
xtype: 'panel',
|
||||
title: 'Details'
|
||||
},
|
||||
{
|
||||
xtype: 'panel',
|
||||
title: 'Definition...'
|
||||
},
|
||||
{
|
||||
xtype: 'panel',
|
||||
title: 'Edit files'
|
||||
},
|
||||
{
|
||||
xtype: 'panel',
|
||||
title: 'Select style'
|
||||
},
|
||||
{
|
||||
xtype: 'panel',
|
||||
title: 'Event monitor'
|
||||
}
|
||||
]
|
||||
}, {
|
||||
xtype: 'panel',
|
||||
region: 'south',
|
||||
title: 'Status',
|
||||
collapsible: true,
|
||||
items: [{
|
||||
xtype: 'text',
|
||||
name: 'statustextfield',
|
||||
text: 'Status...'
|
||||
}],
|
||||
split: true,
|
||||
height: 50,
|
||||
minHeight: 30
|
||||
},
|
||||
{
|
||||
xtype: 'linechartpanel',
|
||||
name: 'linechartpanel',
|
||||
region: 'center',
|
||||
layout: 'fit'
|
||||
},
|
||||
{
|
||||
xtype: 'tabledatagridpanel',
|
||||
name: 'tabledatagridpanel',
|
||||
hidden: true,
|
||||
region: 'center',
|
||||
layout: 'fit'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
me.callParent(arguments);
|
||||
}
|
||||
});
|
14
fhem/www/frontend/index.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
|
||||
"http://www.w3.org/TR/html4/loose.dtd">
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
|
||||
<title>FHEM Frontend</title>
|
||||
<link rel="stylesheet" type="text/css" href="lib/ext-4.1.1a/ext-all-gray-debug.css" />
|
||||
<script type="text/javascript" src="lib/ext-4.1.1a/ext-all.js"></script>
|
||||
<script type="text/javascript" src="app/app.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
9958
fhem/www/frontend/lib/ext-4.1.1a/ext-all-gray-debug.css
Normal file
38
fhem/www/frontend/lib/ext-4.1.1a/ext-all.js
Normal file
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 1010 B |
BIN
fhem/www/frontend/lib/ext-4.1.1a/images/gray/box/corners.gif
Normal file
After Width: | Height: | Size: 1005 B |
BIN
fhem/www/frontend/lib/ext-4.1.1a/images/gray/box/l-blue.gif
Normal file
After Width: | Height: | Size: 810 B |
BIN
fhem/www/frontend/lib/ext-4.1.1a/images/gray/box/l.gif
Normal file
After Width: | Height: | Size: 810 B |
BIN
fhem/www/frontend/lib/ext-4.1.1a/images/gray/box/r-blue.gif
Normal file
After Width: | Height: | Size: 810 B |
BIN
fhem/www/frontend/lib/ext-4.1.1a/images/gray/box/r.gif
Normal file
After Width: | Height: | Size: 810 B |
BIN
fhem/www/frontend/lib/ext-4.1.1a/images/gray/box/tb-blue.gif
Normal file
After Width: | Height: | Size: 851 B |
BIN
fhem/www/frontend/lib/ext-4.1.1a/images/gray/box/tb.gif
Normal file
After Width: | Height: | Size: 839 B |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.9 KiB |