Пишем валентинку на D

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

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

Для начала креатива требуется найти (или сделать самим) 17 красивых открыток в виде JPEG (*.jpeg)-файлов, а также необходимо достать и обрезать (примерно до 1 Мб по размеру) красивую романтичную мелодию.

После того, как все будет подготовлено создаем папку, где будет хранится исходный код нашего проекта (назовем папку, допустим, valentine2 …) и в ней создаем еще одну папку под картинки (пусть она будет называться images, ибо так проще).

Первоначально, предполагаем следующее: запускается программка (в начале с окном небольшой подсказки или кратенькой информацией), затем произойдет отрисовка небольшой интересной функции (что это за функция, рассмотрено в статье этого блога под названием «Сердце» ), после чего пользователь увидит еще одно информационное окно, за которым последует появление окна с красивой открыткой, которая будет случайным образом выбрана из приготовленных ранее 17-ти картинок. Кроме того, по щелчку на картинке будет проиграна выбранная нами мелодия, а по щелчку на форме появится окно с красивым стихотворением (которое я написал сам).

Приступая к выполнению задуманного, будем считать, что программка не является одним монолитным файлом исходного кода (с ресурсами в виде картинок и mp3-файла), а состоит из нескольких модулей, которые будут содержать нужный нам функционал. Начнем с того, что потребуется каким-то образом поместить картинки и mp3-файл мелодии прямо в исполняемый файл программы, и причем, сделать это динамически: т.е. программа должна сама распаковать (или каким-то иным образом получить доступ к своим ресурсам) и сама же удалить созданные ей временные файлы.

Для того, чтобы осуществить задуманное, воспользуемся одной маленькой хитростью D, которая традиционно применяется для вставки в исходный код текстовых или иных файлов — импортом внешнего файла. Например, вставить картинку можно следующим образом:

ubyte[] <имя переменной> = cast(ubyte[]) import(<имя файла>);

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

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

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

Данные задачи легко решаются с помощью обычной стандартной библиотеки языка программирования, а их решение занимает буквально две — четыре строки:

module datautils;

import std.file;
import std.path;
import std.random;
import std.conv;

// синтаксический сахар
alias ubyte[] arr;

// встраивание картинок
auto f = cast (arr[]) [
             cast(ubyte[]) import(`image0.jpg`),
             cast(ubyte[]) import(`image1.jpg`),
             cast(ubyte[]) import(`image2.jpg`),
             cast(ubyte[]) import(`image3.jpg`),
             cast(ubyte[]) import(`image4.jpg`),
             cast(ubyte[]) import(`image5.jpg`),
             cast(ubyte[]) import(`image6.jpg`),
             cast(ubyte[]) import(`image7.jpg`),
             cast(ubyte[]) import(`image8.jpg`),
             cast(ubyte[]) import(`image9.jpg`),
             cast(ubyte[]) import(`image10.jpg`),
             cast(ubyte[]) import(`image11.jpg`),
             cast(ubyte[]) import(`image12.jpg`),
             cast(ubyte[]) import(`image13.jpg`),
             cast(ubyte[]) import(`image14.jpg`),
             cast(ubyte[]) import(`image15.jpg`),
             cast(ubyte[]) import(`image16.jpg`),
             cast(ubyte[]) import(`image17.jpg`)
         ];

// встраивание музыки
ubyte[] mp3data = cast(ubyte[]) import(`this_love.mp3`);


// путь к временному файлу
string tmpFilePath(string fileName)
{
    string tmpfile = buildPath(tempDir(), fileName);
    return tmpfile;
}


// распаковка данных во временный файл
void tmpFileCreate(string fileName,  ubyte[] fd)
{
    scope(exit) std.file.write(tmpFilePath(fileName), fd);
}


// удаление временного файла
void tmpFileDelete(string fileName)
{
    remove(tmpFilePath(fileName));
}


