В прошлой записи я писал, что собираюсь доделать гаджет из этой статьи: Пишем гаджет Windows 7. И я наконец выполнил свое намерение, теперь гаджет выводит время, проведенное в игре. Результат можно увидеть на картинке выше.

Данные гаджет берет из Стима. Как указано в документации, Steam отдает данные о профиле в XML-формате по ссылкам вида http://steamcommunity.com/profiles/<SteamID>/?xml=1 или http://steamcommunity.com/id/<CustomURL>/?xml=1. Также мы можем получить статистику пользователя в конкретной игре по ссылкам вида http://steamcommunity.com/profiles/<SteamID>/stats/<CommunityGameName>/?xml=1 или http://steamcommunity.com/id/<CustomURL>/stats/<CommunityGameName>/?xml=1. Для большинства игр в статистике есть только информация по достижениям, но для некоторых игр указаны и другие данные. Так, у Counter-Strike: Source в статистике есть общее время, проведенное в игре. Для профиля данные по игре будут доступны только в том случае, если пользователь играл в нее в последние две недели, и игра отображается на странице его профиля. Поэтому XML профиля не годится как постоянный источник данных для гаджета.

У статистики другая проблема. В XML профиля указывается то количество часов, которое вы видите в своей библиотеке Steam, открывая страницу игры. В статистике же указывается другое время, и обычно оно меньше. Я не знаю, по каким принципам считается количество часов, могу сказать только, что разброс данных может быть большим. Для моего профиля разница составляет ~20 часов. У одного человека из моего списка друзей в профиле указано 600+ часов для CS:S, в статистике же только 90 часов. Исходя из этих соображений, я решил дать пользователю выбор источника данных. На этом сбор информации можно считать завершенным, переходим к написанию кода.

Страничка гаджета мной уже была написана, об этом можно почитать в статье "Пишем гаджет Windows 7". Добавим на нее никнейм пользователя, кнопку "Обновить" и место для вывода количества часов:
<p id="service">
<span id="nick"></span>
|
<span id="refresh" onclick="work()">
<a href="#">Обновить</a>
</span>
</p>
<h3 id="digits"></h3>
По клику на "Обновить" выполняется главная функция, в которой и будет происходить загрузка и обработка данных. Ее мы напишем чуть позже. Эти элементы абсолютно позиционированы с помощью стилей. Должен сказать, позиционирование в гаджете ведет себя странно. Надежнее всего указывать точные расстояния в пикселях, иначе вообще неизвестно, где окажутся ваши элементы. При этом, если открыть страничку в браузере и сравнить с гаджетом, они будут в разных местах. В начале я долго не мог понять, почему гаджет ничего не показывает, а потом оказалось, что он просто выводит данные за границами гаджета, так что их просто не было видно. В общем, значения расстояний я рассчитал подбором.

Для того, чтобы загружать данные, нужно знать никнейм пользователя и выбранный источник. Для этого создадим страницу настроек. Это тоже простая html-страничка, на которой я разместил текстовый инпут и выпадающий список:

<form action="#">
<p class="settingsP">
Никнейм или ID:
<input type="text" name="nickname" id="nickname">
</p>
<p class="settingsP">
Источник данных:
<select name="source" id="source">
<option value="stats">Статистика</option>
<option value="profile">Профиль</option>
</select>
</p>
</form>
Не забудьте в стилях указать точные размеры для окна настроек. Кнопки "ОК" и "Отмена" система создает сама. Теперь напишем скрипт для сохранения настроек. Сохранить настройки для гаджета можно с помощью специального метода System.Gadget.Settings.writeString('name', data), а потом прочитать с помощью System.Gadget.Settings.readString('name'). При закрытии гаджета настройки сбрасываются, чтобы этого избежать, для их сохранения можно использовать файлы. Я решил, что обойдусь без этого. Если пользователь нажимает "ОК", свойство .closeAction объекта event становится равным .Action.commit. Итак, создаем .js-файлик и подключаем к странице настроек:
// Выводим на страницу старые настройки
nicknameOld = System.Gadget.Settings.readString('nickname');
selectedSourceOld = System.Gadget.Settings.readString('source');
document.getElementById('nickname').value = nicknameOld;
if (selectedSourceOld == 'stats') {
document.getElementById('source').selectedIndex = 0;
}
else {
document.getElementById('source').selectedIndex = 1;
}
// Если пользователь нажал OK, сохраняем настройки
function settingsClosing(event) {
if (event.closeAction == event.Action.commit) {
nickname = document.getElementsByName('nickname')[0].value;
selectedSource = document.getElementsByName("source")[0].value;
System.Gadget.Settings.writeString('nickname', nickname);
System.Gadget.Settings.writeString('source', selectedSource);
}
event.cancel = false;
}

System.Gadget.onSettingsClosing = settingsClosing;
При закрытии страницы настроек вызывается событие System.Gadget.onSettingsClosing, которому я присвоил функцию settingsClosing. Значение элементов формы я получаю методом .value, а устанавливаю пункт выпадающего списка методом .selectedIndex.

