Итак, я продолжаю описывать свой опыт создания приложения для vkontakte.ru. В первой части я описал, как создавалась начальная версия моего музыкального плеера. В этой части я опишу, как я добавлял серверную часть.
Приложение делается с помощью Flex, а под катом описан мой опыт работы вот с такими штуками: TabNavigator, Menu Control, работа с координатами, pop up окна, TitleWindow, самодельные event'ы, Zend, Zend AMF, работа с базой данных, ItemRenderer, crossdomain policy.
А если более коротко, то я просто описываю, как научился добавялть, читать, обновлять и удалять информацию из базы данных при помощи связки Flex + Zend AMF.
Итак, за первые 5 дней «Мидия» набрала 330 пользователей. Не знаю, много это или мало, но главное, что процесс пошел и не собирается останавливаться. Я, в течении этих пяти дней, дал себе передышку и практически не работал над плеером, но расслабляться нельзя, поэтому возобновляю работу и как и прежде буду документировать свои действия.
За первые же дни набралось приличное количество багов и пожеланий пользователей, которые я и буду постепенно устранять/добавлять. Сегодня я начал с добавления логотипа lastFM и ссылки на www.lastfm.com, это обязательное требование, при использовании их API.
Один из пользователей предложил добавить возможность проигрывания всей музыки друзей в одном списке. На мой взгляд, здравая мысль, поэтому ей мы и займемся. К сожалению, в API контакта нет возможности передать список пользователей и получить список их аудио записей одним запросом, а значит придется последовательно передавать в API идентификаторы пользователей и добавлять полученные записи в общий список. Другой интересный вопрос, как добавить в ComboBox (выпадающий список друзей) поле «Вся музыка друзей», это поле выбивается из общей структуры и его надо обрабатывать отдельным способом. С этого и начнем.
Добавить новый пункт в ComboBox было элементарно, создаем новый XML примерно так var newNode:XML = , а потом добавялем его в нужную XMLListCollection через .addItemAt(newNode,1). К сожалению, думая над дальнешими действиями, я решил, что функцию прослушивания всей музыки друзей одним списком пока что не нужно делать. Для того, чтобы составить полный список музыки, например, у 45 друзей, нужно будет отправить 45 запросов к API контакта, при этом максимально можно отправлять 3 запроса в секунду, следовательно на составление списка уйдет минимум 15 секунд, а если при этом возникнут какие-то ошибки, то результат я пока что и вовсе не могу предсказать. В общем, оставим эту функцию на будущее, либо подожду пока в контакте появится возможность передавать сразу несколько uid'ов с методом getAudios, либо буду делать нужную функцию через собственный сервер и собственную базу данных.
Сегодня на дачу, но пока есть время, продолжу разработку. Начнем с исправления кнопки Play. Судя по отзывам, люди, при нажатии на Play, рассчитывают, что начнется проигрывание выделеного трека (что, вообще-то, логично, но изначально я в этом вопросе ориентировался на WinAMP). Задача была решена быстро и ничего особенного для этого делать не пришлось, просто несколько конструкций if...else.
Ещё меня просили, чтобы при нажатии кнопки «Следующий трек», если список закончился, то должен начинаться самый первый трек. Ну это уж совсем элементарно, даже описывать нечего.
Теперь изменение громкости, чтобы громкость изменялась при движении полузнка, а не только после его отпускания. Опять элементарно, всего лишь добавление к <mx:HSlider> свойства liveDragging=«true».
При включенной функции проигрывания случайного трека, кнопка «Следующий трек» должна переходить на следующий случайный трек, а не на следующий по списку. Опять же ничего сложного, if else и всё.
Пожалуй я больше не буду указывать конкретные даты, потому что временные промежутки между ними удручают. В предыдущей записи, я почему-то не написал, что сделал обратный отсчёт времени (до конца трека). В общем, я это сделал и уже не помню как. Кроме того, я наконец-то сделал более менее заметное улучшение. Теперь выводится не только информация об исполинтеле, но и текст текущей песни, который берется с lyricwiki (идею с текстом я подсмотрел в другом плеере, в чем честно признаюсь). Для вывода текста песни, я сделал правую колонку с использованием компонента viewstack, который позволяет переключать отображаемую информацию. В первом слое viewstack я так и оставил информацию об исполнителе, а во второй слой поместил самодельный компонент lyrics, который и выводит текст (там всё элементарно, простая отправка запроса к API lyricwiki с помощью HTTPService и вывод полученного результата). Переключение между слоями (я называю это слоями, хотя возможно есть какое-то более подходящее определение) я сделал с помощью компонента Combobox, опять же всё просто, на уровне свойства change=«vsRightColumn.Selectedindex = cbShow.Selectedindex». Всё заработало с первого раза, единственная проблема в том, что до первого переключения viewstack на отображение текста песни, компонент lyrics не активен. В результате при первом переключении на текст песни, никакой текст не отображается, а начинает отображаться, либо после второго переключения, либо после смены трека, но думаю это можно решить с помощью init() функции и, возможно, самодельного event'а, который будет передавать наверх сообщение, что компонент инициализирован.
Эти заголовки <когда-то>, наверное, не лучшая идея. Теперь буду придумывать, заголовки со смыслом (или без смысла, но хотя бы интересные). Так вот, похоже, я наконец-то созрел для первых экспериментов по добавлению взаимодействия с базой данных и PHP. Первой функцией, которую я хотел бы реализовать с использованием БД будет возможность рекомендовать трек своим друзьям (естественно, только тем, которые установили приложение). Конечно, для этих целей уже можно использовать «стену», но через плеер это должно быть удобнее, а кроме того, есть надежда, что это простимулирует пользователей (которых сейчас всего 750) приглашать в приложение своих друзей.
Итак, первое, что мне понадобится — это спроектировать базу данных. Я в этом деле полнейший ноль, поэтому полез за информацией на Хабр. Для начала, мне хватило вот этих двух статей (спасибо авторам): http://abarmot.habrahabr.ru/blog/23423/, http://habrahabr.ru/blogs/development/45707/. Прочитав статьи, я выделил для себя 3 сущности: пользователь, трек, рекоммендация и сделал для них соответствующие таблицы (users, audio, advices), которые, как мне кажется, удовлетворяют нормальным формам. Ещё я на днях прочитал небольшую часть книжки Getting Real от 37signals и там было написано, что сначала надо сделать интерфейс. Как-то раз я, ради интереса, хотел воспользоваться их продуктами, 5 секунд потыкал в ссылки на их сайте, ничего не понял и забил, но всё равно они, судя по всему, крутые ребята и знают о чём(люблю букву Ё) говорят, так что последую их совету, запущу Flex Builder и буду делать интерфейс. С интерфейсом, кстати, проблема, потому что у меня уже заканчивается свободное место для всяких кнопочек и т.п., но я в лучших традициях быдло проектировщика не буду обращать на это внимание и буду лепить, как получится. Довольно скоро, такой подход перестанет работать, и тогда мне придется капитально всё переработать, но я хотя бы уже буду знать, что к чему и смогу проектировать опираясь на реальный опыт вполне конкретного приложения. В общем, буду резать семь раз, для того, чтобы понять, как оптимально мерить.
Рекомендации можно будет тут же проигрывать в плеере, поэтому отображать их я буду на месте списка песен. Переключение между рекомендациями и своим списком буду делать при помощи Viewstack и кнопок на панели плеера. Начну с кнопок. Нет, я попробовал начать с кнопок и получилось плохо. Начну с Viewstack. Итак, мне надо добавить viewstack и на одном слое разместить старый добрый список песен, а на другом слое datagrid с рекомендациями. Нет, я опять соврал. Оказывается лучше всего для моей цели подойдет TabNavigator — это viewstack к которому уже приделаны табы, для переключения между слоями. Да, вот теперь всё правильно. TabNavigator просто супер, даже странно, почему я раньше его не использовал. Теперь надо добавить в TabNavigator datagrid (или, попросту говоря, таблицу) для отображения рекомендаций. Datagrid должен состоять из следующих колонок: отправитель рекомендации, название исполнителя и трека, длительность и кнопка для получения дополнительного комментария, если отправитель его оставил. Получается довольно много и в одну строчку всё это будет смотреться плохо, нужно найти способ вынести отправителя над треком. Пожалуй, лучше использовать TileList. В общем, я пока что поставил пустой TileList, дальше надо будет его дорабатывать, когда появится работающий dataProvider. Будем считать, что интерфейс для отображения рекомендаций у меня есть, теперь нужен интерфейс для отправки рекомендаций. Вот здесь, похоже пришло время для добавления новой колонки в DataGrid со списком песен. Эта колонка будет содержать в себе кнопку для вызова меню трека. Пока что в меню будет только одна функция — «рекомендовать», но со временем могут появиться и другие.
Во-первых, я решил, что совершенно логичным будет делать меню трека с помощью Menu control. Сначала я добавил новую колонку в DataGrid с треками, в эту колонку, при помощи itemRenderer поместил обычную кнопку. Первое, что мне надо сделать — это вывести меню в нужном месте, т.е. вплотную к правому верхнему углу нажатой кнопки, для этого мне понадобится определять координаты кнопки. X координата меняться не будет, так же, как и размер кнопки, следовательно задача сводится к определению Y координаты. Для проверки, как это работает, я добавил к кнопке click=«testButton(event)» и, собственно, саму функцию:
public function testButton(e:Event):void
{
var tmpString:String = new String();
tmpString=«y: » + String(e.target.y);
Alert.show(tmpString);
}
Как оказалось, для вызова функции из itemRenderer необходимо использовать свойство outerDocument, а сама функция не должна быть private. Таким образом, свойство click пришлось переписать на click=«outerDocument.testButton(event)», после этого всё заработало и я получил Y координату. Y координата показывалась относительно родительского элемента, это было не то, что мне нужно, мне нужно узнать координату относительно всего компонента. Идём в google и пишем
flex 3 coordinates, первой же ссылкой идет статья «Using Flex coordinates» из Adobe Flex 3 Help. Из статьи становится понятно, что существует три типа координат: global, local и content, мне нужна именно global координата и получить её можно методом localToGlobal. Попробуем использовать такую конструкцию.
public function testButton(e:Event):void
{
var tmpString:String = new String();
var pt:Point = new Point(e.target.x, e.target.y);
pt = e.target.localToGlobal(pt);
tmpString=«y: » + String(pt.y);
Alert.show(tmpString);
}
Как и ожидалось, получили глобальную Y координату. Теперь надо создать меню. Как всегда обращаемся к Help'у, в данном случае к «Using Menu-Based Controls». Для начала надо определить структуру меню, это можно сделать различными способами, лично я буду делать через XML, вот так:
<mx:XML format=«e4x» id=«trackMenu»>
</mx:XML>
Теперь делаем функцию для вывода меню:
public function showTrackMenu(e:Event):void
{
var trackMenu:Menu = Menu.createMenu(null, trackMenuData, false);
trackMenu.labelField="@label";
var pt:Point = new Point(e.target.x, e.target.y);
pt = e.target.localToGlobal(pt);
trackMenu.show(pt.x + e.target.width, pt.y);
}
Назначаем кнопке эту функцию: click=«showTrackMenu(event)». Результат получился неожиданый, по горизонтали меню раполагается отлично, а вот по Y меню уезжает вниз ровно на столько, сколько кнопок находится выше нажатой. Я так и не смог понять, почему так просиходит, поэтому сделал вот так:
trackMenu.show(e.target.x + e.target.width+3,e.target.y+294);
Решение, конечно, отстойное, но работает — меню появляется там где надо. Теперь нужно, чтобы при нажатии на пункт меню, появлялось всплывающее окно со списком друзей и возможностью ввести комментарий к своей рекомендации. Сначала нужно понять, как, вообще, определить, что пользователь нажал на определенный пункт меню, для этого я добавлю в меню ещё один пункт «закрыть» и буду с ним экспериментировать. Всё оказалось, очень просто добавляем eventListener и функцию для обработки event'а. Ради эксперимента я добавил к первому пункту меню свойство selectedMenu=«rec», а потом проверил, как это работает:
public function showTrackMenu(e:Event):void
{
var trackMenu:Menu = Menu.createMenu(dgTracklist, trackMenuData, false);
trackMenu.labelField="@label";
trackMenu.addEventListener(MenuEvent.ITEM_CLICK, itemClickInfo);
trackMenu.show(e.target.x + e.target.width+3,e.target.y+294);
}
private function itemClickInfo(event:MenuEvent):void
{
if(event.item.@menuSelected==«rec») Alert.show(event.item.@menuSelected);
}
Всё прекрасно сработало. Теперь нужно добавить всплывающее окно, со списком друзей, полем для комментария и кнопками «отправить» и «отмена». Я уже знаю, что для этого нужно использовать TitleWindow. Не смотря на то, что меню «рекомендовать трек» находится в компоненте плеера, всплывающее окно я буду создавать из основного приложения, а значит надо передать event из компонента с плеером в основное приложение. При этом, в event'е надо будет передавать полную информацию о рекомендуемом треке. Надо создавать custom event. Кажется, я ещё не описывал создание соственных event'ов, поэтому опишу этот процесс поподробнее. Сначала я добавил определение нового event'а
<mx:Metadata>[Event(name=«giveAdvice», type=«com.vkapps.events.GiveAdviceEvent»)]</mx:Metadata>
Теперь надо его создать, создаем новый файл GiveAdviceEvent.as и в нём описываем сам event. Тут я выложу прямо исходником, думаю в нём всё понятно:
package com.vkapps.events
{
import flash.events.Event;
public class GiveAdviceEvent extends Event
{
public static const GIVE_ADVICE:String = «giveAdvice»;
public var aid:String;
public var owner_id:String;
public var artist:String;
public var title:String;
public var duration:String;
public var url:String;
public function GiveAdviceEvent(aid:String,owner_id:String,artist:String,title:String,duration:String,url:String)
{
super(GIVE_ADVICE);
this.aid = artist;
this.owner_id = title;
this.artist = artist;
this.title = title;
this.duration = artist;
this.url = title;
}
override public function clone( ):Event
{
return new GiveAdviceEvent(this.aid,this.owner_id,this.artist,this.title,this.duration,this.url);
}
}
}
Ну вот, event есть. Осталось только его отправить в нужный момент, вот таким образом:
private function itemClickInfo(event:MenuEvent):void
{
if(event.item.@menuSelected==«rec»)
{
var giveAdvice:GiveAdviceEvent = new GiveAdviceEvent(dgTracklist.selectedItem.aid,dgTracklist.selectedItem.owner_id,dgTracklist.selectedItem.artist, dgTracklist.selectedItem.title,dgTracklist.selectedItem.duration,dgTracklist.selectedItem.url)
dispatchEvent(giveAdvice);
}
}
Переходим в основное приложение и добавляем event listener:
player.addEventListener(«giveAdvice»,giveAdvice);
Теперь нужно подготовить всплывающее окно, для создаем новый компонент на основе TitleWindow. Размещаем там необходимые контролы и объявляем переменные, которые будут содержать информацию о треке, примерно, вот так:
[Bindable]
public var _artist:String;
Опять возвращаемся в основную функцию и пишем функцию для обработки event'а (я её сократил):
private function giveAdvice(event:Object):void
{
var pop1:AdviceForm = AdviceForm(PopUpManager.createPopUp(this, AdviceForm, true));
pop1.title=«Отправьте рекомендацию»;
pop1.showCloseButton=true;
pop1._artist=event.artist;
…
pop1._url=event.url;
PopUpManager.centerPopUp(pop1);
}
Всё, основной интерфейс для рекомендаций готов. Теперь нужна серверная часть.
Я написал, что интерфейс для рекомендаций готов, но на самом деле я немного соврал. Для полного счастья, надо чтобы во вспылывающем TitleWindow отображался список друзей установивших приложение, потому что рекомендовать можно только им. В api контакта уже есть подходящий метод getAppFriends. Проблема в другом — опять создавать запрос к api прямо во всплывающем окне — это уже совсем уродливо и приводит меня к необходимости сделать отдельный класс для работы с api контакта и дальше все запросы отправлять при помощи экземпляров этого класса (вообще-то, это очевидно, но раньше острой необходимости в этом небыло). Сдеать класс было элементарно, трудности возникли, когда мне понадобилось вернуть результат запроса из класса в компонент. Единственный (по крайней мере других я не знаю) способ это сделать — это event'ы. Мне нужно отправлять ResultEvent наверх, для этого я опять сделал собственный event:
import flash.events.Event;
import mx.rpc.events.ResultEvent;
public class CustomResultEvent extends Event
{
public static const FRIENDS_RESULT:String = «friendsResult»;
public static const FRIENDS_APP_RESULT:String = «friendsAppResult»;
public var customResult:ResultEvent;
public function CustomResultEvent(type:String, e:ResultEvent)
{
super(type);
this.customResult = e;
}
override public function clone():Event
{
return new CustomResultEvent(type, customResult);
}
}
В файле с API я отправляю его, например, таким образом:
private function friendsAppRequestHandler(event:ResultEvent):void
{
var friendsAppResult:CustomResultEvent = new CustomResultEvent(«friendsAppResult»,event)
dispatchEvent(friendsAppResult);
}
А принимаю, например, вот так:
private function init()
{
api.addEventListener(«friendsAppResult»,friendsHandler);
api.requestAppFriends(this._uid);
}
[Bindable]
private var lFriendsList:XMLListCollection = new XMLListCollection();
private function friendsHandler(event:CustomResultEvent):void
{
lFriendsList.source = new XMLList(event.customResult.result.user);
}
Вот теперь интерфейс на самом деле готов и опять теперь нужно делать серверную часть. Пока что это кажется трудным. Для серверной части я планирую использовать Zend, поэтому идём google ищем «flex zend». Первая ссылка ведет на статью: «Flex and PHP: Party in the Front, Business in the Back», я быстренько пробежал её глазами, но кажется, это не то, что мне надо. Следом я перешел на статью «Integrating Adobe Flex and PHP» и это уже что-то более близкое к тому, что я искал (несмотря на то, что Zend в ней никак не используется). В конце статьи шла ссылка на блог автора: blogs.adobe.com/mikepotter я перешел по ссылке и там, впервые, прочитал про AMF. Дальнейшие поиски в этом направлении, убедили меня, что оптимальным будет использование связки Flex + Zend AMF и в этом смысле мне помогли две статьи:
«Использование Zend_Amf и Adobe Flex SDK» (http://zendframework.ru/articles/flex-with-zend-amf)
«Flex and PHP: remoting with Zend AMF» (http://corlan.org/2008/11/13/flex-and-php-remoting-with-zend-amf/)
В общем, основные вещи, я кажется понял. Надо переходить к практике. Отлаживать я буду на собственном компе, а значит мне надо запустить Denwer и всё там настроить, включая, конечно, базу данных. В локальной базе данных я создал две таблицы: audio и advice. Структура таблицы audio полностью повторяет xml ответ от контакта, а таблица advice сейчас состоит из таких полей:
id — номер рекомендации, aid — id песни, owner_id — id владельца песни, sender_id — id того, кто отправил рекомендацию, comment — комментарий к рекомендации, read — индикация прочитана рекоммендация или нет.
Теперь нужно настроить Zend часть. Во-первых, мне врядли пригодится весь фреймворк, так что я ограничился компонентами: Amf, Db, Filter, Validate, Xmlrpc. Попробуем создать Amf endpoint, как описано в статье. Для написания PHP кода, буду использоать NetBeans, я было попробовал использовать Zend Studio, но у меня всё это дело так дико тормозило, что пришлось отказаться от этой затеи. Начну с создания Value Object (описывыющий класс) для рекомендаций: VOadvice.php. Не буду описывать процесс создания, всё уже описано в статье, которую я упоминал ранее. В общем, проделав, всё что написано в статье по поводу создания серверной части, я получил искомую строчку в браузере «Zend Amf Endpoint», т.е. по идее, всё работает. Опять возвращаемся во Flex и добавляем взаимодействие. Сначала я создал файл services-config.xml, здесь была небольшая загвозка, оказалось, что в начале файла на должно быть пробелов. К счастью, google тут же просветил меня насчет этого. Весь оставшийся день я провел в отчаяных попытках понять почему ничего не работает. Точнее почему код из статьи работает, а мой полностью аналогичный код — нет. Точную причину я так и не нашел (возможно, причина была в том, что в своей модели рекомендации, я не описал поле id), в конце концов я просто скурпулезно поменял названия переменных в коде из статьи и мне удалось добавить рекомендацию. Это, определенно, вин, а то я уже боялся, что придется ложиться спать побежденным, с мрачными мыслями о неожиданном тупике. К счастью, всё решилось, а значит завтра я смогу продолжить движение к цели.
У меня масса других дел, поэтому сегодня врядли удастся много поработать на плеером, но пару часиков я всё таки выделю. Итак, вчера я остановился на том, что смог заставить код из примера добавить новую рекомендацию в мою базу, теперь надо привести этот код в соответствие с моим проектом, т.е. по сути, мне надо тщательно переименовать классы и методы, так чтобы они отображали моё назначение, а не назначение из статьи (в частности, мне надо переименовать всё что было User и Users в Advice и Advices). Переименование прошло успешно, рекомендации добавляются (хотя иногда бывают ошибки, похожие на timeout, это подтверждается записями вроде «Maximum execution time of 30 seconds exceeded» в логах, но пока что я не буду заострять на них внимание). Теперь нужно добавлять различные проверки на корректность данных и, в первую очередь, нужно убедиться, что запрос отправлен именно пользователем Контакта и именно тем, который указан, для этого есть контакт предоставляет auth_key. Пришло время им воспользоваться. auth_key вычисляется на сервере ВКонтакте следующим образом:
auth_key = md5(api_id + '_' + viewer_id + '_' + api_secret) и передается в приложение посредством flashvars, api_secret известен только автору приложения, следовательно в серверной части, мне надо будет вычислять md5 и сравнивать его с переданым из приложения, чтобы убедиться, что запрос отправлен правильным юзером. Сначала мне надо было хотя бы просто поглядеть на этот самый auth_key, во Flex'е он хранится в Application.application.parameters.auth_key, я довольно долго не мог этот ключ получить, пока не обнаружил, что в настройках приложения вконтакте, надо зайти на вкладку «платежи» и эти самые платежи включить, только тогда начинает выдаваться auth_key. В общем, во Флексе я ключ получил, теперь надо передать его на сервер. Поэтому на сервер теперь будем передавать 2 параметра: объект с рекомендацией и auth_key для сравнения. На сервере заново создаем md5 с известным нам api_secret и переданым viewer_id, если ключи совпадают, то всё в порядке. Тем не менее, теоретически, недобросовестный юзер всё таки может узнать свой собственный auth_key и далее отсылать запросы в обход моей Flex оболочки, поэтому переданые данные в любом случае надо проверять. Что ж, видимо Zend Filter и Zend Validate мне в помощь. Отправляюсь читать доки по этим компонентам. После прочтения несложной документации я добавил несколько фильтров и валидаторов, которые нужным образом фильтруют поступающие данные. Итак, на данный момент мы имеем возможность добавить рекомендацию и можно надеяться, что добавлена она будет безопасным образом. Нужна ещё одна проверка — проверка на существование такой записи вконтакте, т.е. мы должны отправить запрос контакту и получить ответ в котором совпадет поле aid полученное от клиента и полученное от API контакта. В случае успешной проверки, если песни с таким aid не существует в таблице audio — надо её туда добавить. После этого надо будет сделать получение с сервера списка рекомендаций для конкретного юзера, отображение рекомендаций в удобном виде, удаление рекомендаций и отправку уведомлений пользователю о том, что ему поступила новая рекомендация. В общем, опять весь день провозился с плеером и не сделал ничего из других дел. Значит теперь я вернусь к плееру только после того, как сделаю самые срочные дела, думаю это неплохая мотивация скорее с ними расправиться.
На несколько дней я уезжал из города, сначала с друзьями на дачу, потом на турбазу, потом ещё в городе что-то такое отметили, в общем, дней пять я отдыхал и не занимался плеером. Очень хорошо, что в предыдущей записи я описал приблизительный фронт ближайших работ и теперь не придется долго вникать. Итак, нам нужно отправить запрос к контакту, похоже, что пришло время для модуля xmlrpc. Полез разбираться. Так, кажется xmlrpc — это совсем не то. Скорее всего нужен zend_rest, попробую про него почитать. Почитал, попробовал, но в конце концов сделал через обычный Zend_Http_Client (потом переделаю, если понадобится). В ответ от api контакта получил «User authorization failed», по всей видимости запросы от сервера надо составлять более сложным образом, но сейчас уже три часа ночи, поэтому лягу спать, а завтра займусь этим вопросом.
Как выяснилось, с внешнего сервера к апи контакта можно отправлять только защищенные запросы, обычные запросы отправлять нельзя. Это довольно неприятная новость, потому что я лишаюсь возможности проверить поступившие данные, думаю разработчики апи со временем как-то решат эту проблему, а пока что придется довольствоваться тем, что есть. Что ж, значит будем счиатать, что добавление рекомендаций работает. Теперь надо сделать получение рекомендаций. Для начала серверную часть. Как только начал делать, сразу заметил, что забыл самое важное — id получателя. Просто добавляем соответствующее поле в базу данных и вносим небольшие изменения в клиенткую и серверную часть, описывать тут особо нечего. После изменений, в базе появилось поле gid в которое заносится id получателя. Вот теперь точно, можно переходить к получению рекомендаций.
Изначально задумывалось, что клиент будет получать только id треков, а дальше будет сам отправлять запрос к контакту для получения остальной информации, но немного подумав, я понял, что это слишком неэффективный подход. При количестве рекомендаций больше двадцати, их получение будет занимать слишком много времени. Поэтому все треки я буду хранить на сервере, так же, как и данные пользователей и получать эту информацию буду с сервера. Для этого я дополнил свою модель advice новыми свойствами, включающими полную информацию о треке и информацию об отправителе (имя и фамилия). Теперь при отправке рекомендации, проверяется наличие в таблице audio такого трека и если такого ещё нет, то он заносится в таблицу. Аналогично поступаем с отправителем, если его ещё нет в моей базе, то добавляем его туда. Соответственно, когда получаем рекомендацию, то получаем полную информацию: рекомендация, информация о треке, информация об отправителе. Для отображения рекомендаций в приложении, я всё таки решил использовать обычный list с item renderer.
Я тут поработал ещё несколько дней и, кажется, всё сделал. Для начала, я сделал itemRenderer для списка рекомендаций. itemRenderer включает в себя пару label с указанием от кого пришла рекомендаций и названием трека, TextArea, в которой отобржается комментарий и кнопка «удалить». Заполняется список примерно так:
private function getAllHandler(event:ResultEvent):void
{
if(event.result.toString() == '') Alert.show('Рекомендаций нет');
else tlAdvices.dataProvider = event.result as Array;
adviceLoaded = true;
}
Запрос на получение списка рекомендаций отправляется при переключении на соответствующую вкладку и только один раз, за это отвечает переменная adviceLoaded. Вся эта конструкция отлично заработала и я перешел к следующему шагу. Теперь надо было сделать получение списка непрочитаных рекомендаций, и если количество непрочитаных рекомендаций больше ноля, то надо это значение отобразить в скобочках в заголовке вкладки «рекомендацийй», т.е. должно быть примерно так «рекомендации (2)». Создаём на сервере новый метод countRead, не буду описывать его целиком, в общем, после всех проверок отправляется такой запрос и возвращается его результат:
$select ->from(array('a' => 'advice'), array('num' => 'count(*)'))
->where('a.gid =?', $gid)
->where('a.read = 0');
Соответственно в клиенте, при запуске плеера мы отправляем запрос roAdvices.countRead(cuid, auth);, и обрабатываем результат запроса вот таким образом:
private function countReadHandler(event:ResultEvent):void
{
if(int(event.result)>0) cAdvices.label = cAdvices.label + ' (' + String(event.result) + ')';
}
Отлично, это работает, теперь надо как-то помечать прочитаные и непрочитаные рекомендации, а так же добавить механизм, который будет переводить рекомендацию в разряд прочитаных. Здесь я решил воспользоваться схемой, которая реализована в «TheBat», непрочитаные рекомендации выделяются жирным шрифтом, прочитаные — обычным, и рекомендация считается прочитаной, после нажатия на неё. Прочитаность или непрочитаность определяется свойством read, которое может принимать значения 1 или 0, сооветственно. Отображать статус рекомендации, естественно, надо внутри itemRenderer. Для этого добавляем туда немного actionscript:
override public function set data( value:Object ): void {
super.data = value;
if(int(data.read)==0)
{
lSender.setStyle(«fontWeight», «bold»);
lTrack.setStyle(«fontWeight», «bold»);
}
else
{
lSender.setStyle(«fontWeight», «normal»);
lTrack.setStyle(«fontWeight», «normal»);
}
if(String(data.comment) == '') taComment.htmlText='Комментария нет';
if(String(data.comment) != '') taComment.htmlText=data.comment;
}
Это позволило разделять прочитаные и непрочитаные рекомендации. Теперь нужно, чтобы после нажатия на рекомендацию, она становилась прочитаной. Это делается в два этапа, во-первых надо изменить поле read для этой рекомендации в базе на сервере, во-вторых отобразить эти изменения в приложении. Добавляем в компонент List такое свойство itemClick=«checkRead(event)», а дальше пишем саму функцию и обработчик события для него:
public function checkRead(event:Event):void
{
if(int(event.currentTarget.selectedItem.read) == 0)
{
index = event.currentTarget.dataProvider.getItemIndex(event.currentTarget.selectedItem);
roAdvices.updateRead(event.currentTarget.selectedItem.id, cuid, auth);
}
}
private function updateReadHandler(event:ResultEvent):void
{
tlAdvices.dataProvider[index].read = 1;
tlAdvices.invalidateList();
}
Ну и на сервере создаем метод updateRead, который завершается таким образом:
$adviceData = array('read' => 1);
$where[] = «id = ». $this->_db->quote($id);
$where[] = «gid = ». $this->_db->quote($gid);
return $this->_db->update('advice', $adviceData, $where);
invalidateList() в функции updateReadHandler нужен, для того, чтобы изменения отобразились в списке.
На данный момент, у нас уже есть возможность, создавать, получать и обновлять рекомендацию, осталось только добавить удаление. Для удаления у нас предусмотрена кнопка «Удалить» в itemRenderer. Хорошей практикой считается отправлять из itemRenderer event наверх, который там будет ловить какая-то функция и делать нужное действие, но у меня почему-то не получилось так сделать, поэтому все действия по удалению записи я производил прямо из itemRenderer. Здесь мне пришлось познакомиться с такой вещью, как listData. listData содержит информацию о родительском компоненте, эта информация нужна мне для, того, чтобы после удаления записи из базы на сервере, так же удалить её и в приложении. Для начала я добавил свойство
implements=«mx.controls.listClasses.IDropInListItemRenderer» в базовый компонент, в моём случае это был vbox:
<?xml version=«1.0» encoding=«utf-8»?>
<mx:VBox xmlns:mx=«www.adobe.com/2006/mxml» width=«298» height=«124» horizontalScrollPolicy=«off» paddingLeft=«10» paddingRight=«10» paddingTop=«10» dropShadowEnabled=«true» dropShadowColor="#A4A4A4" borderStyle=«solid» borderThickness=«0» implements=«mx.controls.listClasses.IDropInListItemRenderer»>
А так же добавил функции:
private var _listData:BaseListData;
public function get listData(): BaseListData
{
return _listData;
}
public function set listData( value:BaseListData ): void
{
_listData = value;
}
Теперь у меня есть доступ к listData, а дальше всё просто. Добавляем пару функций:
private function itemDelete():void
{
var ow:List = listData.owner as List;
index = ow.dataProvider.getItemIndex(this.data);
roAdvices.deleteById(this.data.id, parentDocument.cuid, parentDocument.auth);
}
private function deleteHandler(event:ResultEvent):void
{
var ow:List = listData.owner as List;
ow.dataProvider.removeItemAt(ow.dataProvider.getItemIndex(this.data));
ow.invalidateList();
}
Ну и в кнопку добавляем свойство click=«itemDelete()».
В компонент list, который отображает список всех рекомендаций, я добавил itemDoubleClick=«onUrlChange(event)», это свойство запускает проигрывание песни при двойном щелчке на рекомендации.
Теперь всё готово, осталось обновить приложение в контакте. К моему удивлению, после загрузки новой версии приложения, я не смог соедениться с сервером, для получения рекомендаций. Оказалось, что проблема в файле crossdomain.xml, этот файл должен лежать в коревном каталоге сервера и разрешать получение данных, в моём случае он выглядит вот так:
<?xml version=«1.0»?>
<!DOCTYPE cross-domain-policy SYSTEM «www.macromedia.com/xml/dtds/cross-domain-policy.dtd»>
<cross-domain-policy>
<allow-access-from domain=«www.company.com» secure=«false» />
</cross-domain-policy>
После создания этого файла, всё заработало. Если есть, какие-нибудь вопросы, деньги, нефть или серверы — пишите.
Приложение делается с помощью Flex, а под катом описан мой опыт работы вот с такими штуками: TabNavigator, Menu Control, работа с координатами, pop up окна, TitleWindow, самодельные event'ы, Zend, Zend AMF, работа с базой данных, ItemRenderer, crossdomain policy.
А если более коротко, то я просто описываю, как научился добавялть, читать, обновлять и удалять информацию из базы данных при помощи связки Flex + Zend AMF.
10.06.09
Итак, за первые 5 дней «Мидия» набрала 330 пользователей. Не знаю, много это или мало, но главное, что процесс пошел и не собирается останавливаться. Я, в течении этих пяти дней, дал себе передышку и практически не работал над плеером, но расслабляться нельзя, поэтому возобновляю работу и как и прежде буду документировать свои действия.
За первые же дни набралось приличное количество багов и пожеланий пользователей, которые я и буду постепенно устранять/добавлять. Сегодня я начал с добавления логотипа lastFM и ссылки на www.lastfm.com, это обязательное требование, при использовании их API.
Один из пользователей предложил добавить возможность проигрывания всей музыки друзей в одном списке. На мой взгляд, здравая мысль, поэтому ей мы и займемся. К сожалению, в API контакта нет возможности передать список пользователей и получить список их аудио записей одним запросом, а значит придется последовательно передавать в API идентификаторы пользователей и добавлять полученные записи в общий список. Другой интересный вопрос, как добавить в ComboBox (выпадающий список друзей) поле «Вся музыка друзей», это поле выбивается из общей структуры и его надо обрабатывать отдельным способом. С этого и начнем.
Добавить новый пункт в ComboBox было элементарно, создаем новый XML примерно так var newNode:XML = , а потом добавялем его в нужную XMLListCollection через .addItemAt(newNode,1). К сожалению, думая над дальнешими действиями, я решил, что функцию прослушивания всей музыки друзей одним списком пока что не нужно делать. Для того, чтобы составить полный список музыки, например, у 45 друзей, нужно будет отправить 45 запросов к API контакта, при этом максимально можно отправлять 3 запроса в секунду, следовательно на составление списка уйдет минимум 15 секунд, а если при этом возникнут какие-то ошибки, то результат я пока что и вовсе не могу предсказать. В общем, оставим эту функцию на будущее, либо подожду пока в контакте появится возможность передавать сразу несколько uid'ов с методом getAudios, либо буду делать нужную функцию через собственный сервер и собственную базу данных.
13.06.09
Сегодня на дачу, но пока есть время, продолжу разработку. Начнем с исправления кнопки Play. Судя по отзывам, люди, при нажатии на Play, рассчитывают, что начнется проигрывание выделеного трека (что, вообще-то, логично, но изначально я в этом вопросе ориентировался на WinAMP). Задача была решена быстро и ничего особенного для этого делать не пришлось, просто несколько конструкций if...else.
Ещё меня просили, чтобы при нажатии кнопки «Следующий трек», если список закончился, то должен начинаться самый первый трек. Ну это уж совсем элементарно, даже описывать нечего.
Теперь изменение громкости, чтобы громкость изменялась при движении полузнка, а не только после его отпускания. Опять элементарно, всего лишь добавление к <mx:HSlider> свойства liveDragging=«true».
При включенной функции проигрывания случайного трека, кнопка «Следующий трек» должна переходить на следующий случайный трек, а не на следующий по списку. Опять же ничего сложного, if else и всё.
<Когда-то>
Пожалуй я больше не буду указывать конкретные даты, потому что временные промежутки между ними удручают. В предыдущей записи, я почему-то не написал, что сделал обратный отсчёт времени (до конца трека). В общем, я это сделал и уже не помню как. Кроме того, я наконец-то сделал более менее заметное улучшение. Теперь выводится не только информация об исполинтеле, но и текст текущей песни, который берется с lyricwiki (идею с текстом я подсмотрел в другом плеере, в чем честно признаюсь). Для вывода текста песни, я сделал правую колонку с использованием компонента viewstack, который позволяет переключать отображаемую информацию. В первом слое viewstack я так и оставил информацию об исполнителе, а во второй слой поместил самодельный компонент lyrics, который и выводит текст (там всё элементарно, простая отправка запроса к API lyricwiki с помощью HTTPService и вывод полученного результата). Переключение между слоями (я называю это слоями, хотя возможно есть какое-то более подходящее определение) я сделал с помощью компонента Combobox, опять же всё просто, на уровне свойства change=«vsRightColumn.Selectedindex = cbShow.Selectedindex». Всё заработало с первого раза, единственная проблема в том, что до первого переключения viewstack на отображение текста песни, компонент lyrics не активен. В результате при первом переключении на текст песни, никакой текст не отображается, а начинает отображаться, либо после второго переключения, либо после смены трека, но думаю это можно решить с помощью init() функции и, возможно, самодельного event'а, который будет передавать наверх сообщение, что компонент инициализирован.
<Когда-то>
Эти заголовки <когда-то>, наверное, не лучшая идея. Теперь буду придумывать, заголовки со смыслом (или без смысла, но хотя бы интересные). Так вот, похоже, я наконец-то созрел для первых экспериментов по добавлению взаимодействия с базой данных и PHP. Первой функцией, которую я хотел бы реализовать с использованием БД будет возможность рекомендовать трек своим друзьям (естественно, только тем, которые установили приложение). Конечно, для этих целей уже можно использовать «стену», но через плеер это должно быть удобнее, а кроме того, есть надежда, что это простимулирует пользователей (которых сейчас всего 750) приглашать в приложение своих друзей.
Итак, первое, что мне понадобится — это спроектировать базу данных. Я в этом деле полнейший ноль, поэтому полез за информацией на Хабр. Для начала, мне хватило вот этих двух статей (спасибо авторам): http://abarmot.habrahabr.ru/blog/23423/, http://habrahabr.ru/blogs/development/45707/. Прочитав статьи, я выделил для себя 3 сущности: пользователь, трек, рекоммендация и сделал для них соответствующие таблицы (users, audio, advices), которые, как мне кажется, удовлетворяют нормальным формам. Ещё я на днях прочитал небольшую часть книжки Getting Real от 37signals и там было написано, что сначала надо сделать интерфейс. Как-то раз я, ради интереса, хотел воспользоваться их продуктами, 5 секунд потыкал в ссылки на их сайте, ничего не понял и забил, но всё равно они, судя по всему, крутые ребята и знают о чём(люблю букву Ё) говорят, так что последую их совету, запущу Flex Builder и буду делать интерфейс. С интерфейсом, кстати, проблема, потому что у меня уже заканчивается свободное место для всяких кнопочек и т.п., но я в лучших традициях быдло проектировщика не буду обращать на это внимание и буду лепить, как получится. Довольно скоро, такой подход перестанет работать, и тогда мне придется капитально всё переработать, но я хотя бы уже буду знать, что к чему и смогу проектировать опираясь на реальный опыт вполне конкретного приложения. В общем, буду резать семь раз, для того, чтобы понять, как оптимально мерить.
Рекомендации можно будет тут же проигрывать в плеере, поэтому отображать их я буду на месте списка песен. Переключение между рекомендациями и своим списком буду делать при помощи Viewstack и кнопок на панели плеера. Начну с кнопок. Нет, я попробовал начать с кнопок и получилось плохо. Начну с Viewstack. Итак, мне надо добавить viewstack и на одном слое разместить старый добрый список песен, а на другом слое datagrid с рекомендациями. Нет, я опять соврал. Оказывается лучше всего для моей цели подойдет TabNavigator — это viewstack к которому уже приделаны табы, для переключения между слоями. Да, вот теперь всё правильно. TabNavigator просто супер, даже странно, почему я раньше его не использовал. Теперь надо добавить в TabNavigator datagrid (или, попросту говоря, таблицу) для отображения рекомендаций. Datagrid должен состоять из следующих колонок: отправитель рекомендации, название исполнителя и трека, длительность и кнопка для получения дополнительного комментария, если отправитель его оставил. Получается довольно много и в одну строчку всё это будет смотреться плохо, нужно найти способ вынести отправителя над треком. Пожалуй, лучше использовать TileList. В общем, я пока что поставил пустой TileList, дальше надо будет его дорабатывать, когда появится работающий dataProvider. Будем считать, что интерфейс для отображения рекомендаций у меня есть, теперь нужен интерфейс для отправки рекомендаций. Вот здесь, похоже пришло время для добавления новой колонки в DataGrid со списком песен. Эта колонка будет содержать в себе кнопку для вызова меню трека. Пока что в меню будет только одна функция — «рекомендовать», но со временем могут появиться и другие.
<Продолжаю делать рекомендации>
Во-первых, я решил, что совершенно логичным будет делать меню трека с помощью Menu control. Сначала я добавил новую колонку в DataGrid с треками, в эту колонку, при помощи itemRenderer поместил обычную кнопку. Первое, что мне надо сделать — это вывести меню в нужном месте, т.е. вплотную к правому верхнему углу нажатой кнопки, для этого мне понадобится определять координаты кнопки. X координата меняться не будет, так же, как и размер кнопки, следовательно задача сводится к определению Y координаты. Для проверки, как это работает, я добавил к кнопке click=«testButton(event)» и, собственно, саму функцию:
public function testButton(e:Event):void
{
var tmpString:String = new String();
tmpString=«y: » + String(e.target.y);
Alert.show(tmpString);
}
Как оказалось, для вызова функции из itemRenderer необходимо использовать свойство outerDocument, а сама функция не должна быть private. Таким образом, свойство click пришлось переписать на click=«outerDocument.testButton(event)», после этого всё заработало и я получил Y координату. Y координата показывалась относительно родительского элемента, это было не то, что мне нужно, мне нужно узнать координату относительно всего компонента. Идём в google и пишем
flex 3 coordinates, первой же ссылкой идет статья «Using Flex coordinates» из Adobe Flex 3 Help. Из статьи становится понятно, что существует три типа координат: global, local и content, мне нужна именно global координата и получить её можно методом localToGlobal. Попробуем использовать такую конструкцию.
public function testButton(e:Event):void
{
var tmpString:String = new String();
var pt:Point = new Point(e.target.x, e.target.y);
pt = e.target.localToGlobal(pt);
tmpString=«y: » + String(pt.y);
Alert.show(tmpString);
}
Как и ожидалось, получили глобальную Y координату. Теперь надо создать меню. Как всегда обращаемся к Help'у, в данном случае к «Using Menu-Based Controls». Для начала надо определить структуру меню, это можно сделать различными способами, лично я буду делать через XML, вот так:
<mx:XML format=«e4x» id=«trackMenu»>
</mx:XML>
Теперь делаем функцию для вывода меню:
public function showTrackMenu(e:Event):void
{
var trackMenu:Menu = Menu.createMenu(null, trackMenuData, false);
trackMenu.labelField="@label";
var pt:Point = new Point(e.target.x, e.target.y);
pt = e.target.localToGlobal(pt);
trackMenu.show(pt.x + e.target.width, pt.y);
}
Назначаем кнопке эту функцию: click=«showTrackMenu(event)». Результат получился неожиданый, по горизонтали меню раполагается отлично, а вот по Y меню уезжает вниз ровно на столько, сколько кнопок находится выше нажатой. Я так и не смог понять, почему так просиходит, поэтому сделал вот так:
trackMenu.show(e.target.x + e.target.width+3,e.target.y+294);
Решение, конечно, отстойное, но работает — меню появляется там где надо. Теперь нужно, чтобы при нажатии на пункт меню, появлялось всплывающее окно со списком друзей и возможностью ввести комментарий к своей рекомендации. Сначала нужно понять, как, вообще, определить, что пользователь нажал на определенный пункт меню, для этого я добавлю в меню ещё один пункт «закрыть» и буду с ним экспериментировать. Всё оказалось, очень просто добавляем eventListener и функцию для обработки event'а. Ради эксперимента я добавил к первому пункту меню свойство selectedMenu=«rec», а потом проверил, как это работает:
public function showTrackMenu(e:Event):void
{
var trackMenu:Menu = Menu.createMenu(dgTracklist, trackMenuData, false);
trackMenu.labelField="@label";
trackMenu.addEventListener(MenuEvent.ITEM_CLICK, itemClickInfo);
trackMenu.show(e.target.x + e.target.width+3,e.target.y+294);
}
private function itemClickInfo(event:MenuEvent):void
{
if(event.item.@menuSelected==«rec») Alert.show(event.item.@menuSelected);
}
Всё прекрасно сработало. Теперь нужно добавить всплывающее окно, со списком друзей, полем для комментария и кнопками «отправить» и «отмена». Я уже знаю, что для этого нужно использовать TitleWindow. Не смотря на то, что меню «рекомендовать трек» находится в компоненте плеера, всплывающее окно я буду создавать из основного приложения, а значит надо передать event из компонента с плеером в основное приложение. При этом, в event'е надо будет передавать полную информацию о рекомендуемом треке. Надо создавать custom event. Кажется, я ещё не описывал создание соственных event'ов, поэтому опишу этот процесс поподробнее. Сначала я добавил определение нового event'а
<mx:Metadata>[Event(name=«giveAdvice», type=«com.vkapps.events.GiveAdviceEvent»)]</mx:Metadata>
Теперь надо его создать, создаем новый файл GiveAdviceEvent.as и в нём описываем сам event. Тут я выложу прямо исходником, думаю в нём всё понятно:
package com.vkapps.events
{
import flash.events.Event;
public class GiveAdviceEvent extends Event
{
public static const GIVE_ADVICE:String = «giveAdvice»;
public var aid:String;
public var owner_id:String;
public var artist:String;
public var title:String;
public var duration:String;
public var url:String;
public function GiveAdviceEvent(aid:String,owner_id:String,artist:String,title:String,duration:String,url:String)
{
super(GIVE_ADVICE);
this.aid = artist;
this.owner_id = title;
this.artist = artist;
this.title = title;
this.duration = artist;
this.url = title;
}
override public function clone( ):Event
{
return new GiveAdviceEvent(this.aid,this.owner_id,this.artist,this.title,this.duration,this.url);
}
}
}
Ну вот, event есть. Осталось только его отправить в нужный момент, вот таким образом:
private function itemClickInfo(event:MenuEvent):void
{
if(event.item.@menuSelected==«rec»)
{
var giveAdvice:GiveAdviceEvent = new GiveAdviceEvent(dgTracklist.selectedItem.aid,dgTracklist.selectedItem.owner_id,dgTracklist.selectedItem.artist, dgTracklist.selectedItem.title,dgTracklist.selectedItem.duration,dgTracklist.selectedItem.url)
dispatchEvent(giveAdvice);
}
}
Переходим в основное приложение и добавляем event listener:
player.addEventListener(«giveAdvice»,giveAdvice);
Теперь нужно подготовить всплывающее окно, для создаем новый компонент на основе TitleWindow. Размещаем там необходимые контролы и объявляем переменные, которые будут содержать информацию о треке, примерно, вот так:
[Bindable]
public var _artist:String;
Опять возвращаемся в основную функцию и пишем функцию для обработки event'а (я её сократил):
private function giveAdvice(event:Object):void
{
var pop1:AdviceForm = AdviceForm(PopUpManager.createPopUp(this, AdviceForm, true));
pop1.title=«Отправьте рекомендацию»;
pop1.showCloseButton=true;
pop1._artist=event.artist;
…
pop1._url=event.url;
PopUpManager.centerPopUp(pop1);
}
Всё, основной интерфейс для рекомендаций готов. Теперь нужна серверная часть.
<API, Zend и всякие такие штуки>
Я написал, что интерфейс для рекомендаций готов, но на самом деле я немного соврал. Для полного счастья, надо чтобы во вспылывающем TitleWindow отображался список друзей установивших приложение, потому что рекомендовать можно только им. В api контакта уже есть подходящий метод getAppFriends. Проблема в другом — опять создавать запрос к api прямо во всплывающем окне — это уже совсем уродливо и приводит меня к необходимости сделать отдельный класс для работы с api контакта и дальше все запросы отправлять при помощи экземпляров этого класса (вообще-то, это очевидно, но раньше острой необходимости в этом небыло). Сдеать класс было элементарно, трудности возникли, когда мне понадобилось вернуть результат запроса из класса в компонент. Единственный (по крайней мере других я не знаю) способ это сделать — это event'ы. Мне нужно отправлять ResultEvent наверх, для этого я опять сделал собственный event:
import flash.events.Event;
import mx.rpc.events.ResultEvent;
public class CustomResultEvent extends Event
{
public static const FRIENDS_RESULT:String = «friendsResult»;
public static const FRIENDS_APP_RESULT:String = «friendsAppResult»;
public var customResult:ResultEvent;
public function CustomResultEvent(type:String, e:ResultEvent)
{
super(type);
this.customResult = e;
}
override public function clone():Event
{
return new CustomResultEvent(type, customResult);
}
}
В файле с API я отправляю его, например, таким образом:
private function friendsAppRequestHandler(event:ResultEvent):void
{
var friendsAppResult:CustomResultEvent = new CustomResultEvent(«friendsAppResult»,event)
dispatchEvent(friendsAppResult);
}
А принимаю, например, вот так:
private function init()
{
api.addEventListener(«friendsAppResult»,friendsHandler);
api.requestAppFriends(this._uid);
}
[Bindable]
private var lFriendsList:XMLListCollection = new XMLListCollection();
private function friendsHandler(event:CustomResultEvent):void
{
lFriendsList.source = new XMLList(event.customResult.result.user);
}
Вот теперь интерфейс на самом деле готов и опять теперь нужно делать серверную часть. Пока что это кажется трудным. Для серверной части я планирую использовать Zend, поэтому идём google ищем «flex zend». Первая ссылка ведет на статью: «Flex and PHP: Party in the Front, Business in the Back», я быстренько пробежал её глазами, но кажется, это не то, что мне надо. Следом я перешел на статью «Integrating Adobe Flex and PHP» и это уже что-то более близкое к тому, что я искал (несмотря на то, что Zend в ней никак не используется). В конце статьи шла ссылка на блог автора: blogs.adobe.com/mikepotter я перешел по ссылке и там, впервые, прочитал про AMF. Дальнейшие поиски в этом направлении, убедили меня, что оптимальным будет использование связки Flex + Zend AMF и в этом смысле мне помогли две статьи:
«Использование Zend_Amf и Adobe Flex SDK» (http://zendframework.ru/articles/flex-with-zend-amf)
«Flex and PHP: remoting with Zend AMF» (http://corlan.org/2008/11/13/flex-and-php-remoting-with-zend-amf/)
<Denver, Zend, AMF>
В общем, основные вещи, я кажется понял. Надо переходить к практике. Отлаживать я буду на собственном компе, а значит мне надо запустить Denwer и всё там настроить, включая, конечно, базу данных. В локальной базе данных я создал две таблицы: audio и advice. Структура таблицы audio полностью повторяет xml ответ от контакта, а таблица advice сейчас состоит из таких полей:
id — номер рекомендации, aid — id песни, owner_id — id владельца песни, sender_id — id того, кто отправил рекомендацию, comment — комментарий к рекомендации, read — индикация прочитана рекоммендация или нет.
Теперь нужно настроить Zend часть. Во-первых, мне врядли пригодится весь фреймворк, так что я ограничился компонентами: Amf, Db, Filter, Validate, Xmlrpc. Попробуем создать Amf endpoint, как описано в статье. Для написания PHP кода, буду использоать NetBeans, я было попробовал использовать Zend Studio, но у меня всё это дело так дико тормозило, что пришлось отказаться от этой затеи. Начну с создания Value Object (описывыющий класс) для рекомендаций: VOadvice.php. Не буду описывать процесс создания, всё уже описано в статье, которую я упоминал ранее. В общем, проделав, всё что написано в статье по поводу создания серверной части, я получил искомую строчку в браузере «Zend Amf Endpoint», т.е. по идее, всё работает. Опять возвращаемся во Flex и добавляем взаимодействие. Сначала я создал файл services-config.xml, здесь была небольшая загвозка, оказалось, что в начале файла на должно быть пробелов. К счастью, google тут же просветил меня насчет этого. Весь оставшийся день я провел в отчаяных попытках понять почему ничего не работает. Точнее почему код из статьи работает, а мой полностью аналогичный код — нет. Точную причину я так и не нашел (возможно, причина была в том, что в своей модели рекомендации, я не описал поле id), в конце концов я просто скурпулезно поменял названия переменных в коде из статьи и мне удалось добавить рекомендацию. Это, определенно, вин, а то я уже боялся, что придется ложиться спать побежденным, с мрачными мыслями о неожиданном тупике. К счастью, всё решилось, а значит завтра я смогу продолжить движение к цели.
<AMF, PHP, проверки>
У меня масса других дел, поэтому сегодня врядли удастся много поработать на плеером, но пару часиков я всё таки выделю. Итак, вчера я остановился на том, что смог заставить код из примера добавить новую рекомендацию в мою базу, теперь надо привести этот код в соответствие с моим проектом, т.е. по сути, мне надо тщательно переименовать классы и методы, так чтобы они отображали моё назначение, а не назначение из статьи (в частности, мне надо переименовать всё что было User и Users в Advice и Advices). Переименование прошло успешно, рекомендации добавляются (хотя иногда бывают ошибки, похожие на timeout, это подтверждается записями вроде «Maximum execution time of 30 seconds exceeded» в логах, но пока что я не буду заострять на них внимание). Теперь нужно добавлять различные проверки на корректность данных и, в первую очередь, нужно убедиться, что запрос отправлен именно пользователем Контакта и именно тем, который указан, для этого есть контакт предоставляет auth_key. Пришло время им воспользоваться. auth_key вычисляется на сервере ВКонтакте следующим образом:
auth_key = md5(api_id + '_' + viewer_id + '_' + api_secret) и передается в приложение посредством flashvars, api_secret известен только автору приложения, следовательно в серверной части, мне надо будет вычислять md5 и сравнивать его с переданым из приложения, чтобы убедиться, что запрос отправлен правильным юзером. Сначала мне надо было хотя бы просто поглядеть на этот самый auth_key, во Flex'е он хранится в Application.application.parameters.auth_key, я довольно долго не мог этот ключ получить, пока не обнаружил, что в настройках приложения вконтакте, надо зайти на вкладку «платежи» и эти самые платежи включить, только тогда начинает выдаваться auth_key. В общем, во Флексе я ключ получил, теперь надо передать его на сервер. Поэтому на сервер теперь будем передавать 2 параметра: объект с рекомендацией и auth_key для сравнения. На сервере заново создаем md5 с известным нам api_secret и переданым viewer_id, если ключи совпадают, то всё в порядке. Тем не менее, теоретически, недобросовестный юзер всё таки может узнать свой собственный auth_key и далее отсылать запросы в обход моей Flex оболочки, поэтому переданые данные в любом случае надо проверять. Что ж, видимо Zend Filter и Zend Validate мне в помощь. Отправляюсь читать доки по этим компонентам. После прочтения несложной документации я добавил несколько фильтров и валидаторов, которые нужным образом фильтруют поступающие данные. Итак, на данный момент мы имеем возможность добавить рекомендацию и можно надеяться, что добавлена она будет безопасным образом. Нужна ещё одна проверка — проверка на существование такой записи вконтакте, т.е. мы должны отправить запрос контакту и получить ответ в котором совпадет поле aid полученное от клиента и полученное от API контакта. В случае успешной проверки, если песни с таким aid не существует в таблице audio — надо её туда добавить. После этого надо будет сделать получение с сервера списка рекомендаций для конкретного юзера, отображение рекомендаций в удобном виде, удаление рекомендаций и отправку уведомлений пользователю о том, что ему поступила новая рекомендация. В общем, опять весь день провозился с плеером и не сделал ничего из других дел. Значит теперь я вернусь к плееру только после того, как сделаю самые срочные дела, думаю это неплохая мотивация скорее с ними расправиться.
<Дача, турбаза, «вторник», а потом снова к делам>
На несколько дней я уезжал из города, сначала с друзьями на дачу, потом на турбазу, потом ещё в городе что-то такое отметили, в общем, дней пять я отдыхал и не занимался плеером. Очень хорошо, что в предыдущей записи я описал приблизительный фронт ближайших работ и теперь не придется долго вникать. Итак, нам нужно отправить запрос к контакту, похоже, что пришло время для модуля xmlrpc. Полез разбираться. Так, кажется xmlrpc — это совсем не то. Скорее всего нужен zend_rest, попробую про него почитать. Почитал, попробовал, но в конце концов сделал через обычный Zend_Http_Client (потом переделаю, если понадобится). В ответ от api контакта получил «User authorization failed», по всей видимости запросы от сервера надо составлять более сложным образом, но сейчас уже три часа ночи, поэтому лягу спать, а завтра займусь этим вопросом.
<Только secure>
Как выяснилось, с внешнего сервера к апи контакта можно отправлять только защищенные запросы, обычные запросы отправлять нельзя. Это довольно неприятная новость, потому что я лишаюсь возможности проверить поступившие данные, думаю разработчики апи со временем как-то решат эту проблему, а пока что придется довольствоваться тем, что есть. Что ж, значит будем счиатать, что добавление рекомендаций работает. Теперь надо сделать получение рекомендаций. Для начала серверную часть. Как только начал делать, сразу заметил, что забыл самое важное — id получателя. Просто добавляем соответствующее поле в базу данных и вносим небольшие изменения в клиенткую и серверную часть, описывать тут особо нечего. После изменений, в базе появилось поле gid в которое заносится id получателя. Вот теперь точно, можно переходить к получению рекомендаций.
<База данных>
Изначально задумывалось, что клиент будет получать только id треков, а дальше будет сам отправлять запрос к контакту для получения остальной информации, но немного подумав, я понял, что это слишком неэффективный подход. При количестве рекомендаций больше двадцати, их получение будет занимать слишком много времени. Поэтому все треки я буду хранить на сервере, так же, как и данные пользователей и получать эту информацию буду с сервера. Для этого я дополнил свою модель advice новыми свойствами, включающими полную информацию о треке и информацию об отправителе (имя и фамилия). Теперь при отправке рекомендации, проверяется наличие в таблице audio такого трека и если такого ещё нет, то он заносится в таблицу. Аналогично поступаем с отправителем, если его ещё нет в моей базе, то добавляем его туда. Соответственно, когда получаем рекомендацию, то получаем полную информацию: рекомендация, информация о треке, информация об отправителе. Для отображения рекомендаций в приложении, я всё таки решил использовать обычный list с item renderer.
<Финальные штрихи>
Я тут поработал ещё несколько дней и, кажется, всё сделал. Для начала, я сделал itemRenderer для списка рекомендаций. itemRenderer включает в себя пару label с указанием от кого пришла рекомендаций и названием трека, TextArea, в которой отобржается комментарий и кнопка «удалить». Заполняется список примерно так:
private function getAllHandler(event:ResultEvent):void
{
if(event.result.toString() == '') Alert.show('Рекомендаций нет');
else tlAdvices.dataProvider = event.result as Array;
adviceLoaded = true;
}
Запрос на получение списка рекомендаций отправляется при переключении на соответствующую вкладку и только один раз, за это отвечает переменная adviceLoaded. Вся эта конструкция отлично заработала и я перешел к следующему шагу. Теперь надо было сделать получение списка непрочитаных рекомендаций, и если количество непрочитаных рекомендаций больше ноля, то надо это значение отобразить в скобочках в заголовке вкладки «рекомендацийй», т.е. должно быть примерно так «рекомендации (2)». Создаём на сервере новый метод countRead, не буду описывать его целиком, в общем, после всех проверок отправляется такой запрос и возвращается его результат:
$select ->from(array('a' => 'advice'), array('num' => 'count(*)'))
->where('a.gid =?', $gid)
->where('a.read = 0');
Соответственно в клиенте, при запуске плеера мы отправляем запрос roAdvices.countRead(cuid, auth);, и обрабатываем результат запроса вот таким образом:
private function countReadHandler(event:ResultEvent):void
{
if(int(event.result)>0) cAdvices.label = cAdvices.label + ' (' + String(event.result) + ')';
}
Отлично, это работает, теперь надо как-то помечать прочитаные и непрочитаные рекомендации, а так же добавить механизм, который будет переводить рекомендацию в разряд прочитаных. Здесь я решил воспользоваться схемой, которая реализована в «TheBat», непрочитаные рекомендации выделяются жирным шрифтом, прочитаные — обычным, и рекомендация считается прочитаной, после нажатия на неё. Прочитаность или непрочитаность определяется свойством read, которое может принимать значения 1 или 0, сооветственно. Отображать статус рекомендации, естественно, надо внутри itemRenderer. Для этого добавляем туда немного actionscript:
override public function set data( value:Object ): void {
super.data = value;
if(int(data.read)==0)
{
lSender.setStyle(«fontWeight», «bold»);
lTrack.setStyle(«fontWeight», «bold»);
}
else
{
lSender.setStyle(«fontWeight», «normal»);
lTrack.setStyle(«fontWeight», «normal»);
}
if(String(data.comment) == '') taComment.htmlText='Комментария нет';
if(String(data.comment) != '') taComment.htmlText=data.comment;
}
Это позволило разделять прочитаные и непрочитаные рекомендации. Теперь нужно, чтобы после нажатия на рекомендацию, она становилась прочитаной. Это делается в два этапа, во-первых надо изменить поле read для этой рекомендации в базе на сервере, во-вторых отобразить эти изменения в приложении. Добавляем в компонент List такое свойство itemClick=«checkRead(event)», а дальше пишем саму функцию и обработчик события для него:
public function checkRead(event:Event):void
{
if(int(event.currentTarget.selectedItem.read) == 0)
{
index = event.currentTarget.dataProvider.getItemIndex(event.currentTarget.selectedItem);
roAdvices.updateRead(event.currentTarget.selectedItem.id, cuid, auth);
}
}
private function updateReadHandler(event:ResultEvent):void
{
tlAdvices.dataProvider[index].read = 1;
tlAdvices.invalidateList();
}
Ну и на сервере создаем метод updateRead, который завершается таким образом:
$adviceData = array('read' => 1);
$where[] = «id = ». $this->_db->quote($id);
$where[] = «gid = ». $this->_db->quote($gid);
return $this->_db->update('advice', $adviceData, $where);
invalidateList() в функции updateReadHandler нужен, для того, чтобы изменения отобразились в списке.
На данный момент, у нас уже есть возможность, создавать, получать и обновлять рекомендацию, осталось только добавить удаление. Для удаления у нас предусмотрена кнопка «Удалить» в itemRenderer. Хорошей практикой считается отправлять из itemRenderer event наверх, который там будет ловить какая-то функция и делать нужное действие, но у меня почему-то не получилось так сделать, поэтому все действия по удалению записи я производил прямо из itemRenderer. Здесь мне пришлось познакомиться с такой вещью, как listData. listData содержит информацию о родительском компоненте, эта информация нужна мне для, того, чтобы после удаления записи из базы на сервере, так же удалить её и в приложении. Для начала я добавил свойство
implements=«mx.controls.listClasses.IDropInListItemRenderer» в базовый компонент, в моём случае это был vbox:
<?xml version=«1.0» encoding=«utf-8»?>
<mx:VBox xmlns:mx=«www.adobe.com/2006/mxml» width=«298» height=«124» horizontalScrollPolicy=«off» paddingLeft=«10» paddingRight=«10» paddingTop=«10» dropShadowEnabled=«true» dropShadowColor="#A4A4A4" borderStyle=«solid» borderThickness=«0» implements=«mx.controls.listClasses.IDropInListItemRenderer»>
А так же добавил функции:
private var _listData:BaseListData;
public function get listData(): BaseListData
{
return _listData;
}
public function set listData( value:BaseListData ): void
{
_listData = value;
}
Теперь у меня есть доступ к listData, а дальше всё просто. Добавляем пару функций:
private function itemDelete():void
{
var ow:List = listData.owner as List;
index = ow.dataProvider.getItemIndex(this.data);
roAdvices.deleteById(this.data.id, parentDocument.cuid, parentDocument.auth);
}
private function deleteHandler(event:ResultEvent):void
{
var ow:List = listData.owner as List;
ow.dataProvider.removeItemAt(ow.dataProvider.getItemIndex(this.data));
ow.invalidateList();
}
Ну и в кнопку добавляем свойство click=«itemDelete()».
В компонент list, который отображает список всех рекомендаций, я добавил itemDoubleClick=«onUrlChange(event)», это свойство запускает проигрывание песни при двойном щелчке на рекомендации.
Теперь всё готово, осталось обновить приложение в контакте. К моему удивлению, после загрузки новой версии приложения, я не смог соедениться с сервером, для получения рекомендаций. Оказалось, что проблема в файле crossdomain.xml, этот файл должен лежать в коревном каталоге сервера и разрешать получение данных, в моём случае он выглядит вот так:
<?xml version=«1.0»?>
<!DOCTYPE cross-domain-policy SYSTEM «www.macromedia.com/xml/dtds/cross-domain-policy.dtd»>
<cross-domain-policy>
<allow-access-from domain=«www.company.com» secure=«false» />
</cross-domain-policy>
После создания этого файла, всё заработало. Если есть, какие-нибудь вопросы, деньги, нефть или серверы — пишите.