// распаковать случайно выбранную картинку во временный файл
string randomImage()
{
    auto rnd = Random(unpredictableSeed);
    uint randomIndex = uniform(0, 17, rnd);
    string res = `image` ~ to!string(randomIndex) ~ `.jpg`;
    tmpFileCreate(res, f[randomIndex]);
    return res;
}

и помещается в файл под названием datautils.d.

В начале файла datautils.d объявлен модуль datautils, который облегчит использование уже написанного в нем кода в основном коде валентинки. После объявления модуля, идет стандартная секция импорта из стандартной библиотеки, за которой располагается уже кое-что более интересное: сначала объявляется псевдоним для типа ubyte[], служащий в данном случае синтаксическим сахаром (т.е. конструкцией повышающей удобство написания, но не несущей никакой самостоятельной смысловой нагрузки), а потом объявляется массив массивов ubyte через свежесозданный строкой выше псевдоним.

Для чего необходим массив массивов?

Этот массив служит для того, чтобы непосредственно использовать все преимущества динамических массивов и их индексации, а преобразование типа избавляет от проблем с обращением к элементам массива через вычисляемые в ходе работы программы значения (для чайников: фишка в том, что объявленный массив без явного преобразования типа не может быть проиндексирован с помощью переменной, поскольку массив создается при компиляции программы, а переменная-индекс в это время не может быть вычислена). Кроме того, обратите внимание на названия файлов: перед написанием кода, для дальнейшего удобства, они переименованы в соответствии с простейшим шаблоном image<номер файла>.jpg, а в качестве мелодии использован припев песни Bliss «This Love», в исполнении несравненной Люсинды Дрейтон.

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

Функция tmpFileCreate создаст временный файл, использовав в качестве имени временного файла переданную строку, и запишет в этот временный файл массив беззнаковых байтов, в роли которого выступит какой-либо из созданных на этапе компиляции массивов (напоминаю, эти массивы, на самом деле, байты наших файлов): запись осуществляется одной функцией — std.file.write, которая сделает практически всю работу, при этом оператор scope позаботиться о том, чтобы файл был безопасно закрыт, когда отпадет нужда в записи в него данных.

Функция tmpFileDelete — достаточно тривиальна и просто удалит временный файл, а вот функция randomImage делает уже гораздо более интересные вещи: используя генератор псевдослучайных чисел, функция создает строку с полным путем к произвольно выбранной картинкой (выбор осуществляется из 17 набранных картинок), генерирует по этому пути файл с картинкой и возвращает путь до файла, который также потребуется далее.

С файлами, на этом пока все, и теперь необходимо реализовать проигрывание музыки из mp3-файла, что является довольно нетривиальной задачей: ведь хочется реализовать этот функционал не прибегая к сторонним библиотекам и используя только стандартный функционал…

Итак, поскольку программка будет расчитана на пользователей Windows, а Windows в любом случае у пользователя есть, то разумнее всего, использовать штатные библиотеки операционной системы. В поисках решения этой задачки, я набрел на информацию по библиотеке winmm.dll, которая существует наверное во всех версиях Windows и которая способна обеспечить требуемый функционал: фоновое воспроизведение музыки через «простой» интерфейс.

Создадим в папке нашего проекта файл mediaplayer.d и поместим в него следующее:

module mediaplayer;


import std.string: cstring = toStringz;
import std.c.windows.windows;
import std.conv : to;

// используем winmm.lib
pragma(lib, "winmm.lib");
extern (Windows)
{
    uint mciSendStringA(
        LPCTSTR lpszCommand,
        LPCTSTR lpszReturnString,
        uint cchReturn,
        HANDLE hwndCallback);
}

// обертка для команд к winmm.lib
uint mciSendString(string s)
{
    return mciSendStringA(cstring(s), cast(LPCTSTR) null, 0, cast(HANDLE) 0);
}


