2009 / 02 / 22

По горячим следам: запись скринкаста

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

Итак. Что касается виновника торжества, то вот он:

TweetDeck screencast (rus) от Александра Улизько на Vimeo.

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

ВНЕЗАПНО прорезавшийся вологодский акцент - абсолютно непонятно, откуда он взялся, но, тем не менее, в отдельных местах я отчетливо окаю.

Язык убегает вперед головы — иногда не согласованы рода и числа, предложения остаются оборваны. Вы бы видели, как хохотал proxiper, когда, я пять раз подряд записывал фразу «зарегистрировался в сервисе», потому что с языка упорно слетало слово «сервер». Как с этим бороться, я не знаю, если честно — заучивать текст мне кажется глупым, но иного способа говорить четко и без оговорок я не знаю. Может быть, кто-нибудь из более опытных товарищей поделится?

Откровенно странное качество записи — писалось все на встроенный в макбук микрофон в течении достаточно длительного времени, от этого голос время от времени меняется, что вы наверняка заметили. Думаю, при наличии нормального микрофона и сокращении времени записи конкретно этот недостаток можно изжить.

План скринкаста о tweetdeck'e

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

Монтаж скринкаста

Неправильный выбор программы для записи и редактирования — я записывал все в программу screenflow. Точнее даже, записывает-то она вполне прилично, но вот редактирование в ней совершенно не предусмотренно. Например, совершенно невозможно взять отдельный кадр из видео и показывать только его, а звук пускай идет дальше — иногда это полезно, так как не успеваешь проговорить все, что собирался. Также, совершенно дико реализован процесс дозаписи кусочков звука — чем-то процесс мне напоминает далекую молодость, когда я мышкой накликивал в cakewalk 9 midi-партии. Возникают точно такие же ощущения бессмысленной и лишней работы — но, черт возьми, уже сколько лет-то прошло? Должен же прогресс уйти вперед не?

В общем, выводы из всего этого такие:

  • Скринкасты делать весело и интересно
  • Нужна какая-то более удобная программа для редактирования видео
  • Нужно что-то сделать с речью - перед зеркалом тренироваться, что ли

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

2009 / 02 / 16

Букмарклеты

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

Кто не знает, это такая штука, которую можно добавить в закладки (да, я сегодня дебютирую в роли Капитана Очевидность :) и, при нажатии на нее, произвести какой-нибудь эффект.

Примером может служить герой сегодняшней заметки, который расположен по адресу http://ulizko. com/demo/allthat/. Инструкция по применению:

  • Перетащите ссылку «link» на панель закладок или щелкните по ней правой кнопкой мыши и выберите пункт меню «добавить в избранное».
  • Зайдите на какой-нибудь сайт, вроде http://twitter.com, и нажмите на эту закладку (ну или на избранное).

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

Но. Мы сегодня не об этом, а о том, как делать такие штуки в принципе.

Прежде чем перейти непосредственно к разбору кода, хотелось бы ответить на вопрос (который мне никто не задавал :), а именно, "Какие возможности дает букмарклет?". Правильный ответ — любые. Так как мы получаем возможность подгрузить любой скрипт, мы можем сделать с клиентской страничкой все, что угодно. Например — сделать «выносной» виджет, в котором на любой страничке можно будет добавить запись в блокнот или таскменджер. Или вообще сделать весь таскменеджер выносным. Что тоже важно, они будут работать практически везде — это не плагины к firefox’у и не виджеты к opera. Букмарклетам не важно (ну, почти :), какая у вас ОС или браузер. В общем, есть простор для фантазии.

Итак, как же делать эти самые букмарклеты?

Очень просто: надо создать на страничке элемент anchor с атрибутом href, содержащим javascript-код. Если перевести на русский, то надо сделать вот такую ссылку, адрес которой, по большому счету, и будет букмарклетом:

<a href="javascript:alert('I am bookmarklet'); void 0;">Bookmarklet</a>

Демо:

