[Урок 16] . Рисуем свой интерфейс и панель настроек бота в iMacros+JS

Активно разрабатывая ботов под iMacros, быстро привыкаешь все настройки и данные бота хранить в переменных, либо в файлах. Когда пишешь бота для себя, это не вызывает никаких проблем, но если заниматься коммерческой разработкой, то правилом хорошего тона является наличие простого, понятного и удобного интерфейса. Многие iMacros-разработчики привыкли писать в коде комментарии напротив каждой переменной, чтобы облегчить для заказчика работу с ботом. Тем не менее, на просторах интернета я мельком встречал скрины, где народ кастомизировал окошки iMacros, менял типографику, увеличивал панели для вывода информации и т.д.

Я решил разобраться, как сделать свой интерфейс и внедрить его в браузер. О чем, я вам сегодня и поведаю.

На пути к интерфейсу — «XUL Layout»

Как я уже упоминал в прошлых уроках, Firefox использует XUL. Для тех, кто подзабыл что это за зверь, напомню.

XUL (XML User Interface Language) — это основанный на XML язык разметки, используемый в приложениях Mozilla. XUL позволяет создавать кроссплатформенные приложения с интерфейсами любой сложности, как использующие интернет, так и работающие локально. В приложениях легко изменить графику, всплывающие подсказки и расположение элементов так, чтобы их интерфейс отвечал требованиям бренда или был переведен на любой язык.

Немного гугла спустя я выяснил, что для построения нативных компонентов в браузере мне придется использовать не стандартную html-разметку, а xml-образную. Я посмотрел на примеры кода и мне этот вариант сразу не понравился.

Сами взгляните:

<vbox> 
	<button id="start" label="Start"/> 
	<button id="stop" label="Stop"/> 
	<button id="cancel" label="Cancel"/>
</vbox>

Вроде ничего страшного на первый взгляд. Но, я хочу иметь максимум возможностей. При подобном подходе нам придется делать инлайновые стили или искать костыль для внедрения своей css(таблицей стилей), чтобы интерфейс был не совсем уродливым. Еще в голове сразу мелькают мысли, о том, насколько геморрно будет подключать всякие библиотеки с красивыми графиками, прогрессбарами, анимациями и т.д.

К черту, отметаем!

О, iframe!

Немного подумав, я понял, что самый оптимальный формат под мои требования — это если бы я мог каким-либо образом подключить обычную локально созданную веб-страницу со всеми стилями, скриптами, шрифтами и т.д.

Тем не менее, я решил проверить, как ведут себя обычные html-элементы, если их нарисовать на панели iMacros.

// Получаем доступ к панели iMacros
var Cc = Components.classes,
	Ci = Components.interfaces,
	wm = Cc["@mozilla.org/appshell/window-mediator;1"]
		.getService(Ci.nsIWindowMediator)
		.getMostRecentWindow("navigator:browser");
	
var mainWindow = wm.iMacros.panel.sidebar;

// Создаем классический див и добавляем его в сааамый низ
var div = window.document.createElement('div');
div.className = "info-box";
div.innerHTML = "Тестим дефолтный стиль для strong Текст.";

mainWindow.document.querySelector('#Recorder').appendChild(div);

Подобным образом я сразу проверил создание input, textarea и был огорчен. Они не работали, в них ничего вводить нельзя. Вместо них необходимо использовать элементы, типа <textbox> из XUL, о чем я писал выше. Да и создавать вручную интерфейс подобным образом — дело неблагодарное и утомительное.

Покопавшись внутри встроенных функций Firefox я откопал способы создания новых невидимых вкладок и еще какую-то фигню. Полчаса спустя, мне это надоело, и я решил попробывать iframe. Единственное, что меня пугало, это очень возможные проблемы с передачей контекста iMacros в контекст iframe. Выражаясь проще, мы имеем функцию в iMacros, а нам необходимо навесить обработчик на кнопку, которая находится внутри не только панели iMacros, но и еще внутри iframe, из которого родительское окно вряд ли будет доступно. Общение между ифреймом и родительским окном работает, если оба документа находятся на одном домене. Но, в нашем случае, трудно представить, как поведет себя браузер, когда вместо родительского окна — сам браузер, а документ в ифрейме открывается по протоколу file:///. В спецификации, я такой ситуации не припомню.