// проигрыватель музыки средствами Windows
class MusicPlayer
{

private:
    int _bassVolume, _trebleVolume, _leftVolume, _rightVolume, _masterVolume;
    string _command;
    bool _isOpen;


public:

    this()
    {
        // максимальная громкость
        _bassVolume =   1_000;
        _leftVolume =   1_000;
        _masterVolume = 1_000;
        _rightVolume =  1_000;
        _trebleVolume = 1_000;
    }

    // открыть файл
    void open(string fileName)
    {
        _command = `open ` ~ fileName ~ ` type mpegvideo alias mediaFile`;
        mciSendString(_command);
        _isOpen = true;
    }


    // закрыть файл
    void close()
    {
        _command = `close mediaFile`;
        mciSendString(_command);
        _isOpen = false;
    }

    // воспроизвести (в цикле = нет)
    void play(bool loop = false)
    {
        if (_isOpen) {
            _command = `play mediaFile wait`;
            if (loop) {
                _command ~= ` REPEAT`;
            }
            mciSendString(_command);
        }
    }

    // пауза
    void pause()
    {
        _command = `pause mediaFile`;
        mciSendString(_command);
    }

    // громкость левого динамика
    @property int leftVolume()
    {
        return _leftVolume;
    }

    @property void leftVolume(int value)
    {
        _command = `setaudio mediaFile left volume to ` ~ to!string(10 * value);
        mciSendString(_command);
        _leftVolume = value;
    }

    // громкость правого динамика
    @property int rightVolume()
    {
        return _rightVolume;
    }

    @property void rightVolume(int value)
    {
        _command = `setaudio mediaFile right volume to ` ~ to!string(10 * value);
        mciSendString(_command);
        _rightVolume = value;
    }


    // общая громкость
    @property int masterVolume()
    {
        return _masterVolume;
    }

    @property void masterVolume(int value)
    {
        _command = `setaudio mediaFile volume to ` ~ to!string(10 * value);
        mciSendString(_command);
        _masterVolume = value;
    }

    // громкость басов
    @property int bassVolume()
    {
        return _bassVolume;
    }

    @property void bassVolume(int value)
    {
        _command = `setaudio mediaFile bass to ` ~ to!string(10 * value);
        mciSendString(_command);
        _bassVolume = value;
    }

    // громкость высоких частот
    @property int trebleVolume()
    {
        return _trebleVolume;
    }

    @property void trebleVolume(int value)
    {
        _command = `setaudio mediaFile treble to ` ~ to!string(10 * value);
        mciSendString(_command);
        _trebleVolume = value;
    }

    // файл открыт ?
    @property bool isOpen()
    {
        return _isOpen;
    }

    // текущий проигрыватель (?)
    @property auto getPlayer()
    {
        return this;
    }
}

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

Сначала, используя выражение pragma(lib, <имя библиотеки>), мы показываем компилятору dmd, что следует подключить уже скомпилированную библиотеку (ряд таких библиотек лежит в папке C:\D\dmd2\windows\lib и имеют расширение *.lib), а после чего используем extern(Windows), для указания того, что функция mciSendStringA из библиотеки winmm.dll является внешней по отношению к нашему модулю, а внутри блока extern указываем полную сигнатуру этой функции, взятую из документации от Microsoft. С «голыми» (whole) функциями из DLL работать как-то не очен удобно, а посему, создается небольшая функция-«обертка» (wrapper function) для функции mciSendStringA — mciSendString, в которую будем передавать только строку команды для встроенного в Windows плеера различных медиа-файлов: приводим типы D ко внутренним типам, используемым внутри библиотеки (и здесь важное замечание: в импорт-секции использовано переименование встроенной функции для большего удобства, а все остальное используется как есть) — и все, нас интересует лишь один аргумент функции из winmm.dll, который и будем использовать.