Демо вышеописанного кода

Для того, чтобы javascript код в адресе ссылки заработал, надо добавить перед ним слово javascript:. По умному это называется «указание псевдопротокола javascript». Еще одна важная деталь — если ваш код вернет какое-то значение, то браузер воспримет его в качестве адреса, по которому нужно перейти, и уйдет с текущей страницы. Чтобы избежать этого, не возвращайте значения, то есть допишите в конец скрипта void 0;, либо оберните весь код в анонимную функцию, невозвращающую значения — (function(){... ваш код мог бы быть здесь...})().

В любом случае, все эти вопросы подробно рассмотрены у Ильи Кантора в его заметке Букмарклеты и правила их написания, к которой я вас и отсылаю за подробностями.

Единственную вещь, которую нам еще нужно знать — это то, что все браузеры ограничивают максимальную длину кода букмарклета. И, подобно тому, как скорость каравана равна скорости самого медленного верблюда, так и максимальный размер кроссбраузерного букмарклета равен ограничению, наложенному IE 6 SP2, то есть, 488 символам.

Таким образом, вряд ли мы сможем закодить какую-то комплексную логику в неполных пятистах символах, так что чаще всего букмарклеты просто создают новый тэг script, в который уже сгружают код приложения.

Так поступил и я. Вот код моего букмарклета в человекоадаптированном виде:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(function () {
    // создаем новую внутреннюю переменную a (лучше в данном случае использовать короткие идентификаторы)
    // и сразу же добавляем свой объект в глобальный объект window, и записываем в него данные, которые уникальны
    // для каждого пользователя (ведь они сгенерированы сервером для пользователя перед тем, как он добавил этот букмарклет к себе)
    var a = window.allThat = { 
        userId : '123345456',
        server : 'http://mysite.com/',
        script : document.createElement('script'), // создадим и запомним тэг скрипт, 
        // который сгрузит нам код нашего приложения - мы его потом удалим, если пользователь нажмет кнопку "закрыть"
        css : document.createElement('link')
    },
    /* динамически создаем элементы: */
    h = document.getElementsByTagName('head')[0];
    a.css.rel = 'stylesheet';
    a.css.href = a.server + 'css/bookmarklet.2.css';
    h.appendChild(a.css);
    a.script.src = a.server + 'js/bookmarklet.7.js';
    h.appendChild(a.script);
    h=null;
})();

