Пользователь
0,0
рейтинг
4 марта 2013 в 17:15

Разработка → Пишем своё расширение для браузера Mozilla Firefox tutorial

Итак, после обновления Firefox до 19 версии, полностью отвалилось горячо любимое расширение Яндекс.Бар. Не забуду напомнить, что Яндекс.Бар был заменен Яндекс.Элементами, которые понравились чуть больше, чем никому, поэтому и получили свои заслуженные 2 бала из 5ти.

Почему не понравились? Заменили адресную строку, стало неудобно просматривать почту, заменили закладки и убрали корректор адресной строки (под предлогом установки Punto Switcher, который может и хорош для обычного работника, но никак не для программиста. Поэтому и был удален почти сразу же, как установлен. Да и если можно было бы настроить, то всё равно желание пропало).

Немного погодя было решено создать свое подобное расширение, которое будет включать в себя такие плюшки, как просмотр почты и корректор адресной строки. Чтож, раз не ты, так кто другой?



Первым делом решено было не создавать свой велосипед и воскресить Яндекс.Бар, который не хотел работать в 19 версии браузера. В интернете подсказали, что расширение — это обычный zip архив. Открыли, посмотрели, ужаснулись и закрыли. Воскресить не удалось, даже при всем желании.

Тогда заходим в центр разработчика: builder.addons.mozilla.org. Я предпочел орудовать в веб-редакторе, хоть местами он иногда не очень гладко работал. Посмотрев на другие расширения, позаимствовав код и немного поняв весь смысл сея устройства, началось сначала всё со стенобитной машины и закончилось надфилем.

Билдер включает в себя 3 раздела: это раздел со скриптами (Lib), раздел с загружаемым контентом (картинки, стили и скрипты) и раздел с готовыми библиотеками (Libraries)

Кстати, вот документация: addons.mozilla.org/en-US/developers/docs/sdk/latest, добротно написанная.
Старт расширения начинается с загрузки файла main.js.
Вызывается функция: exports.main.

Пример файла main.js:
const tabs = require("tabs");

exports.main = function (options) {
    
    tabs.on("ready", function(tab){
      tab.attach({
        contentScript: "document.addEventListener('click', function(e) { \
				var target = e.target; \
				if(target.tagName == 'A') { \
                    var mail_to = target.href.match(/^mailto:(.*)/i); \
					if(mail_to != null) { \
						e.preventDefault(); \
						var form = document.createElement('form'); \
						form.setAttribute('action','http://mail.yandex.ru/neo2/#compose/mailto=' + mail_to[1]); \
						form.setAttribute('target','_blank'); \
						document.getElementsByTagName('body')[0].appendChild(form); \
						form.submit(); \
						form.parentNode.removeChild(form); \
					} \
				} \
			}, false);"
      });
    });
}

Что же за магия происходит в этом коде?

Первым делом подключается модуль tabs.
В данном случае он служит для того, чтобы можно было добавлять свой JavaScript код в страницу браузера.
Т.е. что у нас: при событии документа onready происходит добавление любого JavaScript кода в тело документа. В данном примере добавляется обработчик ссылок, у которых адрес начинается с mailto.

Ладно, давайте что-нибудь посложнее сделаем. Добавим-ка свою кнопку в верхний бар!
Опять же, не будем строить велосипеды, а с чистой совестью возьмем уже готовую библиотеку Toolbar Button Complete.

В ней же есть пример добавления кнопки в бар браузера. Я думаю, не стоит его сюда вываливать, т.к. там многоватенько кода.
Итак, кнопка есть, иконку поставили, всё вроде хорошо, но не очень. Как же у нас в Яндекс.Баре было? Ах да, напротив иконки еще и счетчик непрочитанных сообщений был.
Тут я разузнал несколько путей добавления счетчика:
  • универсальный, но более легкий (с помощью стилей)
  • не слишком универсальный, но не такой простой, как первый (с помощью canvas)

Второй способ, правда, нашелся методом тыка в интернет. Но я взял первый.
Нам известно, что верхний бар — это такой же набор элементов со своими классами, идентификаторами, свойствами и способами работы с ними.

Методом тыка типа:
for(var val in document.getElementById('yandex-menu')) {
   console.log(val);
}