Далее создаем класс MusicPlayer — для еще большего удобства в работе. В конструктор данного класса ничего не передается, и он устанавливает громкости разных аудио-каналов в максимальные уровни, которые будут использованы по умолчанию: все параметры, реализованные в виде приватных переменных, получают значение равное 1 000 (это 100 % громкости, умноженные на 10 — именно в таком формате, winmm.dll ставит громкость). Свойства класса bassVolume, trebleVolume, leftVolume, rightVolume, masterVolume говорят сами за себя, и обозначают соответственно (без аргумента и с аргументом) получение и установку громкости басов, громкости высокочастотной составляющей, громкость левого динамика, громкость правого динамика и основную громкость — использовать их крайне просто и удобно, особенно с учетом того, что коэффициент умножения 10 уже учтен при получении/установки свойств.

Использовать данный класс крайне просто:

MusicPlayer mp = new MusicPlayer();
mp.open(<имя файла.mp3>);
mp.play();

сначала создаем экземпляр класса (изменяя затем, при необходимости, параметры громкости), затем используем метод open, указывая в нем полный путь до файла, а затем запускаем метод play.

После долгой и нудной «сервисной» части, можно перейти к более приятной (и математической части) — написанию первой, простой открытки, не связанной никак с подготовленными открытками. Эта первая открытка будет простой математической формализацией геометрического образа «сердце, построенное из маленьких сердечек» и будет выглядеть примерно так:

module mathvalentine;

import dfl.all;
import std.math;


float heartf(float x)
{
  return sqrt(cos(x)) * cos(200.0 * x) + sqrt(abs(x)) - (3.1415926 / 4.0) * ((4 - x * x) ^^ 0.01);
}


void drawHearts(PaintEventArgs ea)
{
	Pen p = new Pen(Color(255, 0, 0), 1);

	for (float i = -1.6; i < 1.6; i += 0.0096)
	{
		auto x = cast(int) (250 - 120 * i);
                auto y = cast(int) (250 - 120 * heartf(i));
        for (float j = -1.57; j < 1.57; j += 0.01)
        {
        	auto ix = cast(int) (x - 4 * j);
        	auto iy = cast(int) (y - 4 * heartf(j));
        	ea.graphics.drawLine(p, ix, iy, ++ix, ++iy);
        }
	}
}


class MathematicValentine: dfl.form.Form
{
	this()
	{
		initializeMathematicValentine();
	}


	private void initializeMathematicValentine()
	{
		formBorderStyle = dfl.all.FormBorderStyle.FIXED_SINGLE;
		maximizeBox = false;
		minimizeBox = false;
		text = "Математическое поздравление";
		clientSize = dfl.all.Size(494, 471);
	}

	protected override void onPaint(PaintEventArgs ea)
	{
		super.onPaint(ea);
		drawHearts(ea);
	}

	protected override void onClosed(EventArgs ea)
	{
		msgBox("Лови интерактивную открытку ! :)\nПосле того, как это надоедливое окно с информацией закроется  - ты увидишь открытку.\nА щелкнув по картинке, получишь кое-что романтическое.\n\n\nP.S: открыв программку заново, ты увидишь уже другую открытку. Сколько их всего здесь - маленький секрет.\nP.P.S: еще один секрет в том, что можно щелкать не только по картинке :)", "Небольшой сюрприз", MsgBoxButtons.OK, MsgBoxIcon.INFORMATION);
		super.onClosed(ea);
	}
}

естественно, этот код помещаем в отдельный файл, с именем mathvalentine.d.

Работает этот код крайне просто: используется уже однажды описанная функция, дающая картинку, на которой изображено сердце (как эта функция работает - уже описано в статье с одноименным названием), но с небольшим изменением: одна функция отрисовки сердца вставлена в другую!

Тщательно подобранные параметры внутри блока отрисовки дают красочную и интересную картинку (я затратил на перебор и эксперименты с этим около часа !), однако, помимо этого, в простой форме с отображением графики содержится небольшая хитрость, которая применяется в многомодульной программе валентинки: событие onClosed сработает после того, как пользователь закроет окно с математическим поздравлением, и выведет небольшое окошко с текстом подсказки по переходу на "следующий уровень вложенности" (шутка! просто пользователь увидит небольшой предварительный текст о том, что его ждет далее).

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