Потом подгружается непосредственно код самого окошка. Думаю, он может представлять некий интерес сам по себе, так что и его я сюда запощу (все комментарии идут на английском, так как заказчик американец):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
(function () {
var Dom = {
    get : function (el) { 
        return (el && el.nodeType) ? el : document.getElementById(el); 
    },
    addListener : function (el, type, fn) { 
            if (document.body.addEventListener) { 
                return function (el, type, fn) { 
                    el.addEventListener(type, fn, false); 
                }; 
            } else if (document.body.attachEvent) { 
                return function (el, type, fn) { 
                    el.attachEvent('on' + type, fn); 
                }; 
            } else { 
                return function (el, type, fn) { 
                    el['on' + type] = fn; 
                }; 
            } 
        }(),
    removeListener : function (el, type, fn) { 
            if (document.body.removeEventListener){ 
                return function (el, type, fn) { 
                    el.removeEventListener(type, fn, false); 
                }; 
            } else if (document.body.detachEvent) { 
                return function (el, type, fn) { 
                    el.detachEvent('on' + type, fn); 
                }; 
            } else { 
                return function (el, type, fn) { 
                    el['on' + type] = function () { return true; }; 
                }; 
            } 
        }(),
    hide : function (el) {
        el.style.display = 'none';
    },
    show : function (el) {
        el.style.display = '';
    }
},

allThat = window.allThat;

allThat.Bookmarklet = function () {
    // where to send user data and where from take wishlists list
    var wishlistsLocation = allThat.server + 'wishlists/',
    sendTo = allThat.server + 'wishes/',

    // bookmarklet window html code
    innerHTML = '<div id="allthat-wish"><div class="allthat-saving" id="allthat-throbber">saving...</div><div id="allthat-logo"><span>AllThat</span></div><button title="close" id="allthat-close"><span>close</span></button><h1><span>Add To Wishlist</span></h1><form action=""><div class="allthat-field"><label for="allthat-product">Product Name:</label><br /><input type="text" name="product" value="(Ex: Black iPhone Adapter)" class="allthat-sample-value" id="allthat-product" /></div>	<div class="allthat-field"><label for="allthat-wishlist">Add to List:</label><br /><select name="wishlist" id="allthat-wishlist"></select></div><fieldset id="allthat-low-price">	<h2>Low Price Alerts!</h2>			<div class="allthat-field" id="allthat-range"><label for="allthat-minprice">How much would you like to pay?</label><br /><input type="text" name="minprice" value="(min)" class="allthat-sample-value" id="allthat-minprice" />to<input type="text" name="maxprice" value="(max)" class="allthat-sample-value" id="allthat-maxprice" /></div><h3>Alert me via:</h3><fieldset id="allthat-alerts"><input type="checkbox" name="email" id="allthat-email" /><label for="allthat-email" id="allthat-email-label">Email</label><br /><input type="checkbox" name="sms" id="allthat-sms" /><label for="allthat-sms" id="allthat-sms-label">SMS</label><br /><input type="checkbox" name="twitter" id="allthat-twitter" /><label for="allthat-twitter" id="allthat-twitter-label">Twitter</label><br /><select name="frequency" id="allthat-frequency"><option value="0" selected="selected">-- Alert Frequency --</option><option value="1">Daily</option><option value="7">Weekly</option><option value="30">Monthly</option></select></fieldset></fieldset><button title="Add" id="allthat-add"><span>Add</span></button></form><div id="allthat-errors" style="color:red;"/></div>',

    // dom elements:
    container = document.createElement('div'),
    errorDiv,
    alertFrequencyDropdown,
    wishlistsDropdown,
    wishlistsWrapper,
    savingThrobber,
    closeButton,
    titleInput,
    minPriceInput,
    maxPriceInput,
    sendButton,
    alerts = {},
    scripts = [],

    // input default values:
    titleDefaultValue = '(Ex: Black iPhone Adapter)',
    minPriceDefaultValue = '(min)',
    maxPriceDefaultValue = '(max)',

    // errors array - used for validation
    errors = [],
    errorMessages = {
        titleEmpty : 'Please enter product name',
        wishlistNotSelected : 'Please chose list',
        frequencyNonSelected : 'Please chose alert frequency'
    };

    // append bookmarklet window html to the target page
    function createTemplate () {
        container.innerHTML = innerHTML;
        document.body.appendChild(container);
    };
    // initialize javascript references to the Dom elements
    function initializeDomElementsReferences () {
        errorDiv = Dom.get('allthat-errors');
        alertFrequencyDropdown = Dom.get('allthat-frequency');
        wishlistsDropdown = Dom.get('allthat-wishlist');
        wishlistsWrapper = wishlistsDropdown.parentNode;
        savingThrobber = Dom.get('allthat-throbber');
        closeButton = Dom.get('allthat-close');
        titleInput = Dom.get('allthat-product');
        minPriceInput = Dom.get('allthat-minprice');
        maxPriceInput = Dom.get('allthat-maxprice');
        sendButton = Dom.get('allthat-add');
        alerts.email = Dom.get('allthat-email');
        alerts.sms = Dom.get('allthat-sms');
        alerts.twitter = Dom.get('allthat-twitter');
    };
    // disable wishlist dropdown before server response with wishlists array doesn't arrive
    function initializeGUI () {
        wishlistsDropdown.disabled = 'disabled';
        wishlistsDropdown.style.width = '90%';
        wishlistsWrapper = wishlistsDropdown.parentNode;
        wishlistsWrapper.style.background = 'transparent url(' + allThat.server + 'images/bookmarklet/ajax-loader-blue.gif) no-repeat right';
        Dom.hide(savingThrobber);
    };
    // bind event listeners to the controls
    function attachListeners () {
        Dom.addListener(closeButton, 'click', destroy);
        Dom.addListener(titleInput, 'focus', function () {if (titleInput.value == titleDefaultValue) activateInput(titleInput);});
        Dom.addListener(minPriceInput, 'focus', function () {if (minPriceInput.value == minPriceDefaultValue) activateInput(minPriceInput);});
        Dom.addListener(maxPriceInput, 'focus', function () {if (maxPriceInput.value == maxPriceDefaultValue) activateInput(maxPriceInput);});
        Dom.addListener(sendButton, 'click', function (e) { e = e || window.event; if (e.preventDefault) e.preventDefault(); addItemToList(); return false;});
    };

    // validators
    function validateItemTitlePresence () {
        var t = titleInput.value;
        if (t.replace(/^s+|s+$/, '').length == 0 || t == titleDefaultValue) errors.push(errorMessages.titleEmpty);
    };

    function validateWishlistPresence () {
        if (typeof wishlistsDropdown.value === 'undefined') errors.push(errorMessages.wishlistNotSelected);
    };

    function validateFrequencyPresence () {
        for (var alert in alerts) { 
            if (alerts[alert].checked && alertFrequencyDropdown.value == 0) {
                errors.push(errorMessages.frequencyNonSelected); 
                return;
            }
        }
    };

    function validate () {
        errors.length = 0;
        validateItemTitlePresence();
        validateWishlistPresence();
        validateFrequencyPresence();
        return errors.length == 0;
    };

    function displayErrors () {
        var output = '', error, i = 0;
        while (error = errors[i++]) {
            output += error + '<br/>';
        }

        errorDiv.innerHTML = output;
    };

    // this function called if user clicks on the 'send' button
    // so that we need to validate data, and, if it's all ok,
    // send request to server. Also, we show throbber and setup callback which would stop it
    function addItemToList () {
        if (validate()) {
            sendItemOnServer();
        }
        displayErrors();
    };

    // serialize data into string, show loader and call sendRequest method
    function sendItemOnServer () {
        var data = 'title=' + encodeURIComponent(titleInput.value) + '&wishlist=' + wishlistsDropdown.value;

        var temp = minPriceInput.value;
        data += (temp == '' || temp == minPriceDefaultValue) ? '' : ('&minPrice=' + temp);
        temp = maxPriceInput.value;
        data += (temp == '' || temp == maxPriceDefaultValue) ? '' : ('&maxPrice=' + temp);

        data += '&alerts=[';
        for (var alert in alerts) { 
            var a = alerts[alert];
            if (a.checked) {
                data += a.name;
            }
        }
        data += ']';
        data += '&alertFrequency=' + alertFrequencyDropdown.value;

        showLoader();
        sendRequest(sendTo, data, 'itemAdded');
    };

    // clear inputs
    function clearFields () {
        hideLoader();
        activateInput (titleInput);
        activateInput (minPriceInput);
        activateInput (maxPriceInput);
        for (var alert in alerts) alerts[alert].checked = false;
    };

    // misc - remove default value and make font color black
    function activateInput (el) {
        el.className = '';
        el.value = '';
    };

    // feed wishlists dropdown with data and enable it
    function activateWhishlistsDropdown (response) {
        var w = response.wishlists, 
            i = 0, 
            wishlist, 
            opt;
        while(wishlist = w[i++]) {
            opt = document.createElement("option");
			opt.appendChild(document.createTextNode(wishlist.title));
			opt.setAttribute("value", wishlist.id);
			wishlistsDropdown.appendChild(opt);
        }
        wishlistsDropdown.disabled = false;
        wishlistsDropdown.style.width = '100%';
        wishlistsWrapper.style.background = '';
    };

    // remove every track of bookmarklet from the page
    function destroy () {
        removeEventListeners();
        removeDOMReferences();
        container.parentNode.removeChild(container);
        container = null;
        allThat.css.parentNode.removeChild(allThat.css);
        allThat.script.parentNode.removeChild(allThat.script);
        allThat = null;
    };
    // to prevent memory leaks on ie6 - remove all js to dom references
    function removeDOMReferences () {
        errorDiv = null;
        alertFrequencyDropdown = null;
        wishlistsDropdown = null;
        wishlistsWrapper = null;
        savingThrobber = null;
        closeButton = null;
        titleInput = null;
        minPriceInput = null;
        maxPriceInput = null;
        sendButton = null;
        alerts.email = null;
        alerts.sms = null;
        alerts.twitter = null;
        var i = scripts.length - 1, script, head = document.getElementsByTagName('head')[0];
        while (script = scripts[i--]) {
            head.removeChild(script);
            script = null;
            scripts[i + 1] = null;
        }
        scripts.length = null;
    };
    // to prevent memory leaks - remove all event listeners
    function removeEventListeners () {
        Dom.removeListener(closeButton, 'click', destroy);
        Dom.removeListener(titleInput, 'focus', function () {if (titleInput.value == titleDefaultValue) activateInput(titleInput);});
        Dom.removeListener(minPriceInput, 'focus', function () {if (minPriceInput.value == minPriceDefaultValue) activateInput(minPriceInput);});
        Dom.removeListener(maxPriceInput, 'focus', function () {if (maxPriceInput.value == maxPriceDefaultValue) activateInput(maxPriceInput);});
        Dom.removeListener(sendButton, 'click', function (e) { e = e || window.event; if (e.preventDefault) e.preventDefault(); addItemToList(); return false;});
    };

    // create dynamic script element and remove it immediately after it load
    function sendRequest (url, data, callback) {
        var head = document.getElementsByTagName('head')[0],
            script = document.createElement('script'),
            noCacheIE = '&noCacheIE=' + (new Date()).getTime(),
            fullUrl = url + '?callback=' + encodeURIComponent(callback) + '&userId=' + allThat.userId+ ((data) ? ('&' + data) : '') + noCacheIE;

        script.setAttribute('type', 'text/javascript');
        script.setAttribute('src', fullUrl);
        script.setAttribute('charset', 'utf-8');

        head.appendChild(script);
        scripts.push(script);
        head = null;
    };

    // misc - hide send button and show saving throbber instead
    function showLoader () {
        Dom.show(savingThrobber);
        Dom.hide(sendButton);
    };
    // misc - hide saving throbber and show send button instead
    function hideLoader () {
        Dom.hide(savingThrobber);
        Dom.show(sendButton);
    };

    return {
        initialize : function () {
            createTemplate();
            initializeDomElementsReferences();
            initializeGUI();
            attachListeners();
            sendRequest(wishlistsLocation, null, 'loadWishlists');
        },

        destroy : function () {
            destroy();
        },

        loadWishlists : activateWhishlistsDropdown,

        itemAdded : clearFields
    };
}();

// to prevent memory leaks, remove all js <-> dom references, including dom elements references and event listeners
Dom.addListener(window, 'unload', function () {
    if (allThat) allThat.Bookmarklet.destroy();
    Dom.removeListener(window, 'unload', arguments.callee);
});


allThat.Bookmarklet.initialize(); // show bookmarklet - this is visual start of the application

})();

Примечания:

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

2009 / 02 / 06

Перевод нормальных слов в код

Так, сегодня пришло в голову, решил записать. Не то, чтобы это очень уж смешная штука была, но забавная. Особенно в конце плодотворного рабочего дня :)

for (var mate in addressBook) { 
    if (mate.blacklisted) { // Ко всем неприятным знакомым:
        if (calculateDistance(Home.getInstance().getLatitude(), 
          mate.getLatitude()) < 100) { // будете проходить мимо -
            continue; // проходите
        }
    }
}