было обнаружено, что методы в точности совпадают с теми, что мы обычно используем при работе с элементами сайта. Но замечу, что по стандарту браузер не знает, что такое ни document, ни window в расширениях (да и еще есть отличия).

Пример решения:
var wuntils = require('sdk/window/utils');
var window = wuntils.getMostRecentBrowserWindow();
var document = window.document;

Замечу, что разработка билдера не стоит на месте и если раньше способ получения активного окна был таким:
var winUtils = require("window-utils");
for (window in winUtils.windowIterator()) {
   if ("chrome://browser/content/browser.xul" != window.location)  return;
   console.log("An open window! " + window.location);
}
то сейчас всё намного легче (пример я выше привел).

Чтож, немного рассказав о особенностях, вернусь к добавлению счетчика для кнопки.
Умные люди подсказали, что по стандарту стиль поля label у кнопки равен display: none;, поэтому как-то нужно было внедрить свой css код в бар. Решение, как оказалось, не сложное (советую завернуть в файл, который будет инклюдится по мере надобности):
const { Cc, Ci } = require('chrome');
const { when: unload } = require('sdk/system/unload');
 
var ios = Cc['@mozilla.org/network/io-service;1'].getService(Ci.nsIIOService);
 
/* Helper that registers style sheets and remembers to unregister on unload */
exports.addXULStylesheet = function addXULStylesheet(url) {
    var uri = newURI(url);
	var sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService);
	
	sss.loadAndRegisterSheet(uri, sss.USER_SHEET);
	unload(function () {
		if (sss.sheetRegistered(uri, sss.USER_SHEET)) {
			sss.unregisterSheet(uri, sss.USER_SHEET);
		}
	});
    
    return sss;
};
 
function newURI(uriStr, base) {
	try {
		var baseURI = base ? ios.newURI(base, null, null) : null;
		return ios.newURI(uriStr, null, baseURI);
	}
	catch (e) {
		if (e.result === chrome.Cr.NS_ERROR_MALFORMED_URI) {
			throw new Error("malformed URI: " + uriStr);
		} else if (e.result === chrome.Cr.NS_ERROR_FAILURE ||
			e.result === chrome.Cr.NS_ERROR_ILLEGAL_VALUE) {
			throw new Error("invalid URI: " + uriStr);
		}
	}
	return null;
}


И в функцию exprorts.main добавляем что-то вроде (хотя добавлять можете куда угодно):
stylesheet.addXULStylesheet(data.url("stylesheet.css"));

не забыв создать в контенте файл stylesheet.css.

У меня файл содержит примерно следующее:
#yandex-mail {
    min-width: 16px;
}
#yandex-mail .toolbarbutton-text { 
    float: right !important;
    display: inline-block !important;
    font-size: 13px;
    padding-left:20px;
    background: url(data:image/png;base64,iVB.............OCYII=) no-repeat left center;
}
#yandex-mail .toolbarbutton-icon { 
    display: none;
}

Почему мы скрываем иконку и добавляем фон? Всё потому, что если этого не сделать, то блоки всегда отображаются как display: block, какие бы значения я не выставлял (кстати, может кто знает по этой теме?) Поэтому и приходится так хитрить.

Также столкнулся с вопросом загрузки контента с других сайтов и парсинг xml.
С первым быстро разобрался, далеко ходить не надо: Request
А вот со вторым пришлось повозиться.

Как мы знаем, получить dom xml документа можно с помощью нескольких функций:
  • XMLHttpRequest — отпал, т.к. выдало ошибку кроссдоменного запроса (может я не так что-то делал?)
  • DOMParser — но тут тоже пришлось повозиться

В чем собственно возня: как и с получением window, так и тут:
var {Cc, Ci} = require("chrome");
var parser = Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
var dom = parser.parseFromString(xmlPrepare (text), "application/xml");


Вот так создание расширений для Firefox ничем не отличается от создания плагинов для jQuery :)

Кстати, конечное творение на сей день: CustomYandexBar, пока находится на проверке. Исходники, в них много чего полезного.