Для осуществления этого скромного замысла, создается файл greeting.d и помещается в него следующий код:

module greeting;

import dfl.all;
import datautils;
import mediaplayer;


class GreetingCard: dfl.form.Form
{
	private:
	    string _currentImage;
	    dfl.picturebox.PictureBox pictureBox1;

	public this()
	{
		_currentImage = randomImage();
		tmpFileCreate(`this_love.mp3`,  mp3data);
		initializeGreetingCard();
	}


	private void initializeGreetingCard()
	{
		maximizeBox = false;
		minimizeBox = false;
		text = "Интерактивная открытка";
		clientSize = dfl.all.Size(484, 461);
		click ~= &onFormClick;


		with (pictureBox1 = new dfl.picturebox.PictureBox())
		{
			name = "pictureBox1";
			sizeMode = dfl.all.PictureBoxSizeMode.STRETCH_IMAGE;
			bounds = dfl.all.Rect(10, 10, 460, 440);
			image = new Picture(tmpFilePath(_currentImage));
			click ~= &this.onPictureClick;
		        parent = this;
	    }
	}

	private void onPictureClick(Object sender, EventArgs ea)
	{
		MusicPlayer mp = new MusicPlayer();
		mp.open(`this_love.mp3`);
		mp.play();
                mp.close();
	}

	private void onFormClick(Object sender, EventArgs ea)
	{
	    const string message = `
	       Всего, что стоят эти грезы,
	       За все гроши мне не купить
	       И это радости курьезы
	       Мне не дано будет испить.
	       И путь неизбранный когда-то,
	       Что к сердцу вел меня всегда,
	       В блаженном знании забытом
	       Я молча отпущу своим годам.
	       В тебе горит огонь мгновений
	       И люди тянутся к нему,
	       Ища презрений или озарений,
	       Идут на смерть свою...во тьму.
	       Но ты так сладостно прекрасна
	       И так к поклонникам тепла,
	       Что так покорно сладострастны
	       Они идут к тебе, не видя зла.
	       И я в опале неприкрытой,
	       Беся и радуя тебя,
	       Молюсь твердыне не открытой,
               Надеясь, веря и любя...` ~ "\n\n";
	    msgBox(message, "Последний секрет", MsgBoxButtons.OK, MsgBoxIcon.INFORMATION);
	}

	protected override void onClosed(EventArgs ea)
	{
		tmpFileDelete(_currentImage);
		tmpFileDelete(`this_love.mp3`);
		super.onClosed(ea);
	}
}

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

После инициализации формы,на ней будет размещен единственный компонент - PictureBox, который представляет собой просто контейнер под картинку. Но помимо размещения контейнера с изображением, необходимо настроить сам компонент: для чего воспользуемся свойство image, в которое помещается экземпляр класса Picture (в него в свою очередь, передаем путь до уже распакованной открытки-изображения).

Для усиления впечатлений от получившийся красивой открытки (и каждый раз - уникальной, конечно, до определенного предела) сделаем так, чтобы по щелчку на картинке воспроизводилась сама собой (и без всяких побочных процессов) мелодия (в роли которой, как я уже говорил, будет фрагмент песни This Love), для этого к объекту PictureBox прикрепим событие click, которое вызовет обработчик, использующий уже известный нам MusicPlayer.

Кроме того, для объекта PictureBox устанавливается дополнительное свойство sizeMode со значением dfl.all.PictureBoxSizeMode.STRETCH_IMAGE, что обозначает растяжение картинки по всему контейнеру таким образом, чтобы размер картинки не сказался на размере самого контейнера.