Отлично, поэкспериментируем!

Добавляем iframe в панель iMacros

Сразу хочу отметить, что в примере выше, мы использовали функцию appendChild() для вставки одного элемента в DOM-дерево.
На этот раз мы будем использовать более удобную и продвинутую функцию insertAdjacentHTML(position, html), которая преобразует текст с тегами в DOM-дерево и вставляет в определенное место.  Эта функция принимает два параметра.
Первый параметр указывает в какое место вставлять разметку. Он может принимать одно из четырех строковых значений:

  • «beforebegin»
  • «afterbegin»
  • «beforeend»
  • «afterend»

Вот наглядная схема, как это работает при вставке в тег <p>:

<!-- beforebegin -->
<p>
<!-- afterbegin -->
foo
<!-- beforeend -->
</p>
<!-- afterend -->

Нам нужен вариант с «afterbegin» — чтобы наша панелька находилась в самом верху. Второй параметр функции  insertAdjacentHTML() — это html-код или обычный текст.

Давайте попробуем!

var Cc = Components.classes, 
	Ci = Components.interfaces, 
	wm = Cc["@mozilla.org/appshell/window-mediator;1"] 
		.getService(Ci.nsIWindowMediator) 
		.getMostRecentWindow("navigator:browser"); 

var mainWindow = wm.iMacros.panel.sidebar;

var html = ` 
	<iframe id="ifr" style="width: 100%;height:400px;background:white;"></iframe>
`;

// Добавляем интерфейс в панель iMacros
mainWindow.document.querySelector('#Recorder').insertAdjacentHTML('afterbegin', html);

Вот, что у меня получилось:

imacros iframe

Слева вы видите большой белый квадрат, это и есть наш iframe. Он пока пустой, но выглядит уже неплохо. Единственное, я бы хотел полностью закрыть панель iMacros, чтобы она вообще не была видна и доступна после загрузки бота. Наверное, кто-то начнет возмущаться, типа нельзя делать недоступной кнопки Play и Pause. На что я отвечаю, что не только можно, но и нужно. Если вы пишите ботов только при помощи встроенной функции записи и в формате .iim, то у вас проблем не будет. При написании серьезных скриптов, iMacros используется исключительно, как интерпретатор Javascript, обходя встроенные механизмы для контроля над воспроизведением скриптов простых смертных. И такие скрипты, особенно с тиковым мониторингом, не желательно запускать дважды не перезапустив браузер, дабы избежать некорректного их выполнения.  iMacros под каждый новый запуск даже одного и того же скрипта запускает отдельный инстанс, вместо того, чтобы использовать один и тот же. Вы можете попасть в ситуацию, когда у вас паралельно работают два одинаковых скрипта и выполняют одни и те же действия — а это не очень хорошо.

Еще хочу отметить одну деталь, наш интерфейс будет пропадать, если нажать на кнопку скрытия\показа сайдбара iMacros. Это происходит, потому что при данном действии iMacros перерисовывает заново всю панель, и если перед этим создали новый файл в папке iMacros не через iMacros — то после такого переключения он сразу появится. Чтбоы избежать подобного, можно запихать интерфейс не в панель iMacros, а в какой-нибудь другой уголок браузера.

Делаем HTML-разметку интерфейса

Предлагаю начать с чего-нибудь простого. Создайте файл ui.htm в папке iMacros. Я накидал простую формочку со стилями и добавил панельку, куда мы будем выводить логи и сообщать о всяческой активности.