Если кому-нибудь не понравится, что использую «их» картинки, бренд или т.п. — пишите.
Роман @lampa
карма
10,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (48)

  • +32
    — Итак, после обновления Firefox до 19 версии, полностью отвалился горячо любимый плагин Яндекс.Бар.
    Разве это не хорошая новость?
    • –5
      Мне было удобно, когда стоял данный плагин. Причины описаны в статье.
    • +13
      Однозначно хорошая. Никогда не любил всякие бары, особенно за их навязчивость.
    • 0
      Я использовал бар для перевода слов на странице в лингво наведением на них курсора мыши.

      Да, держать целый бар ради одной этой функции как-то жирно, но другого аддона с этой функциональностью найти не удалось.
      • 0
        Ну, «элементы» это таки умеют, хотя часть их функций отключить стоит сразу после установки
        • 0
          Элементы оказались для меня бесполезны в этом плане. Упорно переводят с русского на англияский, хотя мне нужно наоборот :-D
          Подозреваю, что из-за того что у меня LC_MESSAGES=en_US.utf8 (всё остальное, включая LANG — ru_RU.utf8).

          Снём элементы нафиг.
          • 0
            Ну да, в плане допиленности они никакие — лично я постеснялся бы настолько сырой продукт выводить на рынок, да ещё ломая устоявшиеся привычки пользователей и сжигая мосты. В этом плане руководители проекта не вызывают ни малейших тёплых чувств…
      • 0
        Обновил расширение — теперь функция перевода слов при наведении присутствует.
        • 0
          Вы об «элементах»? Когда я писал свой комментарий, эта функция там уже была. Но в бесполезном для меня варианте (см. мой каммент выше)
          • 0
            Нет, я о своём расширении, смотрите скриншот:

            image

  • +10
    Как странен мир. Кто-то убивается, чтоб избавиться от штатного «Бара», а кто-то пишет себе свой…
    • +1
      Всё логично. Бары от интернет-гигантов не любят из-за того, что они сливают персональную информацию (как минимум — все посещённые URL-ы). Если делать свой бар, то этого недостатка не будет — одни профиты.
  • +6
    Вроде как это называется не плагин, а расширение (addon, extension). Плагины для firefox — это библиотеки на базе NPAPI.
    • +1
      Спасибо, исправил.
  • +1
    убрали корректор адресной строки (под предлогом установки Punto Switcher, который может и хорош для обычного работника, но никак не для программиста)
    Согласен, сам какое-то время ставил только из-за этой функции его.
  • +2
    Возможность открывать mailto: ссылки в Яндекс почте есть в настройках Firefox.
  • +1
    До чего же запутанное sdk у firefox'a для расширений. простые вещи делаются через какие-то дебри. Ещё и sdk надо ставить, а с ним питона. Отладка через ChromeBug это вообще что-то с чем-то. Пытался перенести своё расширение для рехостинга картинок, вот с XMLHttpRequest как раз одна из проблем. Чтобы выполнять кросдоменные запросы надо использовать его по другому — импортировать эту функцию из XPCOM (на stackoverflow нашёл подобные же проблемы). А там вообще без поллитры никак. Но всё убил баг с BLOBом который никто не хотел фиксить, а у меня не хватает квалификации. Насколько же проще делать расширения для Хрома, просто день и ночь.
    • 0
      Отчасти вы правы, я только из-за заморок с установкой sdk делал проект через веб-интерфейс. Отладкой занимался через консоль ошибок, встроенную в браузер.
    • +2
      Про SDK это вы наверно пошутили.
      А вообще, да, писать расширения для Firefox намного сложнее чем для Chrome (ну, в простых случаях), зато глубина кастомизации у Firefox-расширений просто бесконечная, чего не скажешь о Chrome.
      • +1
        Попробуйте переслать blob через XMLHttpRequest. Я никакого пути не нашёл. imgur видимо тоже и написали свой плагин целиком на java для firefox'a. Вот тут в Upload.js простейший код.. Правда, буду очень признателен.
    • 0
      На сколько проще писать расширение для Хрома, на столько проще они и по функциональности. 99% расширений, расширяют не сам Хром а странички сайтов. Ограничения в Хроме — выше крыши. Реально, не видел под хром расширений уровня FireBug (понятно что в самом хроме есть неплохая встроенная замена).
      • 0
        API хрома постоянно совершенствуется. И вообще хотелось бы больше примеров кроме firebug'a. Тот же chrome bug, повторюсь, вообще трудно использовать. А больше никак расширения под firefox не отладить.
        • 0
          Мне сейчас сложно сразу выкатить список расширений, так как сижу уже на Chromium (что ни говори, но он быстрее FF и меньше проблем с Flash) причем Chromium пиленый, с возможностью Открыть/Сохранить файл (долго не мог из-за этой фичи, перейти с FF на Chrome пока не нашел этот кастом). Но как пример, я не видел под хром расширений вида самого браузера, ну например Download Statusbar (помню использовал под FF). На сколько я понимаю, расширения хрома не умеют изменять/встраиваться в интерфейс браузера.
          • 0
            Я даже скажу, что запрет изменения интерфейса это благо. Да, есть вещи более функциональные, тот же адблок, но хром-то на месте не стоит, а даже бежит. Вот они уже выкатили в dev ветке высокопроизводительный api для мониторинга URI и подмены, там даже функции готовые есть — вернуть пустой документ.
            да достаточно взглянуть на магазин хрома — прямо рай для юзера, и игрушки и всякие интерфейсы к сайтам превращающие их в приложения — всего валом. А из-за очень сложного api в FF вообще сложно даже начать. И это при том, что это JetPack — вообще призван помогать в быстрой разработке. К тому же я сомневаюсь, что все эти возможности вообще нужны основной массе. Лучше бы разработку упростили.

            • +1
              Во-во… Взгляд на магазин Хрома — это как придти на радио рынок, увидеть циган с медведем, клоунов, цирк шапито и не найти нужной детали (а если и найдете то окажется, что деталь — всего обертка). Конечно хром сам развивается быстро и многие онлайн сервисы чаще выкатывают расширение под хром чем под FF.
              Зачем нужны эти игрушки, если в них можно играть зайдя на сайт? Как это расширяет функциональность браузера?
              Про благо расширения интерфейса браузера, могу сказать следующее:
              Одни из 2 частых вопросов про Хром это:
              1. Как сделать так что бы он предлагал не только сохранить файл но и открыть (сохранив в Temp который потом очищается).
              2. Как изменить DownloadBar что бы…
              И тут ответ один — никак пока это не сделают в браузере, и да, это противоречит модели безопасности Google.

              • 0
                Более того — в хром даже пытались впилить закладки как в файрфоксе — боковой панелью и пару версий они продержались, пока в очередном релизе это не обломали…
      • 0
        Ну вот в моём примере наоборот. И пока этот баг не уберут, будет невозможно.
      • 0
        Да ну, вы явно драматизируете. Под Chrome тоже существуют и можно писать много функциональных расширений. Благо, API даёт почти безграничные возможности. Как насчёт замены страниц истории, новой вкладки? Да и вообще доступ почти ко всем пользовательским данным, хранящимся в браузере
        Суть расширений – в предоставлении пользователю каких-то полезных функций, которые нет смысла делать в самом браузере. И при этом не обязательна глубокая интеграция и вообще кастомизация браузера.
        Почему только на firebug равняться?
        • 0
          Замена страниц истории и новой вкладки (привет Speed Dial 2) это конечно хорошо, но это не глубокая кастомизация.
          Дайте мне другой Download Bar. В FF это реально.
          Я про такой уровень кастомизации.
  • 0
    Как только напишете свое первое расширение, обязательно приходите к нам на собеседование: company.yandex.ru/job/vacancies/javascript_developer.xml
    • 0
      А мотоцикл дадут?
      • +1
        Самокат подарим!
  • –8
    Firefox медленно умирает. У них каша с документацией, она или незаконченная или помеченая как устаревшая, и с каждой страницы еще 10 страниц нужно открывать. Еще они не брезгуют убирать фичи которые им показались уже не нужными. Ну а самая большая проблема — это ревью плагина при паблишинге. Такое впечатление что там роботы работают. Уже второй раз завернули плагин ибо им показалось что jQuery включеная в плагин неофициальная (видимо хеш не сходится), уже третюю либу вставляем с официального сайта. И каждый раз нужно ждать несколько недель пока они соизвоолят бегло проверить расширение галимым скриптом. Тоже самое касается упакованых либ — они им не подходят так как облажались они с независимостью плагинов, разделением процессов и областью исполнения кода.

    И да, он до сих пор самый медленный в джаваскрипте, благо вебворкеры помогли вынести функционал в отдельный поток. На других браузерах тот-же самый функционал так заметно не тормозил

    То что они сейчас выкатывают СДК для разработки расширений конечно и хорошо, но с другой стороны почему Хром сразу сделал раз и нормально?! Уже не говоря о том что серьезные вещи пока не поддерживаются и приходится кодить все вручную без СДК.

    А плагины для мобильного файрфокса — отдельная песня. Они уже трижды меняли архитектуру без обратной совместимости. А я чуть голову себе не сломал при подгоне расширения к последнему файрфоксу с Native UI. Оказывается у них аддоны с SD-карточки не читаются в запакованом виде. И люди жаловались, тикеты открывали — а мозиловцы лишь вонтфикс и ворксформи ставили. И уж потом я где-то случайно нарыл решение этой проблемы
    • 0
      Зачем jQuery в плагине???? o_O
      Чем не устроили родные функции, что вы решились прикрутить дополнительные 40кб?

      И почему я, написав тонну расширений, не замечал никакого дискомфорта с документацией?
      Документация у них отличная и почти всегда на все вопросы можно найти ответы.

      Публиковать я не публиковал — особой необходимости не было — про это не спорю, мож так и есть,
      но вот про «умирает» — это зря тоже — все отлично работает и развивается.

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

      Кроме того, если уж говорить про хром — расширения под FF имеют в ущерб простоты гораздо более глубокую кастомизацию,
      но если честно, то и сложность их не настолько критична.
      • 0
        jQuery используется в построении интерфейса над логикой которого работает дизайнер, а не я, потому и используется им эта либа. Хотя я лично сторонник использовать личную реализицию нужных фич в плагинах

        Кастомизация действительно на высоте, но Мозилла думаю со временем ее зарубает (сдк тому явный признак), так как безопасность страдает при этом. Ведь у них даже свои требования к стилю написания кода, названия переменных и свойств, а так-же классов css и айдешек елементов во избежание конфликтов с другими плагинами и самим файрфоксом, и все это зарубывается на этапе паблишинга плагина. И думаю правила эти придумали довольно недавно, так как в других плагинах этого нету ибо раньше проверка была не столь критична.

        А вы работали с расширениями не требующими рестарта браузера? Да, СДК сейчас старается сглаживает боль, но раньше когда его не было нужно было всегда за собой «подтирать» при выключении/удалении плагина, а это порой нетривиально, так как забыть о чем-то довольно дегко. Не понимаю почему в других браузерах сей процесс автоматизирован?

        А насчет документации, возможно мое мнение подпортила документация по Firefox OS, вот там жуть полная, я так и не нашел нормальной информации как сделать приложение-сервис. А общение с поддержкой ни к чему не привело, молчание в ответ. И главное нужное апи вроде и есть, да вот информации по нему нету.

        Да, я тоже написал много расширений для всех популярных браузеров, и вот такое у меня мнение сложилось про файрфокс неспроста, а на опыте
    • 0
      > почему Хром сразу сделал раз и нормально?!

      Честно говоря, если заглянуть в «Adblock Plus — Bug reports» так не кажется.
  • +2
    горячо любимое расширение Яндекс.Бар
    горячо любимое
    HAHAHA OH WOW
    (куда более горячо любимый Habrastorage не пускает к загрузке файлов, говоря о нужности авторизации на Хабрахабре)
    • +1
      Если что-то не так в орфографии — напишите мне, я исправлю. Про «горячо любимое» — так я выразил свои эмоции данному продукту :)
    • +1
      Вы на самом деле не представляете, скольким людям это приложение нравилось. Хотя политика «впарить всем» — реально отвратительна.
  • 0
    > создать свое подобное расширение, которое будет включать в себя такие плюшки, как просмотр почты и корректор адресной строки

    Про настройку mailto: уже написали, а для адресной строки есть скажем UrlCorrector. Хотя в адресах сейчас встречается всё больше кириллицы, что уменьшает полезность подобных расширений. В ближайшей перспективе, видимо действительно будет удобнее пользоваться пунто (с ручной коррекцией по scroll lock).
    ______

    Да, тут выше критиковали сложность фоксовых расширений. Упомяну что небольшие вещи (не только кнопки) довольно удобно делать в виде Сustom Buttons, правда необходимо и само расширение. См. например, forum.mozilla-russia.org/viewforum.php?id=34
  • +1
    Вместе с «Баром» почили в бозе и визуальные закладки Яндекса. А то, что предложили взамен — тоже не совсем оно. В общем, история та же — написал для себя замену. Но наверно было бы глупо и эгоистично не поделиться со всеми, следуя примеру топикстартера. Если что, сильно не пинайте — писалось для себя :-)
  • 0
    под предлогом установки Punto Switcher, который может и хорош для обычного работника, но никак не для программиста

    при клике правой кнопкой по иконке Пунто в трее есть опция «Автопереключение», снимаете птичку и конвертация только по Break

    P.S. а еще в пунто мне понравилось переключать раскладки по КапсЛоку — намного быстрее как Ctrl+Shift, так и Command+Space как на Маке. Все равно никто уже почти не пользуется капсом.
    • +1
      Не менее удобно переключать между двумя часто используемыми языками одиночным левым и правым Ctrl — всегда знаешь, на какой язык переключился.
  • 0
    Аддоны для FF несколько запутаннее аддонов для Chrome, но если разобраться, всё не так плохо.

    Как мы знаем, получить dom xml документа можно с помощью нескольких функций:
    XMLHttpRequest — отпал, т.к. выдало ошибку кроссдоменного запроса (может я не так что-то делал?)


    Кроссдоменные запросы делать можно, только немного не так. Из контент-скриптов запросы делать нельзя, их надо делать по особому:

    function get_alexa_rank(url) {
    		var Request = require("request").Request;
    		Request({
    			url: "some url',
    			onComplete: function (response) {
    				worker.port.emit('message1', response.text);
    			}
    		}).get();
    
    	}
    
    	function get_google_pr(url) {
    		var Request = require("request").Request;
    		Request({
    			url: 'some url',
    			onComplete: function (response) {
    				var google_pr = response.text;
    				google_pr = google_pr.charAt(google_pr.length - 2);
    				worker.port.emit('message2', google_pr);
    			}
    		}).get();
    }
    


    На стороне контент-скрипта листенеры выглядят так:

    self.port.on("message1", function(addonMessage) {
    		var alexaRaw = $.parseXML(addonMessage),
    			alexa = $(alexaRaw).find('POPULARITY').attr('TEXT');
    		if (alexa === undefined) {
    			$('#alexa_rank').text('No data');
    		} else {
    			$('#alexa_rank').text(alexa);
    		}
    
    	});
    	self.port.on("message2", function(addonMessage) {
    		if (addonMessage === '') {
    			$('#google_pr').text('0');
    		} else {
    			$('#google_pr').text(addonMessage);
    		}
    	});
    
    


    Гуглите модуль Request и передачу сообщений к контент-скриптам и обратно.
    • 0
      Спасибо за ответ! Про модуль Request у меня написано в статье. Модуль легкий, по этому всего одно предложение) Наверно вы и не заметили.
  • +1
    Мне по части перевода всего и всея в адресной строке понравилась реализация отсюда: addons.mozilla.org/en-us/firefox/addon/input-language-assistant/
    Во-первых не запрещает писать русские буквы, если они нужны, во-вторых не добавляет кнопок, в-третьих ему похоже плевать на раскладки). А что отвалилось то в отжившем своё баре? Он вроде давно «не совместим», но работает, не?
  • 0
    Спасибо за статью.
    Придется мне в этом деле тоже поразобраться. Мой давно юзаемый LastPass продался в Logmein. Понимаю, что может быть это и паранойя. Но я не люблю вот такие вот закиды.

    Никогда не писал плагины для FireFox, но похоже будет проще разобраться с этим, чем вспоминать (почти наугад по остатком 5-ей летней давности) как через WinAPI добраться до полей ввода на странице и загонять туба из базы парольчики.

    Буду делать маленькую персональную замену для LastPass
    • 0
      Ничего сложного, я смотрел в том году сдк, его допилили, убрали онлайн версию и вообще всё стало хорошо) Не написали еще?

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.