Теперь у нас есть настройки, можно загружать и обрабатывать данные. Создаем новый .js-файл и подключаем его к странице гаджета. Сначала объявим главную функцию и сразу сделаем автообновление данных повтором функции с интервалом в час:
(function work() {
// Код
setTimeout(work, 3600000)
}) ();
Дальнейший код должен находиться внутри этой функции. Чтобы подключить настройки, нужно указать ссылку на соответствующую страницу, как указано в документации к гаджетам:
System.Gadget.settingsUI = 'settings.html';
При первом запуске гаджета настройки еще не указаны, поэтому следует установить настройки по умолчанию:
if (System.Gadget.Settings.readString('nickname') == '') {
//Здесь можно указать свой ник для установки по умолчанию
System.Gadget.Settings.writeString('nickname', '');
}
if (System.Gadget.Settings.readString('source') == '') {
System.Gadget.Settings.writeString('source', 'stats');
}
Теперь читаем и применяем настройки:
// Читаем настройки
var nickname = System.Gadget.Settings.readString('nickname');
// Если введено ID, то тип переменной nickname будет числовым, необходимо привести ее к строковому типу
nickname = nickname.toString();
var source = System.Gadget.Settings.readString('source');
// Собираем персональные ссылки на XML
if (nickname.substring(0, 7) == '7656119') { // число 7656119 есть в начале всех ID
urlStats = 'http://steamcommunity.com/profiles/' + nickname + '/stats/CS:S/?xml=1';
urlProfile = 'http://steamcommunity.com/profiles/' + nickname + '/?xml=1';
}
else { // Никнейм
urlStats = 'http://steamcommunity.com/id/' + nickname + '/stats/CS:S/?xml=1';
urlProfile = 'http://steamcommunity.com/id/' + nickname + '/?xml=1';
}
// Выводим никнейм в гаджет
outputNick = document.getElementById('nick');
outputNick.innerHTML = nickname;
// В зависимости от настроек выбираем источник данных
if (source == 'stats') {
urlSteam = urlStats;
}
else {
urlSteam = urlProfile;
}
Теперь, когда мы применили настройки, можно загружать и парсить XML:
// Функция для загрузки XML
function loading() {
if (window.ActiveXObject) {
xmlSteam = new ActiveXObject('Microsoft.XMLDOM');
}
xmlSteam.onreadystatechange = XmlReady;
xmlSteam.load(urlSteam);
}
// Если XML загрузился, парсим его, если не загрузился - загружаем еще раз
function XmlReady() {
if (xmlSteam.readyState == 4) {
if (xmlSteam.childNodes.length == 0) {
loading();
}
else {
if (source == 'stats') {
time_string = xmlSteam.getElementsByTagName('timeplayedfmt')[0].firstChild.data;
time = time_string.substring(0, time_string.lastIndexOf('m') + 1);
}
else {
GamesList = xmlSteam.getElementsByTagName("gameName");
for (i = 0; i < GamesList.length; i++) {
GamesListItem = GamesList[i].childNodes[0].data;
if(GamesListItem == "Counter-Strike: Source") {
time = xmlSteam.getElementsByTagName("hoursOnRecord")[i].firstChild.data + ' hrs';
break;
}
}
}
if (time !== undefined) {
output = document.getElementById('digits');
output.innerHTML = time;
}
}
}
}

loading();
В предыдущем написанном мной гаджете загрузка XML была синхронной. Но запрос к серверу Стима периодически отваливается, гаджет надолго зависает, часто файл загружается не полностью. Поэтому я выбрал асинхронную загрузку с проверкой статуса. Но даже если на запрос приходил ответ "Completed", иногда оказывалось, что файл загружен не полностью. Поэтому я дополнительно проверяю, есть ли в файле хоть что-то, и если нету, то запускаю функцию загрузки еще раз.

С парсингом XML статистики особых сложностей не возникло, берем строку из тега timeplayedfmt и обрезаем из нее секунды, точности до минут вполне хватит. В XML профиля данные о играх находятся в шести блоках mostPlayedGame, в каждом из них есть нужный нам тег hoursOnRecord, но неясно, в каком по счету из этих тегов данные для CS:S. Также в блоке mostPlayedGame находится название игры, поэтому проходимся циклом по тегам gameName и, когда находим значение "Counter-Strike: Source", берем тег hoursOnRecord с таким же номером. Если данные не найдены, в гаджет выводится undefined, поэтому я отсекаю это значение.

На этом функция work() заканчивается, остался один штрих. Когда закрывается окно настроек, их необходимо применить сразу, чтобы не ждать час до следующего обновления. Так что навешиваем на системное событие System.Gadget.onSettingsClosed функцию, которая запустит общую (это надо указать в том же файле после функции work()):
System.Gadget.onSettingsClosed = SettingsClosed;
function SettingsClosed(event) {
if (event.closeAction == event.Action.commit) {
work();
}
}
И еще, если после применения новых настроек данные не будут найдены (например, введен никнейм пользователя, который не играет в CS:S), в гаджет ничего не выведется, и в нем останутся старые значения времени. Поэтому в начале функции work() указываем:
var time = '';
Огромное спасибо Хикке и theaqua за помощь. Если вам что-то непонятно, можно почитать предыдущие статьи на эту тему:
В них я подробно рассказывал о аналогичных действиях. Также можно задать вопрос или высказать предложение/замечание в комментариях.

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

Поиск