<!DOCTYPE html>
<html lang="ru">
<head> 
<meta charset="UTF-8"> 
<title>Nagibaka Bot</title> 
<style> 
* { font-size: 10px; font-family: Arial; } 
.btn{ display: inline-block; padding: 5px 15px; font-size: 16px; border:1px solid #D0C5C5; cursor: pointer;} 
.btn.primary{ background: #98D397; } 
h1{ font-size: 14px; font-weight: bold; } 
.log{ background: #000; color: #fff; border-radius: 5px; border:none; font-family: Consolas; padding: 10px; font-size: 12px; height: 400px; overflow:auto; margin: 10px 0; } 
.log .time{ font-weight: bold; color:yellow; } .log div{font-size: 12px;} label{ font-size: 14px; float: left; width: 80px; vertical-align: middle; } 
input[type=text] { display: inline-block; width: 200px; text-align: left; padding: 5px; font-size: 14px; vertical-align: middle; } 
.row{ margin-bottom: 10px; line-height: 30px; }
 </style>
</head>

<body>
 
   <h1>Настройки бота</h1> 
 
   <div class="row"> <label>Логин:</label> <input type="text" name="login" placeholder="Введите логин"> </div>
   <div class="row"> <label>Пароль:</label> <input type="text" name="login" placeholder="Введите пароль"> </div>
  
   <hr> 

   <button class="btn primary" id="btn-start">Запуск</button>
   
   <!-- Для логгирования --> 
   <div class="log"></div>
 
</body>
</html>

Как-то паршиво мой плагин на сайте подсветил и отформатировал код, ну да ладно. На нашей форме будет всего два поля, для логина и для пароля. Я решил сразу реализовать автоматическое сохранение значений этих полей, которое будет происходить сразу после события "onchange" — это когда вы что-то изменили в поле ввода и перевели фокус на другой элемент. Можно было бы впилить кнопку «Сохранить настройки», но весь современный мир уже отказался от такого подхода, как например сделал вк.

Финальный код для отрисовки интерфейса

Разбирать каждую строчку кода я не буду, надеюсь хватит и подробных комментариев. Иногда народ жалуется, когда видит мои простыни кода. Не знаю, почему это является для некоторых людей проблемой. В любом нормальном редакторе кода(например в моем любимом Sublime Text 3) есть фолдинг, это когда можно свернуть кусок отформатированного кода или функции. У меня это выглядит, как на скрине ниже. Чем более масштабные приложения вы пишите, тем больше внимания вы должны уделять форматированию кода и построению архитектуры. Так проще и самому разбираться и расширять функционал.

Алгоритм у нас следующий:

  1. Добавляем iframe на панель iMacros(при помощи css он у нас занимает всю область панели)
  2. Ждем 200мс, пока загрузится ui.htm в наш ифрейм
  3. Загружаем настройки, если они есть и добавляем их в поля ввода
  4. Навешиваем обработчики события на кнопку и поля ввода
  5. Профит!

god javascript code

Код программы:

/**
 * UI for iMacros(Demo)
 *
 * @author: Nagibaka <https://nagibaka.ru>
 * 
 */
var APP = {

	// Путь для сохранения конфига
	_configPath: 'C:\\BOTS\\iMacros\\bot.json',


	// Функция отрисовки интерфейса
	drawUI: function () {

		var that = this; // тут this ссылается на APP

		var Cc = Components.classes,
			Ci = Components.interfaces,
			wm = Cc["@mozilla.org/appshell/window-mediator;1"]
				.getService(Ci.nsIWindowMediator)
				.getMostRecentWindow("navigator:browser");
			
		this.mainWindow = wm.iMacros.panel.sidebar;

		// Вычисляем высоту панели iMacros
		var sidebarHeight = this.mainWindow.document.querySelector('#Recorder').clientHeight;
		

		var html = ` 
			<div style="position: relative;"> <
				div style="position:absolute;top: 0px; left: 0px; right: 0px;bottom: 0px;;z-index:999; height: ${sidebarHeight}px;"> 
					<iframe id="ifr" style="width: 100%;height:100%;background:white;" src="file:///c:\\BOTS\\iMacros\\ui.htm"></iframe> 
				</div> 
			</div> 
		`;

		// Добавляем интерфейс в панель iMacros
		this.mainWindow.document.querySelector('#Recorder').insertAdjacentHTML('afterbegin', html);

		// Надеемся, что за 200мс интерфейс загрузится в ифрейм(обычно необходимо 2-3мс), 
		// Навешиваем логику на кнопки и инпуты
		// при ошибках - можно увеличить число
		window.setTimeout(function () {
			that.addEventsOnUI();
		}, 200);

	},

	// Добавляем обработчики событий внутри ифрейма
	addEventsOnUI: function () {

		var that = this;
		var uiDoc = this.mainWindow.document.querySelector('#ifr').contentDocument;

		// Навешиваем обработчик события на "клик"
		uiDoc.querySelector('#btn-start').onclick =  function () {		   	
		   	window.console.log(that);
		   	alert('Клик работает! А если вы посмотрите в консоли - то увидите, что отсюда нам доступен полностью весь объект APP со всеми функциями и методами!');
		    // Тут можно выполнять любые действия
		    // ...
		};

		// Загружаем настройки таблицы
		that.loadConfig();

		// Автосохранение конфига при изменении любого поля
		var inputs = uiDoc.querySelectorAll('.row input[type=text]');
		
		for (var i = 0; i < inputs.length; i++) {
			inputs[i].addEventListener('change', function () {
				that.saveConfig();
			});
		}
	

		that.log('Бот готов к работе.');
		

	},

	// Вывод логов в ифрейме в панель для логов
	log: function (msg) {	

		var date = new window.Date().toLocaleString();

		// Если интерфейс доступен, то выводим лог туда
		if (this.mainWindow != undefined) {
			if (this.mainWindow.document.querySelector('#ifr').contentDocument.querySelector('.log') != null) {
				var uiDoc = this.mainWindow.document.querySelector('#ifr').contentDocument;		
				var html = '<div><span class="time">' + date + '</span> ' + msg + '</div>';
				uiDoc.querySelector('.log').insertAdjacentHTML('afterbegin', html);
			}
		}

	},

	// Функция сохранения в JSON
	saveToJSON: function (fileName, obj) { 
		try {	 
			var fileDescriptor = imns.FIO.openNode(fileName);
			imns.FIO.writeTextFile( fileDescriptor, JSON.stringify(obj, null, 4) );	
			return true;		 
		} catch (e) {		
			return null;
		}	 
	},


	// Функция чтения из JSON
	loadFromJSON: function (filename) {
		try {
			var fileDescriptor = imns.FIO.openNode(filename);
			return JSON.parse(imns.FIO.readTextFile(fileDescriptor));	
		} catch (e) {		
			return null;
		}
	},

	// Сохранение конфига
	saveConfig: function () {

		var that = this;

		var obj  = {
			login: that.mainWindow.document.querySelector('#ifr').contentDocument.querySelector('input[name=login]').value,
			pass: that.mainWindow.document.querySelector('#ifr').contentDocument.querySelector('input[name=pass]').value
		};

		// Сохраняем конфиг в файл JSON
		this.saveToJSON(this._configPath, obj);
		this.log('Конфиг сохранен в файл: ' + this._configPath);

	},

	// Загрузка конфига из файла
	loadConfig: function () {

		var obj = this.loadFromJSON(this._configPath);

		if (obj != null) {
			this.mainWindow.document.querySelector('#ifr').contentDocument.querySelector('input[name=login]').value = obj.login;
			this.mainWindow.document.querySelector('#ifr').contentDocument.querySelector('input[name=pass]').value = obj.pass;
			this.log('Конфиг загружен из файла: ' + this._configPath);
		}		

	},

	// Инициализация бота
	init: function () {

		// Рисуем интерфейс
		this.drawUI();
	}
}


// Инициализируем нашего бота
APP.init();

 

Вот так должно выглядеть:

imacros custom ui интерфейс

Не забываем сохранять файл в кодировке «UTF-8 with BOM«. Иначе вместо русских символов будут кракозябры.

На сегодня всё!

Пишите ваши вопросы и пожелания в комментах.

23 комментариев

  1. \\Еще хочу отметить одну деталь, наш интерфейс будет пропадать, если нажать на кнопку скрытия\показа сайдбара iMacros….Чтбоы избежать подобного, можно запихать интерфейс не в панель iMacros, а в какой-нибудь другой уголок браузера.

    Как добавить на панель после меню «Справка» ?

    Ответить
  2. При выполнении кода программы вылазит ошибка :
    SyntaxError: An invalid or illegal string was specified, line 43 (Error code: -991)

    Ответить
  3. SyntaxError: An invalid or illegal string was specified, line 45 (Error code: -991)
    Ругается на эту строку — that.addEventsOnUI();

    Специально скачал Вашу сборку Firefox 35 и iMacros, грешил на более новый фф — но та же ошибка. В чем может быть проблема?

    Ответить
    • Вопрос снят, проблема была в кавычках.
      Тогда может подскажите — как сделать, чтобы при запуске фф автоматом запускался нужны скрипт, не нужно было его через плей сначала толкать? И если панель имакроса свернуть и развернуть — все не возвращалось в исходное состояние до запуска скрипта?

      Ответить
  4. Евгений, какие кавычки, в каком месте та же проблема
    SyntaxError: An invalid or illegal string was specified, line 45 (Error code: -991)
    Ругается на эту строку — that.addEventsOnUI();

    Ответить
  5. Что делать если например FRAME F= любое значение принимает при каждой перезагрузки страницы? Как поставить постоянное значение?

    Ответить
  6. Да ебанный рот, нихуя не работает, какие нахуй кавычки сука, какая нахуй строка, по человечи напишите. А то блядь один :
    » Кто не разобрался ошибка здесь
    var html = `
    `;
    исправлено.»
    Что сука исправлено, если нхуя не работает ?!
    Или еще один :
    «Да, вытянуть в строку, без переноса, ну у меня так сработало.
    Да нихуя не сработала. Автор ты нормально отредактировать свой интерфейсв состоянии ?! Чтобы люди не допиливая смогли заюзать то о чепм ты так старательно писал.

    Ответить
    • «Jesus
      ебанный рот, нихуя не работает, какие нахуй кавычки сука, какая нахуй блядь сука нхуя не работает нихуя не сработала дрочить во сне»

      Животное, ты что забыло на техническом сайте?
      Не можешь нихрена понять, так кто виноват в дебилизме кроме самого себя? Дрочить хочешь, ну так и дрочи, на сиськиру, а не лезь в коды. Что это за истерика? Кто будет отвечать такой истеричке-имбецилу, которая кроме матов не может не сформулировать вопросы ни прочесть уже сказанное? Да никто, все адекватные разобрались, а обиженной школоте дорога на сиськиру, а не в коды :)))

      зы впервые вижу такой взрыв пукана на техническом сайте, ныть и опять как быдло материться в ответ не стоит, все равно отписался от такой появившейся параши в комментариях от дефективного )))

      Ответить
  7. И конфиг сука твой, хуй сохраняется при изменение полей. Таким интерфейсов только левой пяткой дрочить во сне

    Ответить
  8. У кого голова на плечах есть, тот разберётся. Вопрос к адекватам: вот мы закрыли, всю стандартную панель. Но при необходимости, как остановить работу скрипта? Т.е. как нажать на кнопку «Стоп»?

    Ответить
  9. а обработчик addEventListener у кого-то срабатывает?
    ну или иными словами, конфиг у кого-нибудь сохранился?

    Ответить
  10. И все же, кто-то знает как сделать кнопку «СТОП» а точнее как прописать это событие, и после закрыть фрейм.
    Ничего не выходит((((

    Ответить

Оставить комментарий