Пишем онлайн-радио на языке Vala

Привет всем! В своем первом посте я хочу рассказать о создании простого радио на языке Vala. Я ни разу не программист, а скорее любитель и все свои разработки я писал на Java, но так как на компьютере использую GNU/Linux, а именно старый добрый Debian, да еще на GNOME, то подумал, а почему бы не попробовать написать что-нибудь под эту систему.

Подготовка

Сказано — сделано! Писать решил в родном текстовом редакторе GEdit. Установил компилятор и пакет для разработки на GTK:

sudo apt install valac libgtk-3-dev

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

sudo apt install libgstreamer1.0-dev

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

Графический интерфейс

Файлы

Итак, редактор у нас готов. С чего начать? Конечно, с интерфейса! GUI будем писать ручками в редакторе. Но сначала о самом коде. У нас будет два файла. Один называется Application.vala и имеет такое содержание:

   namespace Raddiola {
    public class Application : Gtk.Application {
        public MainWindow app_window;

    public Application() {
        Object(flags: ApplicationFlags.FLAGS_NONE, application_id: "com.github.alexkdeveloper.raddiola");
    }

    protected override void activate() {
        if(get_windows().length() > 0) {
            app_window.present();
            return;
        }

        app_window = new MainWindow(this);
        app_window.show_all();
    }

    public static int main(string[] args) {
        Gst.init (ref args);
        var app = new Raddiola.Application();
        return app.run(args);
    }
}

}

Он предназначен для инициализации и запуска всех компонентов программы. Второй файл называется MainWindow.vala и содержит описание пользовательского интерфейса и логику приложения.

Внешний вид

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

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

a3d17baf3d1fdaa4e4add6606ac025c1.png

Для добавления и изменения станций имеется вторая страница, которая открывается при нажатии на соответствующую кнопку. Вот она:

019889fd9734b45f78e3df7a7f7232dc.png

Хидербар

После объявления всех необходимых компонентов и указания начальных свойств окна, создаем хидербар:

using Gtk;
using Gst;
namespace Raddiola {
public class MainWindow : Gtk.ApplicationWindow {

private Stack stack;
private Box vbox_player_page;
private Box vbox_edit_page;
private dynamic Element player;
private Gtk.ListStore list_store;
private TreeView tree_view;
private GLib.List list;
private Entry entry_name;
private Entry entry_url;
private Button back_button;
private Button add_button;
private Button delete_button;
private Button edit_button;
private Button play_button;
private Button stop_button;
private string directory_path;
private string item;
private int mode;
    public MainWindow(Gtk.Application application) {
        GLib.Object(application: application,
                     title: "Raddiola",
                     window_position: WindowPosition.CENTER,
                     resizable: true,
                     height_request: 500,
                     width_request: 500,
                     border_width: 10);
    }        



construct {        
Gtk.HeaderBar headerbar = new Gtk.HeaderBar();
headerbar.get_style_context().add_class(Gtk.STYLE_CLASS_FLAT);
headerbar.show_close_button = true;
set_titlebar(headerbar);
back_button = new Gtk.Button ();
    back_button.set_image (new Gtk.Image.from_icon_name ("go-previous-symbolic", Gtk.IconSize.SMALL_TOOLBAR));
    back_button.vexpand = false;
add_button = new Gtk.Button ();
    add_button.set_image (new Gtk.Image.from_icon_name ("list-add-symbolic", Gtk.IconSize.SMALL_TOOLBAR));
    add_button.vexpand = false;
delete_button = new Gtk.Button ();
    delete_button.set_image (new Gtk.Image.from_icon_name ("list-remove-symbolic", Gtk.IconSize.SMALL_TOOLBAR));
    delete_button.vexpand = false;
edit_button = new Gtk.Button ();
    edit_button.set_image (new Gtk.Image.from_icon_name ("document-edit-symbolic", Gtk.IconSize.SMALL_TOOLBAR));
    edit_button.vexpand = false;
play_button = new Gtk.Button();
    play_button.set_image (new Gtk.Image.from_icon_name ("media-playback-start-symbolic", Gtk.IconSize.SMALL_TOOLBAR));
    play_button.vexpand = false;
stop_button = new Gtk.Button();
    stop_button.set_image (new Gtk.Image.from_icon_name ("media-playback-stop-symbolic", Gtk.IconSize.SMALL_TOOLBAR));
    stop_button.vexpand = false;  

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

        back_button.set_tooltip_text("back");
        add_button.set_tooltip_text("add station");
        delete_button.set_tooltip_text("delete station");
        edit_button.set_tooltip_text("edit station");
        play_button.set_tooltip_text("play");
        stop_button.set_tooltip_text("stop");
        back_button.clicked.connect(on_back_clicked);
        add_button.clicked.connect(on_add_clicked);
        delete_button.clicked.connect(on_delete_dialog);
        edit_button.clicked.connect(on_edit_clicked);
        play_button.clicked.connect(on_play_station);
        stop_button.clicked.connect(on_stop_station);
        headerbar.pack_start(back_button);
        headerbar.pack_start(add_button);
        headerbar.pack_start(delete_button);
        headerbar.pack_start(edit_button);
        headerbar.pack_end(stop_button);
        headerbar.pack_end(play_button);

Далее, с помощью методаset_widget_visible мы скрываем на время ненужные кнопки:

set_widget_visible(back_button,false);
set_widget_visible(stop_button,false);

Код метода:

private void set_widget_visible (Gtk.Widget widget, bool visible) {
         widget.no_show_all = !visible;
         widget.visible = visible;
  }

Стек

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

stack = new Stack();
          stack.set_transition_duration (600);
          stack.set_transition_type (StackTransitionType.SLIDE_LEFT_RIGHT);
          add (stack);//добавили стек
   list_store = new Gtk.ListStore(Columns.N_COLUMNS, typeof(string));//начали создавать список
           tree_view = new TreeView.with_model(list_store);
           var text = new CellRendererText ();
           var column = new TreeViewColumn ();
           column.pack_start (text, true);
           column.add_attribute (text, "markup", Columns.TEXT);
           tree_view.append_column (column);
           tree_view.set_headers_visible (false);
           tree_view.cursor_changed.connect(on_select_item);
   var scroll = new ScrolledWindow (null, null);
        scroll.set_policy (PolicyType.AUTOMATIC, PolicyType.AUTOMATIC);
        scroll.add (this.tree_view);
   vbox_player_page = new Box(Orientation.VERTICAL,20);
   vbox_player_page.pack_start(scroll,true,true,0);
   stack.add(vbox_player_page);//добавили в стек контейнер со списком

Вторая страница

Теперь можно приступать к созданию второй страницы. Она содержит два текстовых поля и кнопку. В одно поле пользователь вводит название станции, а в другое ее URL. Поля содержат значки, при нажатии на которые поля можно очистить от содержимого.

entry_name = new Entry();
        entry_name.set_icon_from_icon_name (Gtk.EntryIconPosition.SECONDARY, "edit-clear-symbolic");
        entry_name.icon_press.connect ((pos, event) => {
        if (pos == Gtk.EntryIconPosition.SECONDARY) {
              entry_name.set_text("");//очистка поля ввода названия станции
           }
        });
        var label_name = new Label.with_mnemonic ("_Name:");
        var hbox_name = new Box (Orientation.HORIZONTAL, 20);
        hbox_name.pack_start (label_name, false, true, 0);
        hbox_name.pack_start (entry_name, true, true, 0);
        entry_url = new Entry();
        entry_url.set_icon_from_icon_name (Gtk.EntryIconPosition.SECONDARY, "edit-clear-symbolic");
        entry_url.icon_press.connect ((pos, event) => {
        if (pos == Gtk.EntryIconPosition.SECONDARY) {
              entry_url.set_text("");//очистка поля ввода URL
           }
        });
        var label_url = new Label.with_mnemonic ("_URL:");
        var hbox_url = new Box (Orientation.HORIZONTAL, 20);
        hbox_url.pack_start (label_url, false, true, 0);
        hbox_url.pack_start (entry_url, true, true, 0);
        var button_ok = new Button.with_label("OK");
        button_ok.clicked.connect(on_ok_clicked);
        vbox_edit_page = new Box(Orientation.VERTICAL,20);
        vbox_edit_page.pack_start(hbox_name,false,true,0);
        vbox_edit_page.pack_start(hbox_url,false,true,0);
        vbox_edit_page.pack_start(button_ok,false,true,0);
        stack.add(vbox_edit_page);
        stack.visible_child = vbox_player_page;//показываем первую страницу приложения

Логика

Методы для создания дефолтных станций и их показа

Создание GUI завершено. Дальше нам понадобится фабрика для создания объекта player, c помощью которого мы будем воспроизводить станции. Также нам необходима директория где будут хранится станции в виде текстовых файлов. При создании этой директории нужно создать в ней файлы станций по умолчанию и показать этот список пользователю.

 player = ElementFactory.make ("playbin", "play");//создаем нужный объект с помощью фабрики
   directory_path = Environment.get_home_dir()+"/.stations_for_radio_app";
   GLib.File file = GLib.File.new_for_path(directory_path);
   if(!file.query_exists()){
     try{
        file.make_directory();//создали директорию
     }catch(Error e){
        stderr.printf ("Error: %s\n", e.message);
     }
     create_default_stations();//создание станций по умолчанию
   }
   show_stations();//показываем список станций
 }

Метод create_default_stations.Создаем два массива. В одном хранятся названия десяти станций, а в другом их URL. С помощью цикла создаем десять текстовых документов. Название документа — это название станции, а содержимое документа — ее URL.

private void create_default_stations(){
          string[] name_station = {"NonStopPlay","Classical Music","Fip Radio","Jazz Legends","Joy Radio","Live-icy","Music Radio","Radio Electron","Dubstep","Trancemission"};
          string[] url_station = {"http://stream.nonstopplay.co.uk/nsp-128k-mp3","http://stream.srg-ssr.ch/m/rsc_de/mp3_128","http://direct.fipradio.fr/live/fip-midfi.mp3","http://jazz128legends.streamr.ru/","http://airtime.joyradio.cc:8000/airtime_192.mp3","http://live-icy.gss.dr.dk:8000/A/A05H.mp3","http://ice-the.musicradio.com/CapitalXTRANationalMP3","http://radio-electron.ru:8000/128","http://air.radiorecord.ru:8102/dub_320","http://air.radiorecord.ru:8102/tm_320"};
          for(int i=0;i<10;i++){
            try {
                 FileUtils.set_contents (directory_path+"/"+name_station[i], url_station[i]);
              } catch (Error e) {
                     stderr.printf ("Error: %s\n", e.message);
             }
          }
   }

Метод show_stations:

private void show_stations () {
           list_store.clear();
           list = new GLib.List ();
            try {
            Dir dir = Dir.open (directory_path, 0);
            string? name = null;
            while ((name = dir.read_name ()) != null) {
                list.append(name);
            }
        } catch (FileError err) {
            stderr.printf (err.message);
        }
         TreeIter iter;
           foreach (string item in list) {
               list_store.append(out iter);
               list_store.set(iter, Columns.TEXT, item);//показываем список станций
           }
       }
   

Сообщения пользователю и запуск/остановка воспроизведения

Для вывода сообщений пользователю в приложении используется метод alert. Вот его содержимое:

private void alert (string str){
          var dialog_alert = new Gtk.MessageDialog(this, Gtk.DialogFlags.MODAL, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, str);
          dialog_alert.set_title("Message");
          dialog_alert.run();
          dialog_alert.destroy();
       }   

Запуск и остановка воспроизведения:

private void on_play_station(){ //воспроизведение
         var selection = tree_view.get_selection();
           selection.set_mode(SelectionMode.SINGLE);
           TreeModel model;
           TreeIter iter;
           if (!selection.get_selected(out model, out iter)) {
               alert("Choose a station");
               return;
           }
      string uri;
        try {
            FileUtils.get_contents (directory_path+"/"+item, out uri);
        } catch (Error e) {
            stderr.printf ("Error: %s\n", e.message);
        }
      player.uri = uri;
      player.set_state (State.PLAYING);
      set_widget_visible(play_button,false);
      set_widget_visible(stop_button,true);
   }
   private void on_stop_station(){//остановка воспроизведения
      player.set_state (State.READY);
      set_widget_visible(play_button,true);
      set_widget_visible(stop_button,false);
   }
   

Добавление и изменение станций

Нажатия на кнопки добавления и изменения станции:

private void on_add_clicked () {//к добавлению станции
              stack.visible_child = vbox_edit_page;
              set_buttons_on_edit_page();
              mode = 1;
              if(!is_empty(entry_name.get_text())){
                    entry_name.set_text("");
              }
              if(!is_empty(entry_url.get_text())){
                    entry_url.set_text("");
              }
  }
   private void on_edit_clicked(){//к изменению станции
         var selection = tree_view.get_selection();
           selection.set_mode(SelectionMode.SINGLE);
           TreeModel model;
           TreeIter iter;
           if (!selection.get_selected(out model, out iter)) {
               alert("Choose a station");
               return;
           }
        stack.visible_child = vbox_edit_page;
        set_buttons_on_edit_page();
        mode = 0;
        entry_name.set_text(item);//показываем название станции
        string url;
        try {
            FileUtils.get_contents (directory_path+"/"+item, out url);
        } catch (Error e) {
            stderr.printf ("Error: %s\n", e.message);
        }
        entry_url.set_text(url);//показываем URL станции
   }

Нажатие на кнопку ОК на второй странице приложения. По значению переменной modeопределяем какой режим обрабатывать. Есть два режима: режим добавления и режим изменения станции. Также присутствует логика не позволяющая добавить станцию с уже существующим названием или поменять название станции на уже имеющееся в списке.

private void on_ok_clicked(){
         if(is_empty(entry_name.get_text())){
            alert("Enter the name");
                    entry_name.grab_focus();
                    return;
        }
        if(is_empty(entry_url.get_text())){
           alert("Enter the url");
                   entry_url.grab_focus();
                   return;
        }
        switch(mode){//выбор по значению переменной
            case 0://изменение станции
        GLib.File select_file = GLib.File.new_for_path(directory_path+"/"+item);
        GLib.File edit_file = GLib.File.new_for_path(directory_path+"/"+entry_name.get_text().strip());
        if (select_file.get_basename() != edit_file.get_basename() && !edit_file.query_exists()){
                FileUtils.rename(select_file.get_path(), edit_file.get_path());
                if(!edit_file.query_exists()){
                    alert("Rename failed");
                    return;
                }
                try {
                 FileUtils.set_contents (edit_file.get_path(), entry_url.get_text().strip());
              } catch (Error e) {
                     stderr.printf ("Error: %s\n", e.message);
            }
            }else{
                if (select_file.get_basename() != edit_file.get_basename()) {
                    alert("A station with the same name already exists");
                    entry_name.grab_focus();
                    return;
                }
                try {
                 FileUtils.set_contents (edit_file.get_path(), entry_url.get_text().strip());
              } catch (Error e) {
                     stderr.printf ("Error: %s\n", e.message);
             }
            }
            show_stations();
            break;
            case 1://добавление станции
    GLib.File file = GLib.File.new_for_path(directory_path+"/"+entry_name.get_text().strip());
        if(file.query_exists()){
            alert("A station with the same name already exists");
            entry_name.grab_focus();
            return;
        }
        try {
            FileUtils.set_contents (file.get_path(), entry_url.get_text().strip());
        } catch (Error e) {
            stderr.printf ("Error: %s\n", e.message);
        }
        if(!file.query_exists()){
           alert("Add failed");
           return;
        }else{
           show_stations();
        }
        break;
      }
      on_back_clicked();
   }

Удаление станций и другие методы

Для удаления станций применяется такой метод:

private void on_delete_dialog(){
       var selection = tree_view.get_selection();
           selection.set_mode(SelectionMode.SINGLE);
           TreeModel model;
           TreeIter iter;
           if (!selection.get_selected(out model, out iter)) {
               alert("Choose a station");
               return;
           }
           GLib.File file = GLib.File.new_for_path(directory_path+"/"+item);
         var dialog_delete_station = new Gtk.MessageDialog(this, Gtk.DialogFlags.MODAL,Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK_CANCEL, "Delete station "+file.get_basename()+" ?");
         dialog_delete_station.set_title("Question");
         Gtk.ResponseType result = (ResponseType)dialog_delete_station.run ();
         dialog_delete_station.destroy();
         if(result==Gtk.ResponseType.OK){
         FileUtils.remove (directory_path+"/"+item);
         if(file.query_exists()){
            alert("Delete failed");
         }else{
             show_stations();//показываем список станций после удаления
         }
      }
   }

Для отображения кнопок в хидербаре на разных страницах используются следующие методы:

   private void set_buttons_on_player_page(){//кнопки на странице списка
       set_widget_visible(back_button,false);
       set_widget_visible(add_button,true);
       set_widget_visible(delete_button,true);
       set_widget_visible(edit_button,true);
   }
   private void set_buttons_on_edit_page(){//кнопки на странице изменения/добавления
       set_widget_visible(back_button,true);
       set_widget_visible(add_button,false);
       set_widget_visible(delete_button,false);
       set_widget_visible(edit_button,false);
   }

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

private void on_select_item () {
           var selection = tree_view.get_selection();
           selection.set_mode(SelectionMode.SINGLE);
           TreeModel model;
           TreeIter iter;
           if (!selection.get_selected(out model, out iter)) {
               return;
           }
           TreePath path = model.get_path(iter);
           var index = int.parse(path.to_string());
           if (index >= 0) {
               item = list.nth_data(index);//записываем значение позиции в списке в переменную
           }
       }

Следующий код возвращает пользователя назад к списку станций:

private void on_back_clicked(){
       stack.visible_child = vbox_player_page;
       set_buttons_on_player_page();
   }

Для определения пустоты текстового поля используется метод is_empty:

private bool is_empty(string str){
        return str.strip().length == 0;
      }

Перечислитель для списка:

private enum Columns {
           TEXT, N_COLUMNS
       }

Компиляция и запуск

Чтобы скомпилировать приложение нужно в терминале перейти в директорию исходников программы и ввести такую команду:

valac --pkg gtk+-3.0 --pkg gstreamer-1.0 Application.vala MainWindow.vala

Для запуска приложения командуем:

./Application

Ссылка на репозиторий GitHub: https://github.com/kalexal-kaa/gtk-radio

Ссылка на SourceForge: https://sourceforge.net/projects/gtk-radio/

Вот пожалуй и все! Если пост понравится, то напишу и про другие свои разработки.

© Habrahabr.ru