Если вы внимательно рассмотрели код, то вы наверняка увидели, что к форме GreetingCard также привязано событие click, и оно сработает, если пользователь нажмет на совсем небольшой области окна, не занятой контейнером PictureBox, и вызовет при этом появление окна со стихотворением как-то написанным мной.

Согласитесь, что в нашем мире, стоит чаще поощрять любопытство рядовых пользователей!

А теперь, после того, как картинка отображена и программа закрылась, программе необходимо прибраться за собой, удалив файл картинки и файл мелодии, что и сделано с помощью описанного выше метода onClosed (я же говорил, что некоторые функции из datautils нам потребуются значительно позже!).

Теперь, когда все готово, создаем файл main.d, в котором выполним интеграцию оставшихся частей программы следующим образом:

import dfl.all;
import mathvalentine;
import greeting;


void main()
{
	msgBox("Поздравляю с Днем всех Влюбленных !\nСпециально для тебя, мой небольшой (и скромный), но многоуровневый подарок.\n\nЗакрой это окно, и увидишь первый уровень. \n:)", "С Днем Святого Валентина !!!", MsgBoxButtons.OK, MsgBoxIcon.INFORMATION);
	try
	{
		Application.enableVisualStyles();
		
		auto mv = new MathematicValentine();
		Application.run(mv);
		
		auto vc = new GreetingCard();
	        Application.run(vc);
	}
	catch(DflThrowable o)
	{
		msgBox(o.toString(), "Fatal Error", MsgBoxButtons.OK, MsgBoxIcon.ERROR);
	}
}

как видите никаких особых хитростей: вызываем окно с предварительным текстом (дабы подсказать пользователю, чего ждать от программы), а затем последовательно создаем/запускаем на выполнение уже описанные классы MathematicValentine и GreetingCard - вот собственно и все: DFL достаточна умна, чтобы последовательно отобразить созданные окна (хотя, так лучше не делать, но для первого раза сойдет).

Компилируем программу командой:

dmd -de -w -release -O -m32 -IC:\D\dmd2\windows\import -JC:\projects\valentine2 dfl_debug.lib ole32.lib oleAut32.lib gdi32.lib Comctl32.lib Comdlg32.lib advapi32.lib uuid.lib ws2_32.lib -L/SUBSYSTEM:WINDOWS main.d datautils.d mediaplayer.d mathvalentine.d greetings.d -of valentine2

(В моем случае весь проект располагался здесь: C:\projects\valentine2). И наслаждаемся результатом - окно с математической открыткой:

Снимокэкранаот2015-02-2119 02 33_0_oи окно с обычной картинкой:
Снимокэкранаот2015-02-2119 02 57_0_o

На всякий случай, текст песни Bliss - This Love:

I feel it now
It's all around me
A silent voice
I can't deny
A mothers arms
How they surround me
They sing the beauty of my life

It will last forever (this love)
It will never fade away (this love)
Never cause me pain (this love) (this love) (this love)

I close my eyes
It takes me over
A memory deep down in my soul
And safe from harm
To be beside you
Our light will shine upon the world

It will last forever (this love)
It will never fade away (this love)
Never cause me pain (this love) (this love) (this love)

А также мое стихотворение:

 Всего, что стоят эти грезы,
За все гроши мне не купить
И это радости курьезы
Мне не дано будет испить.
И путь неизбранный когда-то,
Что к сердцу вел меня всегда,
В блаженном знании забытом
Я молча отпущу своим годам.
В тебе горит огонь мгновений
И люди тянутся к нему,
Ища презрений или озарений,
Идут на смерть свою...во тьму.
Но ты так сладостно прекрасна
И так к поклонникам тепла,
Что так покорно сладострастны
Они идут к тебе, не видя зла.
И я в опале неприкрытой,
Беся и радуя тебя,
Молюсь твердыне не открытой,
Надеясь, веря и любя...

aquaratixc

Программист-самоучка и программист-любитель

Добавить комментарий