├── po ├── LINGUAS ├── meson.build └── POTFILES ├── data ├── com.gitlab.nvlgit.Balss.desktop.in ├── com.gitlab.nvlgit.Balss.appdata.xml.in ├── icons │ ├── hicolor │ │ ├── symbolic │ │ │ └── apps │ │ │ │ └── com.gitlab.nvlgit.Balss-symbolic.svg │ │ ├── 16x16 │ │ │ └── apps │ │ │ │ └── com.gitlab.nvlgit.Balss.svg │ │ ├── 24x24 │ │ │ └── apps │ │ │ │ └── com.gitlab.nvlgit.Balss.svg │ │ ├── 32x32 │ │ │ └── apps │ │ │ │ └── com.gitlab.nvlgit.Balss.svg │ │ ├── 64x64 │ │ │ └── apps │ │ │ │ └── com.gitlab.nvlgit.Balss.svg │ │ ├── 128x128 │ │ │ └── apps │ │ │ │ └── com.gitlab.nvlgit.Balss.svg │ │ └── scalable │ │ │ └── apps │ │ │ └── com.gitlab.nvlgit.Balss.svg │ └── meson.build ├── meson.build └── com.gitlab.nvlgit.Balss.gschema.xml ├── src ├── ui │ ├── media-playback-rate-prefix-symbolic.svg │ ├── media-playback-rate-symbolic.svg │ ├── balss-controls-symbolic.svg │ ├── style.css │ ├── chapter-list-box.ui │ ├── chapter-row.ui │ ├── shortcuts-window.ui │ ├── chapter-indicator.ui │ ├── control-button.ui │ └── window.ui ├── balss.gresource.xml ├── main.vala ├── about-dialog.vala ├── meson.build ├── chapter-row.vala ├── app-prefs.vala ├── chapter-list-box.vala ├── chapter-indicator.vala ├── prefs-page.vala ├── app.vala ├── control-button.vala ├── vapi │ └── mpv.vapi ├── mpv-wrapper-backend.vala └── window.vala ├── meson.build ├── README.md ├── com.gitlab.nvlgit.Balss.yml └── LICENSE /po/LINGUAS: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /po/meson.build: -------------------------------------------------------------------------------- 1 | i18n.gettext('balss', preset: 'glib') 2 | -------------------------------------------------------------------------------- /data/com.gitlab.nvlgit.Balss.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Balss 3 | Exec=@app_id@ 4 | Terminal=false 5 | Type=Application 6 | Icon=@app_id@ 7 | Categories=GTK;GNOME;AudioVideo;Player; 8 | StartupNotify=true 9 | Comment=Simple audio book player -------------------------------------------------------------------------------- /src/ui/media-playback-rate-prefix-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Gnome Symbolic Icon Theme 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /po/POTFILES: -------------------------------------------------------------------------------- 1 | data/com.gitlab.nvlgit.Balss.desktop.in 2 | data/com.gitlab.nvlgit.Balss.appdata.xml.in 3 | data/com.gitlab.nvlgit.Balss.gschema.xml 4 | src/window.ui 5 | src/app-menu.ui 6 | src/prefs.ui 7 | src/chapter-list-box.ui 8 | src/app.vala 9 | src/window.vala 10 | src/app-about.vala 11 | -------------------------------------------------------------------------------- /data/com.gitlab.nvlgit.Balss.appdata.xml.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | com.gitlab.nvlgit.Balss.desktop 4 | CC0-1.0 5 | GPL-3.0-or-later 6 | 7 | 8 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/apps/com.gitlab.nvlgit.Balss-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /data/icons/meson.build: -------------------------------------------------------------------------------- 1 | icon_sizes = ['16x16', '24x24', '32x32', '64x64', '128x128', 'scalable'] 2 | 3 | foreach s: icon_sizes 4 | install_data( 5 | join_paths('hicolor', s, 'apps', app_id + '.svg'), 6 | install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', s, 'apps') 7 | ) 8 | endforeach 9 | 10 | install_data( 11 | join_paths('hicolor', 'symbolic', 'apps', app_id + '-symbolic.svg'), 12 | install_dir: join_paths(datadir, 'icons', 'hicolor', 'symbolic', 'apps') 13 | ) -------------------------------------------------------------------------------- /src/balss.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | window.ui 5 | prefs-page.ui 6 | chapter-row.ui 7 | chapter-list-box.ui 8 | chapter-indicator.ui 9 | control-button.ui 10 | shortcuts-window.ui 11 | style.css 12 | 13 | 14 | media-playback-rate-symbolic.svg 15 | media-playback-rate-prefix-symbolic.svg 16 | balss-controls-symbolic.svg 17 | 18 | 19 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('Balss', ['c', 'vala'], 2 | version: '0.1.0', 3 | meson_version: '>= 0.40.0', 4 | ) 5 | 6 | app_id = 'com.gitlab.nvlgit.' + meson.project_name() 7 | 8 | i18n = import('i18n') 9 | 10 | # Directory variables 11 | bindir = get_option('bindir') 12 | datadir = get_option('datadir') 13 | desktopdir = join_paths(datadir, 'applications') 14 | localedir = get_option('localedir') 15 | po_dir = join_paths(meson.source_root(), 'po') 16 | prefix = get_option('prefix') 17 | 18 | doc_subdir = join_paths(datadir, 'doc', meson.project_name()) 19 | install_data('README.md', 20 | install_dir: doc_subdir) 21 | 22 | licenses_subdir = join_paths(datadir, 'licenses', meson.project_name()) 23 | install_data('LICENSE', 24 | install_dir: licenses_subdir) 25 | 26 | 27 | subdir('data') 28 | subdir('src') 29 | #subdir('po') 30 | 31 | meson.add_install_script('build-aux/meson/postinstall.py') -------------------------------------------------------------------------------- /src/ui/media-playback-rate-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | Gnome Symbolic Icon Theme 9 | 10 | 11 | 12 | Gnome Symbolic Icon Theme 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/ui/balss-controls-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | Gnome Symbolic Icon Theme 9 | 10 | 11 | 12 | Gnome Symbolic Icon Theme 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main.vala: -------------------------------------------------------------------------------- 1 | /* main.vala 2 | * 3 | * Copyright (C) 2018 Nick 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | [CCode(cname="VERSION")] extern const string VERSION; 20 | [CCode(cname="APP_ID")] extern const string APP_ID; 21 | [CCode(cname="GETTEXT_PACKAGE")] extern const string GETTEXT_PACKAGE; 22 | [CCode(cname="LOCALEDIR")] extern const string LOCALEDIR; 23 | 24 | 25 | public static int main (string[] args) { 26 | 27 | Intl.setlocale (LocaleCategory.ALL, ""); 28 | Intl.bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); 29 | Intl.bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); 30 | Intl.textdomain (GETTEXT_PACKAGE); 31 | GLib.Environment.set_prgname (APP_ID); 32 | 33 | var app = new Balss.App (); 34 | return app.run (args); 35 | } 36 | -------------------------------------------------------------------------------- /src/ui/style.css: -------------------------------------------------------------------------------- 1 | .balss-listbox-background { 2 | background-color: alpha ( @theme_bg_color, 0.5 ); 3 | } 4 | 5 | .balss-seekbar trough { 6 | background-color: alpha ( @theme_selected_fg_color, 0.1 ); 7 | background-image: none; 8 | background-size: cover; 9 | background-clip: content-box; 10 | border-color: alpha ( @theme_selected_fg_color, 0.7 ); 11 | } 12 | 13 | .balss-seekbar trough highlight { 14 | background-image: image(alpha (@theme_selected_fg_color, 0.9) ); 15 | background-size: cover; 16 | background-clip: content-box; 17 | border-color: alpha ( @theme_selected_fg_color, 0.7 ); 18 | 19 | } 20 | 21 | .balss-seekbar slider { 22 | box-shadow: none; 23 | border-color: transparent; 24 | background-color: transparent; 25 | background-image: none; 26 | background-size: cover; 27 | } 28 | 29 | .balss-seekbar slider:active, 30 | .balss-seekbar slider:hover, 31 | .balss-seekbar:hover slider { 32 | background-color: @theme_selected_fg_color; 33 | border-color: alpha ( black, 0.2 ); 34 | } 35 | 36 | /* workaraund for linked style */ 37 | .balss-frame border { 38 | border-right-width: 0px; 39 | 40 | } 41 | 42 | .balss-active-chapter-row { 43 | background-color: @theme_selected_bg_color; 44 | color: @theme_selected_fg_color; 45 | } 46 | 47 | .balss-active-chapter-row:backdrop label{ 48 | color: @theme_selected_fg_color; 49 | } 50 | 51 | .balss-volume-scale { 52 | padding-left: 0; 53 | padding-right: 0; 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Balss 2 | 3 | 4 | Balss is an easy to use player for audiobook files with chapters such as m4b or mka. The goal of the Balss is that the following capabilities: 5 | * Easy to use chapter time navigation 6 | * Setup playback rate (including setup default playback rate) 7 | * Capability to resume playback last played file from stopped position 8 | 9 | ![ScreenShot](https://user-images.githubusercontent.com/29505119/46797894-10a78e80-cd59-11e8-8c1f-7d3041870cf9.png) 10 | 11 | ### Building and Installation 12 | 13 | ```bash 14 | git clone https://gitlab.com/nvlgit/Balss.git && cd Balss 15 | meson builddir --prefix=/usr && cd builddir 16 | ninja 17 | su -c 'ninja install' 18 | ``` 19 | For rpmbuild: balss.spec 20 | 21 | ### Build Dependencies 22 | * gtk+-3.0 >= 3.22 23 | * libmpv >= 0.28 24 | * meson 25 | 26 | ### Run Dependencies 27 | * libmpv >= 0.28 28 | 29 | 30 | ## Flatpak Building and Installation 31 | 32 | Make temp dir 33 | ```bash 34 | mkdir temp && cd temp 35 | ``` 36 | Build 37 | ```bash 38 | wget https://gitlab.com/nvlgit/Balss/raw/master/com.gitlab.nvlgit.Balss.yml 39 | flatpak-builder build-dir com.gitlab.nvlgit.Balss.yml 40 | flatpak build-export my-repo build-dir 41 | flatpak build-bundle my-repo Balss.flatpak com.gitlab.nvlgit.Balss 42 | ``` 43 | Install 44 | ```bash 45 | flatpak install Balss.flatpak 46 | ``` 47 | Remove temp dir 48 | ```bash 49 | cd .. && rm -R temp 50 | ``` 51 | -------------------------------------------------------------------------------- /data/meson.build: -------------------------------------------------------------------------------- 1 | desktop_conf = configuration_data() 2 | desktop_conf.set('app_id', app_id) 3 | 4 | 5 | desktop_file = i18n.merge_file( 6 | 'desktop', 7 | input: configure_file( 8 | input: '@0@.desktop.in'.format(app_id), 9 | output: '@0@.desktop.in'.format(app_id), 10 | configuration: desktop_conf 11 | ), 12 | output: '@0@.desktop'.format(app_id), 13 | install: true, 14 | install_dir: desktopdir, 15 | po_dir: po_dir, 16 | type: 'desktop' 17 | ) 18 | 19 | desktop_utils = find_program('desktop-file-validate', required: false) 20 | if desktop_utils.found() 21 | test('Validate desktop file', desktop_utils, 22 | args: [desktop_file] 23 | ) 24 | endif 25 | 26 | appstream_file = i18n.merge_file( 27 | input: 'com.gitlab.nvlgit.Balss.appdata.xml.in', 28 | output: 'com.gitlab.nvlgit.Balss.appdata.xml', 29 | po_dir: '../po', 30 | install: true, 31 | install_dir: join_paths(get_option('datadir'), 'appdata') 32 | ) 33 | 34 | appstream_util = find_program('appstream-util', required: false) 35 | if appstream_util.found() 36 | test('Validate appstream file', appstream_util, 37 | args: ['validate', appstream_file] 38 | ) 39 | endif 40 | 41 | install_data('com.gitlab.nvlgit.Balss.gschema.xml', 42 | install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas') 43 | ) 44 | 45 | compile_schemas = find_program('glib-compile-schemas', required: false) 46 | if compile_schemas.found() 47 | test('Validate schema file', compile_schemas, 48 | args: ['--strict', '--dry-run', meson.current_source_dir()] 49 | ) 50 | endif 51 | 52 | subdir('icons') -------------------------------------------------------------------------------- /src/about-dialog.vala: -------------------------------------------------------------------------------- 1 | /* about-dialog.vala * 2 | * 3 | * Copyright (C) 2018 Nick * 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | namespace Balss { 20 | 21 | public class AppAbout : Gtk.AboutDialog { 22 | 23 | public AppAbout (Gtk.Window window) { 24 | 25 | GLib.Object (transient_for: window, use_header_bar: 1); 26 | this.set_destroy_with_parent (true); 27 | this.set_modal (true); 28 | 29 | logo_icon_name = APP_ID; 30 | 31 | this.program_name = "Balss"; 32 | 33 | this.version = VERSION; 34 | this.comments = _("Audiobook player"); 35 | this.website = "https://github.com/nvlgit/Balss"; 36 | this.website_label = "Balss on gitlab.com"; 37 | this.copyright = "Copyright © 2018 Nick"; 38 | 39 | this.artists = null; 40 | this.authors = {"Nick", ""}; 41 | this.documenters = null; 42 | this.translator_credits = null; 43 | 44 | this.license = "GNU Public Licence version 3"; 45 | this.wrap_license = true; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | balss_sources = [ 2 | 'main.vala', 3 | 'app.vala', 4 | 'window.vala', 5 | 'about-dialog.vala', 6 | 'app-prefs.vala', 7 | 'chapter-row.vala', 8 | 'chapter-list-box.vala', 9 | 'chapter-indicator.vala', 10 | 'control-button.vala', 11 | 'mpv-wrapper-backend.vala', 12 | 'prefs-page.vala', 13 | ] 14 | 15 | c_args = [ 16 | '-DVERSION="' + meson.project_version() + '"', 17 | '-DAPP_ID="' + app_id + '"', 18 | '-DGETTEXT_PACKAGE="' + app_id + '"', 19 | '-DLOCALEDIR="' + join_paths(prefix, localedir) + '"' 20 | ] 21 | 22 | add_project_arguments(['--vapidir', join_paths(meson.current_source_dir(), 'vapi')], 23 | language: 'vala') 24 | 25 | balss_deps = [ 26 | dependency('gio-2.0', version: '>= 2.50'), 27 | dependency('gtk+-3.0', version: '>= 3.22'), 28 | dependency('mpv', version: '>= 1.20'), 29 | ] 30 | 31 | cc = meson.get_compiler('c') 32 | m_dep = cc.find_library('m', required : true) 33 | 34 | gnome = import('gnome') 35 | 36 | add_project_arguments(['--gresourcesdir', join_paths(meson.current_source_dir(), 'ui')], 37 | language: 'vala') 38 | 39 | add_project_arguments(['--gresourcesdir', join_paths(meson.source_root(), 'data', 'icons', 'hicolor', 'scalable', 'apps')], 40 | language: 'vala') 41 | 42 | 43 | 44 | balss_sources += gnome.compile_resources('balss-resources', 45 | 'balss.gresource.xml', 46 | source_dir: join_paths(meson.current_source_dir(), 'ui'), 47 | c_name: 'balss' 48 | ) 49 | 50 | executable(app_id, balss_sources, 51 | vala_args: '--target-glib=2.50', 52 | c_args: c_args, 53 | dependencies: [balss_deps, m_dep], 54 | install: true, 55 | ) -------------------------------------------------------------------------------- /data/com.gitlab.nvlgit.Balss.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | false 7 | Dark theme 8 | 9 | Force use dark theme 10 | 11 | 12 | 13 | 14 | true 15 | Resume playback on startup 16 | 17 | Resume playback the last played URI 18 | 19 | 20 | 21 | 22 | 'none' 23 | Last URI 24 | 25 | 26 | 27 | 0 28 | Last position 29 | 30 | 31 | 32 | 33 | 1.0 34 | Playback rate 35 | 36 | Default playback rate 37 | 38 | 39 | 40 | 41 | false 42 | Show desktop notifications 43 | 44 | Notify about a new chapter 45 | 46 | 47 | 48 | 49 | 540 50 | Window width 51 | 52 | 53 | 54 | 500 55 | Window height 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/ui/chapter-list-box.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 51 | 52 | -------------------------------------------------------------------------------- /data/icons/hicolor/16x16/apps/com.gitlab.nvlgit.Balss.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /data/icons/hicolor/24x24/apps/com.gitlab.nvlgit.Balss.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/chapter-row.vala: -------------------------------------------------------------------------------- 1 | /* chapter-row.vala 2 | * 3 | * Copyright 2018 Nick 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | namespace Balss { 20 | 21 | [GtkTemplate (ui = "/com/gitlab/nvlgit/Balss/chapter-row.ui")] 22 | public class ChapterRow : Gtk.ListBoxRow { 23 | [GtkChild] public Gtk.Label number_label; 24 | [GtkChild] private Gtk.Label time_label; 25 | [GtkChild] private Gtk.Label title_label; 26 | [GtkChild] private Gtk.Label duration_label; 27 | [GtkChild] public Gtk.Scale seekbar; 28 | [GtkChild] public Gtk.Revealer revealer; 29 | [GtkChild] public Gtk.Box box; 30 | public signal void seek_changed (double p); 31 | 32 | 33 | [GtkCallback] 34 | private void seekbar_value_changed_cb () { 35 | 36 | 37 | seek_changed (seekbar.get_value () ); //emit signal 38 | } 39 | 40 | 41 | public ChapterRow () { 42 | } 43 | 44 | 45 | public void set_range (double start, double end) { 46 | 47 | this.seekbar.value_changed.disconnect (seekbar_value_changed_cb); 48 | this.seekbar.set_range (start, end); 49 | this.seekbar.value_changed.connect (seekbar_value_changed_cb); 50 | } 51 | 52 | 53 | public void set_position (double p) { 54 | 55 | this.seekbar.value_changed.disconnect (seekbar_value_changed_cb); 56 | this.seekbar.set_value (p); 57 | this.seekbar.value_changed.connect (seekbar_value_changed_cb); 58 | } 59 | 60 | 61 | public string number { 62 | 63 | get { return number_label.get_text (); } 64 | set { number_label.set_text (value); } 65 | } 66 | 67 | 68 | public string title { 69 | 70 | get { return title_label.get_text (); } 71 | set { title_label.set_text (value); } 72 | } 73 | 74 | 75 | public string time { 76 | 77 | get { return time_label.get_text (); } 78 | set { time_label.set_text (value); } 79 | } 80 | 81 | 82 | public string duration { 83 | 84 | get { return duration_label.get_text (); } 85 | set { duration_label.set_text (value); } 86 | } 87 | 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /com.gitlab.nvlgit.Balss.yml: -------------------------------------------------------------------------------- 1 | app-id: com.gitlab.nvlgit.Balss 2 | 3 | runtime: org.gnome.Platform 4 | runtime-version: master 5 | 6 | sdk: org.gnome.Sdk 7 | 8 | command: com.gitlab.nvlgit.Balss 9 | 10 | build-options: 11 | cflags: -O2 -g 12 | cxxflags: -O2 -g 13 | 14 | cleanup: 15 | - "/include" 16 | - "/debug" 17 | 18 | finish-args: 19 | - "--device=dri" 20 | - "--socket=x11" 21 | - "--share=ipc" 22 | - "--socket=wayland" 23 | - "--socket=pulseaudio" 24 | - "--filesystem=host:ro" 25 | - "--filesystem=xdg-run/dconf" 26 | - "--filesystem=~/.config/dconf:ro" 27 | - "--env=DCONF_USER_CONFIG_DIR=.config/dconf" 28 | - "--talk-name=ca.desrt.dconf" 29 | 30 | modules: 31 | 32 | - name: ffmpeg 33 | 34 | sources: 35 | - type: archive 36 | url: https://ffmpeg.org/releases/ffmpeg-4.0.2.tar.xz 37 | sha256: "a95c0cc9eb990e94031d2183f2e6e444cc61c99f6f182d1575c433d62afb2f97" 38 | 39 | config-opts: 40 | - "--disable-everything" 41 | - "--enable-shared" 42 | - "--disable-static" 43 | - "--disable-doc" 44 | - "--disable-programs" 45 | - "--enable-decoder=aac" 46 | - "--enable-decoder=alac" 47 | - "--enable-decoder=ac3" 48 | - "--enable-decoder=mp3" 49 | - "--enable-decoder=vorbis" 50 | - "--enable-decoder=opus" 51 | - "--enable-parser=vorbis" 52 | - "--enable-parser=aac" 53 | - "--enable-parser=ac3" 54 | - "--enable-demuxer=aac" 55 | - "--enable-demuxer=matroska" 56 | - "--enable-demuxer=ac3" 57 | - "--enable-demuxer=mp3" 58 | - "--enable-demuxer=mov" 59 | 60 | 61 | cleanup: 62 | 63 | - "/include" 64 | - "/lib/pkgconfig" 65 | - "/share/ffmpeg/examples" 66 | 67 | 68 | - name: libmpv 69 | sources: 70 | 71 | - type: git 72 | url: https://github.com/mpv-player/mpv.git 73 | tag: v0.28.2 74 | 75 | - type: file 76 | url: https://waf.io/waf-2.0.9 77 | sha256: "2a8e0816f023995e557f79ea8940d322bec18f286917c8f9a6fa2dc3875dfa48" 78 | dest-filename: waf 79 | 80 | buildsystem: simple 81 | 82 | build-commands: 83 | 84 | - python3 waf configure --prefix=/app --libdir=/app/lib --enable-libmpv-shared --disable-cplayer --disable-build-date --disable-android --disable-libass --disable-lua --disable-javascript --disable-cplugins --enable-pulse --disable-vulkan --disable-vdpau --disable-x11 --disable-oss-audio --disable-alsa --disable-manpage-build --disable-debug-build --disable-libbluray 85 | - python3 waf 86 | - python3 waf install 87 | 88 | cleanup: 89 | 90 | - "/include" 91 | - "/lib/pkgconfig" 92 | 93 | 94 | - name: Balss 95 | 96 | sources: 97 | - type: git 98 | url: https://gitlab.com/nvlgit/Balss.git 99 | buildsystem: meson 100 | builddir: true 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /src/app-prefs.vala: -------------------------------------------------------------------------------- 1 | /* app-prefs.vala * 2 | * 3 | * Copyright (C) 2018 Nick * 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | namespace Balss { 20 | 21 | 22 | public class Prefs : GLib.Object{ 23 | 24 | private GLib.Settings settings; 25 | 26 | private bool _dark_theme; 27 | private bool _play_last; 28 | private string _last_uri; 29 | private double _last_position; 30 | private double _playback_rate; 31 | private bool _show_notifications; 32 | private int _window_width; 33 | private int _window_height; 34 | 35 | 36 | public Prefs () { 37 | 38 | settings = new GLib.Settings ("com.gitlab.nvlgit.Balss"); 39 | 40 | _dark_theme = settings.get_boolean ("dark-theme"); 41 | _play_last = settings.get_boolean ("play-last"); 42 | _last_uri = settings.get_string ("last-uri"); 43 | _last_position = settings.get_double ("last-position"); 44 | _playback_rate = settings.get_double ("playback-rate"); 45 | _show_notifications = settings.get_boolean ("show-notifications"); 46 | _window_width = settings.get_int ("window-width"); 47 | _window_height = settings.get_int ("window-height"); 48 | 49 | } 50 | 51 | public bool dark_theme { 52 | 53 | get { return _dark_theme; } 54 | set { 55 | _dark_theme = value; 56 | settings.set_boolean ("dark-theme", value); 57 | } 58 | } 59 | 60 | public bool play_last { 61 | 62 | get { return _play_last; } 63 | set { 64 | _play_last = value; 65 | settings.set_boolean ("play-last", value); 66 | } 67 | } 68 | 69 | public string last_uri { 70 | 71 | get { return _last_uri; } 72 | set { 73 | _last_uri = value; 74 | settings.set_string ("last-uri", value); 75 | } 76 | } 77 | 78 | public double last_position { 79 | 80 | get { return _last_position; } 81 | set { 82 | _last_position = value; 83 | settings.set_double ("last-position", value); 84 | } 85 | } 86 | 87 | public double playback_rate { 88 | 89 | get { return _playback_rate; } 90 | set { 91 | _playback_rate = value; 92 | settings.set_double ("playback-rate", value); 93 | } 94 | } 95 | 96 | public bool show_notifications { 97 | 98 | get { return _show_notifications; } 99 | set { 100 | _show_notifications = value; 101 | settings.set_boolean ("show-notifications", value); 102 | } 103 | } 104 | 105 | public int window_width { 106 | 107 | get { return _window_width; } 108 | set { 109 | _window_width = value; 110 | settings.set_int ("window-width", value); 111 | } 112 | } 113 | 114 | public int window_height { 115 | 116 | get { return _window_height; } 117 | set { 118 | _window_height = value; 119 | settings.set_int ("window-height", value); 120 | } 121 | } 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /data/icons/hicolor/32x32/apps/com.gitlab.nvlgit.Balss.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/chapter-list-box.vala: -------------------------------------------------------------------------------- 1 | /* chapter-list-box.vala 2 | * 3 | * Copyright 2018 Nick 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | namespace Balss { 20 | 21 | [GtkTemplate (ui = "/com/gitlab/nvlgit/Balss/chapter-list-box.ui")] 22 | public class ChapterListBox : Gtk.Box { 23 | 24 | [GtkChild] private Gtk.ScrolledWindow scrolled_window; 25 | [GtkChild] private Gtk.ListBox list; 26 | private Gtk.SizeGroup sizegroup; 27 | private int last_index; 28 | public signal void seek_changed (double pos); 29 | 30 | 31 | public signal void chapter_row_activated (int index); 32 | 33 | construct { 34 | sizegroup = new Gtk.SizeGroup (Gtk.SizeGroupMode.HORIZONTAL); 35 | last_index = 0; 36 | //this.list.set_focus_child.connect (set_focus_child_cb); 37 | 38 | } 39 | 40 | public ChapterListBox () {} 41 | 42 | 43 | public void set_range (double offset, double end) { 44 | 45 | Gtk.ListBoxRow row = this.list.get_row_at_index (last_index); 46 | ( (Balss.ChapterRow) row).set_range (offset, end); 47 | } 48 | 49 | public void set_time (string time) { 50 | 51 | Gtk.ListBoxRow row = this.list.get_row_at_index (last_index); 52 | ( (Balss.ChapterRow) row).time = time + " / "; 53 | } 54 | 55 | public void set_position (double p) { 56 | 57 | Gtk.ListBoxRow row = this.list.get_row_at_index (last_index); 58 | ( (Balss.ChapterRow) row).set_position (p); 59 | } 60 | 61 | private void add_separators () { 62 | 63 | this.list.set_header_func (update_header_fn); 64 | 65 | } 66 | private void update_header_fn (Gtk.ListBoxRow row, Gtk.ListBoxRow? before){ 67 | 68 | Gtk.Widget? current; 69 | 70 | if (before == null) { 71 | row.set_header (null); 72 | return; 73 | } 74 | 75 | current = row.get_header (); 76 | if (current == null) { 77 | current = new Gtk.Separator (Gtk.Orientation.HORIZONTAL); 78 | current.show (); 79 | row.set_header (current); 80 | } 81 | 82 | } 83 | 84 | public void add_row (Balss.ChapterRow row){ 85 | 86 | this.list.add (row); 87 | add_separators (); 88 | sizegroup.add_widget (row.number_label); 89 | row.seek_changed.connect ( (p) => { 90 | seek_changed (p); // Emit signal 91 | }); 92 | } 93 | 94 | [GtkCallback] 95 | private void list_box_row_activated_cb (Gtk.ListBoxRow row) { 96 | 97 | int i = row.get_index (); 98 | chapter_row_activated (i); //emit signal 99 | } 100 | 101 | public void mark_row_at_index (int i) { 102 | 103 | Gtk.ListBoxRow last_row = this.list.get_row_at_index (last_index); 104 | ( (Balss.ChapterRow) last_row).time = ""; 105 | ( (Balss.ChapterRow) last_row).revealer.set_reveal_child (false); 106 | Gtk.StyleContext last_ctx = ( (Balss.ChapterRow) last_row).get_style_context (); 107 | last_ctx.remove_class ("balss-active-chapter-row"); 108 | 109 | Gtk.ListBoxRow row = this.list.get_row_at_index (i); 110 | 111 | ( (Balss.ChapterRow) row).revealer.set_reveal_child (true); 112 | Gtk.StyleContext ctx = ( (Balss.ChapterRow) row).get_style_context (); 113 | ctx.add_class ("balss-active-chapter-row"); 114 | this.last_index = i; 115 | GLib.Timeout.add (150, () => { // delay while revealer revealed 116 | row.grab_focus (); 117 | return false; 118 | }); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/chapter-indicator.vala: -------------------------------------------------------------------------------- 1 | /* chapter-indicator.vala 2 | * 3 | * Copyright 2018 Nick 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | 20 | namespace Balss { 21 | 22 | [GtkTemplate (ui = "/com/gitlab/nvlgit/Balss/chapter-indicator.ui")] 23 | public class ChapterIndicator : Gtk.Box { 24 | 25 | [GtkChild] private Gtk.DrawingArea area; 26 | [GtkChild] private Gtk.EventBox event_box; 27 | [GtkChild] private Gtk.Frame frame; 28 | [GtkChild] private Gtk.Popover indicator_popover; 29 | [GtkChild] private Gtk.Label label; 30 | [GtkChild] private Gtk.Grid tooltip_grid; 31 | [GtkChild] private Gtk.Label tooltip_persent_label; 32 | [GtkChild] private Gtk.Label tooltip_time_label; 33 | [GtkChild] private Gtk.Button button_previous; 34 | [GtkChild] private Gtk.Button button_next; 35 | private double _value; 36 | public signal void button_clicked (int direction); 37 | 38 | construct { 39 | 40 | this._value = 0; 41 | this.area.draw.connect (draw_cb); 42 | this.event_box.events |= Gdk.EventMask.BUTTON_PRESS_MASK; 43 | this.event_box.button_press_event.connect ((event) => { 44 | debug ("button_press_event.connect"); 45 | indicator_popover.popup (); 46 | return true; 47 | }); 48 | this.frame.has_tooltip = true; 49 | this.frame.query_tooltip.connect ((x, y, keyboard_tooltip, tooltip) => { 50 | tooltip.set_custom (tooltip_grid); 51 | return true; 52 | }); 53 | } 54 | 55 | public ChapterIndicator () {} 56 | 57 | public void clear () { 58 | 59 | this.button_next.sensitive = false; 60 | this.button_previous.sensitive = false; 61 | this.label.label = ""; 62 | set_fraction (0); 63 | } 64 | 65 | public void set_info (int chapter, int count) { 66 | 67 | this.label.label = "%d / %d".printf (chapter, count); 68 | this.button_previous.sensitive = (chapter == 1) ? false : true; 69 | this.button_next.sensitive = (chapter == count) ? false : true; 70 | } 71 | 72 | public void set_tooltip (int persent, string pos, string dur) { 73 | 74 | tooltip_persent_label.label = "%d %%".printf (persent); 75 | tooltip_time_label.label = "%s / %s".printf (pos, dur); 76 | } 77 | 78 | public void set_fraction (double val) { 79 | 80 | if (val > 1) 81 | val = 1; 82 | this._value = val; 83 | 84 | this.area.queue_draw (); 85 | } 86 | 87 | private bool draw_cb (Gtk.Widget da, Cairo.Context cr) { 88 | 89 | uint w; 90 | uint h; 91 | Gdk.RGBA color; 92 | Gdk.RGBA selected_color; 93 | Gtk.StyleContext style_context; 94 | 95 | style_context = area.get_style_context (); 96 | color = style_context.get_color ( area.get_state_flags () ); 97 | bool result = style_context.lookup_color ("theme_selected_bg_color", 98 | out selected_color); 99 | if (!result) 100 | return false; 101 | 102 | double sum = color.red + color.green + color.blue; 103 | selected_color.alpha *= (sum < 1) ? 0.2 : 0.5; 104 | 105 | if (_value > 0) { 106 | _value = double.max (0.01, _value); 107 | } 108 | 109 | if (_value > 0.99 && _value < 1.0 ) { 110 | _value = 0.99; 111 | } 112 | 113 | w = area.get_allocated_width (); 114 | h = area.get_allocated_height (); 115 | 116 | Gdk.cairo_set_source_rgba (cr, selected_color); 117 | cr.rectangle(0, 0, w * _value, h); 118 | cr.fill (); 119 | 120 | return true; 121 | } 122 | [GtkCallback] private void button_next_clicked_cb () { 123 | button_clicked (1); 124 | } 125 | [GtkCallback] private void button_previous_clicked_cb () { 126 | button_clicked (-1); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/prefs-page.vala: -------------------------------------------------------------------------------- 1 | /* prefs-page.vala 2 | * 3 | * Copyright (C) 2018 Nick 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | 20 | 21 | namespace Balss { 22 | 23 | [GtkTemplate (ui = "/com/gitlab/nvlgit/Balss/prefs-page.ui")] 24 | public class PrefsPage : Gtk.Box { 25 | 26 | 27 | [GtkChild] private Gtk.Switch play_last_switch; 28 | [GtkChild] private Gtk.SpinButton rate_spinbutton; 29 | [GtkChild] private Gtk.Switch dark_theme_switch; 30 | [GtkChild] private Gtk.Switch mpris_switch; 31 | [GtkChild] private Gtk.Switch notification_switch; 32 | [GtkChild] private Gtk.ListBox pb_listbox; 33 | [GtkChild] private Gtk.ListBoxRow play_last_row; 34 | [GtkChild] private Gtk.ListBoxRow rate_row; 35 | [GtkChild] private Gtk.ListBox appearence_listbox; 36 | [GtkChild] private Gtk.ListBoxRow dark_theme_row; 37 | [GtkChild] private Gtk.ListBox features_listbox; 38 | [GtkChild] private Gtk.ListBoxRow mpris_row; 39 | [GtkChild] private Gtk.ListBoxRow notify_row; 40 | 41 | 42 | 43 | construct { 44 | 45 | add_separators (this.pb_listbox); 46 | add_separators (this.features_listbox); 47 | connect_prefs_signals (); 48 | connect_rows_signals (); 49 | } 50 | 51 | 52 | 53 | public PrefsPage () { 54 | 55 | dark_theme_switch.active = App.preferences.dark_theme; 56 | notification_switch.active = App.preferences.show_notifications; 57 | play_last_switch.active = App.preferences.play_last; 58 | rate_spinbutton.value = App.preferences.playback_rate; 59 | } 60 | 61 | 62 | 63 | public void update_gtk_theme () { 64 | 65 | var gtk_settings = Gtk.Settings.get_default (); 66 | gtk_settings.gtk_application_prefer_dark_theme = App.preferences.dark_theme; 67 | } 68 | 69 | 70 | 71 | private void add_separators (Gtk.ListBox lbox) { 72 | 73 | lbox.set_header_func (update_header_fn); 74 | } 75 | 76 | 77 | 78 | private void update_header_fn (Gtk.ListBoxRow row, Gtk.ListBoxRow? before){ 79 | 80 | Gtk.Widget? current; 81 | 82 | if (before == null) { 83 | row.set_header (null); 84 | return; 85 | } 86 | 87 | current = row.get_header (); 88 | if (current == null) { 89 | current = new Gtk.Separator (Gtk.Orientation.HORIZONTAL); 90 | current.show (); 91 | row.set_header (current); 92 | } 93 | } 94 | 95 | 96 | 97 | private void connect_prefs_signals () { 98 | 99 | App.preferences.notify["dark-theme"].connect (update_gtk_theme); 100 | 101 | dark_theme_switch.state_set.connect( () => { 102 | App.preferences.dark_theme = dark_theme_switch.active; 103 | return false; 104 | }); 105 | 106 | play_last_switch.state_set.connect( () => { 107 | App.preferences.play_last = play_last_switch.active; 108 | return false; 109 | }); 110 | 111 | notification_switch.state_set.connect( () => { 112 | App.preferences.show_notifications = notification_switch.active; 113 | return false; 114 | }); 115 | 116 | rate_spinbutton.value_changed.connect( () => { 117 | App.preferences.playback_rate = rate_spinbutton.value; 118 | }); 119 | } 120 | 121 | 122 | private void connect_rows_signals () { 123 | 124 | this.pb_listbox.row_activated.connect ( (row) => { 125 | 126 | if (row == play_last_row) 127 | play_last_switch.active = !play_last_switch.active; 128 | if (row == rate_row) 129 | rate_spinbutton.grab_focus (); 130 | }); 131 | 132 | this.appearence_listbox.row_activated.connect ( (row) => { 133 | 134 | if (row == dark_theme_row) 135 | dark_theme_switch.active = !dark_theme_switch.active; 136 | }); 137 | 138 | this.features_listbox.row_activated.connect ( (row) => { 139 | 140 | if (row == mpris_row) 141 | mpris_switch.active = !mpris_switch.active; 142 | if (row == notify_row) 143 | notification_switch.active = !notification_switch.active; 144 | }); 145 | } 146 | 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/app.vala: -------------------------------------------------------------------------------- 1 | /* app.vala 2 | * 3 | * Copyright (C) 2018 Nick 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | 20 | namespace Balss { 21 | 22 | const string APP_ID = "com.gitlab.nvlgit.Balss"; 23 | 24 | 25 | public class App : Gtk.Application { 26 | 27 | 28 | private PlayerWindow win; 29 | public static Prefs preferences; 30 | 31 | private const ActionEntry[] actions = { 32 | 33 | { "open", open_cb }, 34 | { "prefs", prefs_cb }, 35 | { "shortcuts", shortcuts_cb }, 36 | { "about", about_cb }, 37 | { "quit", quit_cb } 38 | }; 39 | 40 | 41 | 42 | public App () { 43 | 44 | application_id = APP_ID; 45 | flags |= GLib.ApplicationFlags.HANDLES_OPEN; 46 | } 47 | 48 | 49 | 50 | private void open_cb (SimpleAction action, Variant? parameter) { 51 | 52 | win = (PlayerWindow) this.active_window; 53 | win.show_page ("main"); 54 | 55 | var chooser = new Gtk.FileChooserDialog ("Select file", 56 | win as PlayerWindow, 57 | Gtk.FileChooserAction.OPEN, 58 | _("_Cancel"), 59 | Gtk.ResponseType.CANCEL, 60 | _("_Open"), 61 | Gtk.ResponseType.ACCEPT, 62 | null); 63 | var filter = new Gtk.FileFilter (); 64 | filter.set_filter_name (_("m4b books") ); 65 | filter.add_pattern ("*.m4b"); 66 | filter.add_pattern ("*.M4B"); 67 | filter.add_pattern ("*.M4b"); 68 | filter.add_pattern ("*.m4B"); 69 | chooser.add_filter (filter); 70 | 71 | filter = new Gtk.FileFilter (); 72 | filter.set_filter_name (_("All audio files") ); 73 | filter.add_mime_type ("audio/*"); 74 | chooser.add_filter (filter); 75 | 76 | filter = new Gtk.FileFilter (); 77 | filter.set_filter_name (_("All files") ); 78 | filter.add_pattern ("*"); 79 | chooser.add_filter (filter); 80 | 81 | chooser.local_only = true; 82 | chooser.select_multiple = false; 83 | chooser.set_modal (true); 84 | 85 | chooser.response.connect ( (fc, r) => { 86 | 87 | var c = fc as Gtk.FileChooserDialog; 88 | if (r == Gtk.ResponseType.ACCEPT) 89 | win.open (c.get_uri () ); 90 | chooser.destroy (); 91 | }); 92 | 93 | chooser.show (); 94 | } 95 | 96 | 97 | 98 | private void prefs_cb (SimpleAction action, Variant? parameter) { 99 | 100 | win = (PlayerWindow) this.active_window; 101 | win.show_page ("prefs"); 102 | } 103 | 104 | 105 | 106 | private void shortcuts_cb (SimpleAction action, Variant? parameter) { 107 | 108 | var builder = new Gtk.Builder.from_resource ( 109 | "/com/gitlab/nvlgit/Balss/shortcuts-window.ui"); 110 | var shortcuts_window = (Gtk.Window) builder.get_object ("shortcuts-window"); 111 | shortcuts_window.show (); 112 | } 113 | 114 | 115 | 116 | void about_cb (SimpleAction action, Variant? parameter) { 117 | 118 | AppAbout about = new AppAbout (get_active_window () ); 119 | about.present (); 120 | } 121 | 122 | 123 | 124 | void quit_cb (SimpleAction action, Variant? parameter) { 125 | 126 | this.win.on_close_window (); 127 | this.quit (); 128 | } 129 | 130 | 131 | 132 | private void notify_desktop (string title, string body) { 133 | 134 | var n = new GLib.Notification (title); 135 | n.set_body (body); 136 | ( (GLib.Application) this).send_notification (null, n); 137 | } 138 | 139 | 140 | 141 | public override void startup () { 142 | 143 | base.startup (); 144 | preferences = new Prefs (); 145 | 146 | var css_provider = new Gtk.CssProvider (); 147 | css_provider.load_from_resource ("/com/gitlab/nvlgit/Balss/style.css"); 148 | Gtk.StyleContext.add_provider_for_screen (Gdk.Screen.get_default (), 149 | css_provider, 150 | Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); 151 | 152 | Gtk.IconTheme icon_theme = Gtk.IconTheme.get_default (); 153 | icon_theme.add_resource_path("/com/gitlab/nvlgit/Balss/icons"); 154 | 155 | this.add_action_entries (actions, this); 156 | } 157 | 158 | 159 | 160 | protected override void activate () { 161 | 162 | //base.activate (); 163 | win = (PlayerWindow) this.active_window; 164 | if (win == null) { 165 | win = new PlayerWindow (this); 166 | win.new_notification.connect (notify_desktop); 167 | } 168 | win.present (); 169 | } 170 | 171 | 172 | 173 | public override void open (GLib.File[] files, 174 | string hint) { 175 | 176 | win = (PlayerWindow) this.active_window; 177 | if (win == null) 178 | win = new PlayerWindow (this); 179 | 180 | string uri = files[0].get_uri (); 181 | win.open (uri); 182 | win.present (); 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/ui/chapter-row.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 127 | 128 | -------------------------------------------------------------------------------- /data/icons/hicolor/64x64/apps/com.gitlab.nvlgit.Balss.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /data/icons/hicolor/128x128/apps/com.gitlab.nvlgit.Balss.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /data/icons/hicolor/scalable/apps/com.gitlab.nvlgit.Balss.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/control-button.vala: -------------------------------------------------------------------------------- 1 | /* control-button.vala 2 | * 3 | * Copyright 2018 Nick 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | 20 | namespace Balss { 21 | 22 | [GtkTemplate (ui = "/com/gitlab/nvlgit/Balss/control-button.ui")] 23 | public class ControlMenuButton : Gtk.MenuButton { 24 | 25 | [GtkChild] private Gtk.Scale volume_scale; 26 | [GtkChild] private Gtk.SpinButton rate_spinbutton; 27 | [GtkChild] private Gtk.DrawingArea rate_area; 28 | [GtkChild] private Gtk.Image volume_button_image; 29 | [GtkChild] private Gtk.Grid tooltip_grid; 30 | [GtkChild] private Gtk.Label tooltip_rate_label; 31 | [GtkChild] private Gtk.Label tooltip_volume_label; 32 | 33 | public signal void volume_changed (double val); 34 | public signal void rate_changed (double val); 35 | 36 | private double _rate; 37 | private double _volume; 38 | 39 | const string[] ICONS = { "audio-volume-muted-symbolic", 40 | "audio-volume-low-symbolic", 41 | "audio-volume-medium-symbolic", 42 | "audio-volume-high-symbolic" }; 43 | [GtkCallback] 44 | private void volume_scale_value_changed_cb (Gtk.Range range) { 45 | 46 | volume_changed (range.get_value () ); //Emit signal 47 | } 48 | 49 | [GtkCallback] 50 | private void rate_spinbutton_value_changed_cb () { 51 | 52 | rate_changed (rate_spinbutton.value); //Emit signal 53 | } 54 | 55 | 56 | construct { 57 | 58 | _rate = 0.5; 59 | _volume = 0.5; 60 | this.rate_area.draw.connect (draw_rate_cb); 61 | //this.set_tooltip_window (tooltip_window); 62 | this.has_tooltip = true; 63 | this.query_tooltip.connect ((x, y, keyboard_tooltip, tooltip) => { 64 | tooltip.set_custom (tooltip_grid); 65 | return true; 66 | }); 67 | } 68 | 69 | 70 | 71 | public ControlMenuButton () {} 72 | 73 | 74 | 75 | public double rate { 76 | 77 | get { return this._rate; } 78 | set { 79 | this._rate = value; 80 | rate_spinbutton.value_changed.disconnect (rate_spinbutton_value_changed_cb); 81 | rate_spinbutton.set_value (rate); 82 | rate_spinbutton.value_changed.connect (rate_spinbutton_value_changed_cb); 83 | GLib.Idle.add ( () => { 84 | this.rate_area.queue_draw (); 85 | set_tooltip (); 86 | return false; 87 | }); 88 | 89 | } 90 | } 91 | 92 | 93 | 94 | public double volume { 95 | 96 | get { return this._volume; } 97 | set { 98 | this._volume = value; 99 | double cur_val = this.volume_scale.get_value (); 100 | 101 | if (GLib.Math.fabs (cur_val - value) > 0.001) { 102 | 103 | volume_scale.value_changed.disconnect (volume_scale_value_changed_cb); 104 | volume_scale.set_value (value); 105 | volume_scale.value_changed.connect (volume_scale_value_changed_cb); 106 | } 107 | GLib.Idle.add ( () => { 108 | if (this._volume < 0.05) 109 | volume_button_image.icon_name = ICONS[0]; 110 | else if (this._volume < 0.45) 111 | volume_button_image.icon_name = ICONS[1]; 112 | else if (this._volume < 0.95) 113 | volume_button_image.icon_name = ICONS[2]; 114 | else 115 | volume_button_image.icon_name = ICONS[3]; 116 | set_tooltip (); 117 | return false; 118 | }); 119 | } 120 | } 121 | 122 | 123 | 124 | private void set_tooltip () { 125 | 126 | string v = "%d %%".printf ( 127 | (int) GLib.Math.round (100 * this._volume) ); 128 | string r = "%g ×".printf (this._rate); 129 | this.tooltip_volume_label.label = v; 130 | this.tooltip_rate_label.label = r; 131 | } 132 | 133 | 134 | 135 | private bool draw_rate_cb (Cairo.Context cr) { 136 | 137 | uint w; 138 | uint h; 139 | Gdk.RGBA color; 140 | Gdk.RGBA dimmed_color; 141 | Gtk.StyleContext style_context; 142 | 143 | style_context = rate_area.get_style_context (); 144 | color = style_context.get_color ( rate_area.get_state_flags () ); 145 | dimmed_color = color; 146 | dimmed_color.alpha *= 0.5; 147 | 148 | if (_rate < 0.5) { 149 | _rate = 0.5; 150 | } 151 | 152 | if (_rate > 1.5) { 153 | _rate = 1.5; 154 | } 155 | 156 | w = rate_area.get_allocated_width (); 157 | h = rate_area.get_allocated_height (); 158 | // coordinates for the center 159 | double px = uint.min (w, h) / 16.0; 160 | double xc = w / 2.0; 161 | double yc = 11 * px; 162 | 163 | double angle_start = 0.95 * GLib.Math.PI; 164 | double angle_end = 2.05 * GLib.Math.PI; 165 | double angle_arrow = (1 + (_rate * 0.5) ) * GLib.Math.PI; 166 | 167 | cr.set_line_width (2 * px); 168 | cr.set_line_cap (Cairo.LineCap.SQUARE); 169 | 170 | Gdk.cairo_set_source_rgba (cr, color); 171 | 172 | cr.arc (xc, yc, 7 * px, angle_start, angle_arrow - 0.65); 173 | cr.stroke (); 174 | int r = (int) GLib.Math.round (_rate * 100); 175 | Gdk.cairo_set_source_rgba (cr, 176 | (r == 100) ? color : dimmed_color); 177 | cr.arc (xc, yc, 7 * px, angle_arrow + 0.65 , angle_end); 178 | cr.stroke (); 179 | 180 | // speedometer arrow 181 | cr.arc (xc, yc, 2 * px, 0, 360); 182 | cr.set_line_width (0); 183 | Gdk.cairo_set_source_rgba (cr, color); 184 | cr.fill (); 185 | // triangle 186 | double x1 = xc + (11 * px * GLib.Math.cos (angle_arrow) ); 187 | double y1 = yc + (11 * px * GLib.Math.sin (angle_arrow) ); 188 | double x2 = xc + ( 2 * px * GLib.Math.cos (angle_arrow - (GLib.Math.PI / 2.0) ) ); 189 | double y2 = yc + ( 2 * px * GLib.Math.sin (angle_arrow - (GLib.Math.PI / 2.0) ) ); 190 | double x3 = xc + ( 2 * px * GLib.Math.cos (angle_arrow + (GLib.Math.PI / 2.0) ) ); 191 | double y3 = yc + ( 2 * px * GLib.Math.sin (angle_arrow + (GLib.Math.PI / 2.0) ) ); 192 | cr.move_to (x1, y1); 193 | cr.line_to (x2, y2); 194 | cr.line_to (x3, y3); 195 | cr.close_path (); 196 | cr.fill (); 197 | 198 | return true; 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/ui/shortcuts-window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | 7 | 8 | true 9 | Keyboard Shortcuts 10 | Keyboard Shortcuts 11 | 12 | 13 | true 14 | General 15 | 16 | 17 | true 18 | <ctrl>o 19 | Open an audiobook 20 | 21 | 22 | 23 | 24 | true 25 | Preferences 26 | <ctrl>comma 27 | 28 | 29 | 30 | 31 | true 32 | Keyboard Shortcuts 33 | <ctrl><shift>question 34 | 35 | 36 | 37 | 38 | true 39 | <ctrl>q 40 | Quit 41 | 42 | 43 | 44 | 45 | true 46 | F9 47 | Popup controls 48 | 49 | 50 | 51 | 52 | true 53 | F10 54 | Popup main menu 55 | 56 | 57 | 58 | 59 | 60 | 61 | true 62 | Volume 63 | 64 | 65 | true 66 | 9 67 | Decrease volume 68 | 69 | 70 | 71 | 72 | true 73 | 0 74 | Increase volume 75 | 76 | 77 | 78 | 79 | 80 | 81 | true 82 | Playback 83 | 84 | 85 | true 86 | P space 87 | Play / Pause 88 | 89 | 90 | 91 | 92 | true 93 | <ctrl>Up 94 | Play previous chapter 95 | 96 | 97 | 98 | 99 | true 100 | <ctrl>Down 101 | Play next chapter 102 | 103 | 104 | 105 | 106 | true 107 | <ctrl>Right 108 | Seek forward 109 | 110 | 111 | 112 | 113 | true 114 | <ctrl>Left 115 | Seek backward 116 | 117 | 118 | 119 | 120 | 121 | 122 | true 123 | Playback 124 | 125 | 126 | true 127 | <ctrl><shift>braceleft 128 | Decrease rate (-0.05) 129 | 130 | 131 | 132 | 133 | true 134 | <ctrl><shift>braceright 135 | Increase rate (+0.05) 136 | 137 | 138 | 139 | 140 | true 141 | <ctrl><shift>BackSpace 142 | Reset rate to 1× 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /src/vapi/mpv.vapi: -------------------------------------------------------------------------------- 1 | [CCode (cheader_filename = "mpv/client.h")] 2 | namespace Mpv { 3 | 4 | [CCode (cname = "enum mpv_error", cprefix = "MPV_ERROR_")] 5 | public enum Error { 6 | 7 | SUCCESS, // No error happened 8 | EVENT_QUEUE_FULL, // The event ringbuffer is full. This means the client is choked, and can't receive any events 9 | NOMEM, // Memory allocation failed 10 | UNINITIALIZED, // The mpv core wasn't configured and initialized yet. 11 | INVALID_PARAMETER, // Generic catch-all error if a parameter is set to an invalid or unsupported value. 12 | OPTION_NOT_FOUND, // Trying to set an option that doesn't exist. 13 | OPTION_FORMAT, // Trying to set an option using an unsupported MPV_FORMAT. 14 | OPTION_ERROR, // Setting the option failed. Typically this happens if the provided option value could not be parsed. 15 | PROPERTY_NOT_FOUND, // The accessed property doesn't exist. 16 | PROPERTY_FORMAT, // Trying to set or get a property using an unsupported MPV_FORMAT. 17 | PROPERTY_UNAVAILABLE, // The property exists, but is not available. 18 | PROPERTY_ERROR, // Error setting or getting a property. 19 | COMMAND, // General error when running a command with mpv_command and similar. 20 | LOADING_FAILED, // Generic error on loading (usually used with mpv_event_end_file.error) 21 | AO_INIT_FAILED, // Initializing the audio output failed 22 | VO_INIT_FAILED, // Initializing the video output failed 23 | NOTHING_TO_PLAY, // There was no audio or video data to play 24 | UNKNOWN_FORMAT, // When trying to load the file, the file format could not be determined, or the file was too broken to open it. 25 | UNSUPPORTED, // Generic error for signaling that certain system requirements are not fulfilled. 26 | NOT_IMPLEMENTED, // The API function which was called is a stub only 27 | GENERIC // Unspecified error 28 | } 29 | 30 | [CCode (cname = "enum mpv_format", has_type_id = false, cprefix = "MPV_FORMAT_")] 31 | public enum Format { 32 | 33 | NONE, 34 | STRING, 35 | OSD_STRING, 36 | FLAG, 37 | INT64, 38 | DOUBLE, 39 | NODE, 40 | NODE_ARRAY, 41 | NODE_MAP, 42 | BYTE_ARRAY 43 | } 44 | 45 | [CCode (cname = "enum mpv_event_id", has_type_id = false, cprefix = "MPV_EVENT_")] 46 | public enum EventID { 47 | 48 | NONE, 49 | SHUTDOWN, 50 | LOG_MESSAGE, 51 | GET_PROPERTY_REPLY, 52 | SET_PROPERTY_REPLY, 53 | COMMAND_REPLY, 54 | START_FILE, 55 | END_FILE, 56 | FILE_LOADED, 57 | IDLE, 58 | TICK, 59 | CLIENT_MESSAGE, 60 | VIDEO_RECONFIG, 61 | AUDIO_RECONFIG, 62 | SEEK, 63 | PLAYBACK_RESTART, 64 | PROPERTY_CHANGE, 65 | QUEUE_OVERFLOW, 66 | HOOK, 67 | //depricated 68 | TRACKS_CHANGED, 69 | TRACK_SWITCHED, 70 | PAUSE, 71 | UNPAUSE, 72 | SCRIPT_INPUT_DISPATCH, 73 | METADATA_UPDATE, 74 | CHAPTER_CHANGE, 75 | } 76 | 77 | [CCode (cname = "enum mpv_end_file_reason", has_type_id = false, cprefix = "MPV_END_FILE_REASON_")] 78 | public enum EndFileReason { 79 | 80 | EOF, 81 | STOP, 82 | QUIT, 83 | ERROR, 84 | REDIRECT, 85 | } 86 | 87 | [CCode (cname = "enum mpv_log_level", has_type_id = false, cprefix = "MPV_LOG_LEVEL_")] 88 | public enum LogLevel { 89 | 90 | NONE, /// "no" - disable absolutely all messages 91 | FATAL, /// "fatal" - critical/aborting errors 92 | ERROR, /// "error" - simple errors 93 | WARN, /// "warn" - possible problems 94 | INFO, /// "info" - informational message 95 | V , /// "v" - noisy informational message 96 | DEBUG, /// "debug" - very noisy technical information 97 | TRACE /// "trace" - extremely noisy 98 | } 99 | 100 | [Compact] 101 | [CCode (cname = "struct mpv_event_property", unref_function ="", free_function ="", copy_function = "")] 102 | public class EventProperty { 103 | 104 | public string name; 105 | public Mpv.Format format; 106 | public unowned T data; 107 | } 108 | 109 | [CCode (cname = "struct mpv_event_log_message")] 110 | public struct EventLogMessage { 111 | 112 | public string prefix; 113 | public string level; 114 | public string text; 115 | public Mpv.LogLevel log_level; 116 | } 117 | 118 | [Compact] 119 | [CCode (cname = "struct mpv_event", unref_function ="", free_function ="", copy_function = "")] 120 | public class Event { 121 | 122 | public Mpv.EventID event_id; 123 | public int error; 124 | public uint64 reply_userdata; 125 | public unowned T data; 126 | } 127 | 128 | [CCode (cname = "struct mpv_event_end_file")] 129 | public struct EventEndFile { 130 | 131 | public Mpv.EndFileReason reason; 132 | public Mpv.Error error; 133 | } 134 | 135 | [CCode (cname = "struct mpv_event_client_message")] 136 | public struct EventClientMessage { 137 | 138 | public int num_args; 139 | public string[] args; 140 | } 141 | 142 | [CCode (cname = "struct mpv_event_hook")] 143 | public struct EventHook { 144 | 145 | public string name; 146 | public uint64 id; 147 | } 148 | 149 | [Compact] 150 | [CCode (cname = "struct mpv_node_list", destroy_function = "", unref_function = "", free_function = "", copy_function = "")] 151 | public class NodeList { 152 | 153 | public int num; // Number of entries. Negative values are not allowed. 154 | [CCode (array_length = false)] 155 | public weak Mpv.Node[] values; // Mpv.Format.NODE_ARRAY: values[N] refers to value of the Nth item 156 | // Mpv.Format.NODE_MAP: values[N] refers to value of the Nth key/value pair 157 | [CCode (array_length = false)] 158 | public string[] keys; // Mpv.Format.NODE_MAP: keys[N] refers to key of the Nth key/value pair. 159 | } 160 | 161 | [Compact] 162 | [CCode (cname = "struct mpv_byte_array", destroy_function = "")] 163 | public class ByteArray { 164 | [CCode (array_length_cname = "size", array_length_type = "size_t")] 165 | public uint8[] data; 166 | } 167 | 168 | [Compact] 169 | [CCode (cname = "struct mpv_node", destroy_function = "", free_function = "mpv_free_node_contents")] 170 | public struct Node { 171 | 172 | [CCode (cname = "u.string")] 173 | public string u_string; 174 | [CCode (cname = "u.flag")] 175 | public int u_flag; 176 | [CCode (cname = "u.int64")] 177 | public int64 u_int64; 178 | [CCode (cname = "u.double_")] 179 | public double u_double; 180 | [CCode (cname = "u.list")] 181 | public weak Mpv.NodeList u_list; 182 | [CCode (cname = "u.ba")] 183 | public Mpv.ByteArray u_ba; 184 | public Mpv.Format format; 185 | //[CCode (cname = "mpv_free_node_contents")] 186 | //public void free_node_contents (); 187 | 188 | } 189 | 190 | [Compact] 191 | [CCode (cname = "mpv_handle", has_type_id = false, ref_function = "", cprefix = "mpv_", unref_function ="", free_function = "mpv_free")] 192 | public class Handle { 193 | 194 | [CCode (cname = "mpv_create")] 195 | public Handle (); 196 | 197 | public string client_name { 198 | [CCode (cname = "mpv_client_name")] get; 199 | } 200 | public Mpv.Error initialize (); 201 | public void terminate_destroy (); 202 | public string load_config_file (string filename); 203 | public int64 get_time_us(); 204 | public Mpv.Error set_option (string name, Mpv.Format format); 205 | public Mpv.Error set_option_string (string name, string data); 206 | public Mpv.Error command ([CCode (array_length = false)] string[]? args = null); 207 | public Mpv.Error command_node (Node args, Node result); 208 | public Mpv.Error command_string (string args); 209 | public Mpv.Error command_async (uint64 reply_userdata, [CCode (array_length = false)] string[]? args = null); 210 | public Mpv.Error command_node_async (uint64 reply_userdata, Mpv.Node args); 211 | [CCode (simple_generics = true, has_target = false)] 212 | public Mpv.Error set_property (string name, Mpv.Format format, T data); 213 | public Mpv.Error set_property_string (string name, string data); 214 | [CCode (simple_generics = true, has_target = false)] 215 | public Mpv.Error set_property_async (uint64 reply_userdata, string name, Mpv.Format format, T data); 216 | 217 | [CCode (cname = "mpv_get_property", simple_generics = true, has_target = false)] 218 | public Mpv.Error get_property_double (string name, Mpv.Format format, out double data); 219 | 220 | [CCode (cname = "mpv_get_property", simple_generics = true, has_target = false)] 221 | public Mpv.Error get_property_flag (string name, Mpv.Format format, out int data); 222 | 223 | [CCode (cname = "mpv_get_property", simple_generics = true, has_target = false)] 224 | public Mpv.Error get_property_int64 (string name, Mpv.Format format, out int64 data); 225 | 226 | [CCode (cname = "mpv_get_property", simple_generics = true, has_target = false)] 227 | public Mpv.Error get_property_node (string name, Mpv.Format format, out Mpv.Node data); 228 | 229 | public string get_property_string (string name); 230 | public string get_property_osd_string (string name); 231 | [CCode (simple_generics = true, has_target = false)] 232 | public Mpv.Error get_property_async (uint64 reply_userdata, string name, Mpv.Format format, T data); 233 | public Mpv.Error observe_property (uint64 reply_userdata, string name, Mpv.Format format); 234 | public Mpv.Error unobserve_property (uint64 reply_userdata); 235 | public Mpv.Error request_event (EventID event, bool enable); 236 | public Mpv.Error request_log_messages (string min_level); 237 | public Event? wait_event (double timeout); 238 | public void wakeup (); 239 | public void wait_async_requests (); 240 | public Mpv.Error hook_add (uint64 reply_userdata, string name, int priority); 241 | public Mpv.Error hook_continue (uint64 id); 242 | public void set_wakeup_callback (CallBack callback); 243 | } 244 | [CCode (cname = "cb", simple_generics = true, has_target = true)] 245 | public delegate void CallBack (); 246 | 247 | [CCode (cname = "mpv_free_node_contents")] 248 | public void free_node_contents (Node node); 249 | [CCode (cname = "mpv_event_name")] 250 | public string event_name (EventID event_id); 251 | 252 | 253 | } -------------------------------------------------------------------------------- /src/ui/chapter-indicator.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 145 | 146 | False 147 | BalssChapterIndicator 148 | 149 | 150 | True 151 | False 152 | 12 153 | vertical 154 | 6 155 | 156 | 157 | 158 | 159 | 160 | True 161 | False 162 | True 163 | vertical 164 | 6 165 | 166 | 167 | True 168 | False 169 | 6 170 | 171 | 172 | 173 | 174 | 175 | True 176 | False 177 | True 178 | vertical 179 | 180 | 181 | 182 | 183 | 184 | False 185 | True 186 | 1 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | False 195 | True 196 | 0 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | False 205 | True 206 | 1 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | True 217 | False 218 | 6 219 | 220 | 221 | True 222 | False 223 | Total time: 224 | 0 225 | 226 | 227 | 228 | 231 | 232 | 233 | 0 234 | 0 235 | 236 | 237 | 238 | 239 | True 240 | False 241 | 1 242 | 243 | 244 | 1 245 | 0 246 | 247 | 248 | 249 | 250 | True 251 | False 252 | 253 | 254 | 0 255 | 1 256 | 2 257 | 258 | 259 | 260 | 261 | -------------------------------------------------------------------------------- /src/ui/control-button.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 0.5 7 | 1.5 8 | 1 9 | 0.05 10 | 10 11 | 12 | 13 | True 14 | False 15 | 6 16 | 17 | 18 | True 19 | False 20 | Volume: 21 | 1 22 | 23 | 24 | 25 | 28 | 29 | 30 | 0 31 | 0 32 | 33 | 34 | 35 | 36 | True 37 | False 38 | Rate: 39 | 1 40 | 41 | 42 | 43 | 46 | 47 | 48 | 0 49 | 1 50 | 51 | 52 | 53 | 54 | True 55 | False 56 | 0 57 | 58 | 59 | 1 60 | 0 61 | 62 | 63 | 64 | 65 | True 66 | False 67 | 0 68 | 69 | 70 | 1 71 | 1 72 | 73 | 74 | 75 | 76 | 1 77 | 0.01 78 | 10 79 | 80 | 81 | False 82 | 83 | 84 | True 85 | False 86 | 12 87 | vertical 88 | 6 89 | 90 | 91 | 92 | 93 | 94 | True 95 | False 96 | 0 97 | 12 98 | 99 | 100 | True 101 | False 102 | vertical 103 | 104 | 105 | True 106 | False 107 | start 108 | center 109 | True 110 | vertical 111 | 112 | 113 | True 114 | False 115 | start 116 | center 117 | multimedia-volume-control-symbolic 118 | 119 | 120 | False 121 | True 122 | 0 123 | 124 | 125 | 126 | 127 | False 128 | True 129 | 0 130 | 131 | 132 | 133 | 134 | False 135 | True 136 | 0 137 | 138 | 139 | 140 | 141 | True 142 | True 143 | True 144 | volume_adjustment 145 | False 146 | 147 | 150 | 151 | 152 | False 153 | True 154 | 1 155 | 156 | 157 | 158 | 159 | False 160 | True 161 | 1 162 | 163 | 164 | 165 | 166 | True 167 | False 168 | True 169 | 0 170 | 12 171 | 172 | 173 | True 174 | False 175 | True 176 | vertical 177 | 178 | 179 | True 180 | False 181 | center 182 | True 183 | vertical 184 | 185 | 186 | True 187 | False 188 | start 189 | center 190 | media-playback-rate-symbolic 191 | 192 | 193 | False 194 | True 195 | 0 196 | 197 | 198 | 199 | 200 | False 201 | True 202 | 0 203 | 204 | 205 | 206 | 207 | True 208 | True 209 | 0 210 | 211 | 212 | 213 | 214 | True 215 | True 216 | center 217 | 4 218 | 8 219 | 5 220 | 1.00 221 | 1 222 | media-playback-rate-prefix-symbolic 223 | False 224 | False 225 | number 226 | rate_adjustment 227 | 2 228 | 1 229 | 230 | 231 | 232 | False 233 | True 234 | 1 235 | 236 | 237 | 238 | 239 | False 240 | True 241 | 3 242 | 243 | 244 | 245 | 246 | 247 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | -------------------------------------------------------------------------------- /src/mpv-wrapper-backend.vala: -------------------------------------------------------------------------------- 1 | /* mpv-wrapper-backend.vala 2 | * 3 | * Copyright (C) 2018 Nick 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | 20 | 21 | namespace Balss { 22 | 23 | 24 | public struct Chapter { 25 | 26 | double offset; 27 | double end; 28 | string title; 29 | } 30 | 31 | 32 | 33 | public struct Metadata { 34 | 35 | string title; 36 | string artist; 37 | string album; 38 | string album_artist; 39 | string composer; 40 | string genre; 41 | string date; 42 | string track; 43 | string disc; 44 | } 45 | 46 | 47 | 48 | public class Player : GLib.Object { 49 | 50 | 51 | private Mpv.Handle ctx; 52 | 53 | public signal void volume_changed (double volume); 54 | public signal void mute_changed (bool mute); 55 | public signal void chapter_changed (int index); 56 | public signal void rate_changed (double speed); 57 | public signal void duration_changed (double duration); 58 | public signal void position_updated (double position); 59 | public signal void pause_changed (bool pause); 60 | public signal void metadata_updated (); 61 | public signal void end_of_stream (); 62 | public signal void shutdown (); 63 | 64 | public Metadata metadata; 65 | 66 | 67 | 68 | construct { 69 | 70 | // libmpv: The LC_NUMERIC locale category must be set to "C". 71 | Intl.setlocale (NUMERIC, "C"); 72 | 73 | ctx = new Mpv.Handle (); 74 | set_options (); 75 | set_observed_properties (); 76 | ctx.set_wakeup_callback (wakeup_callback); 77 | ctx.initialize (); 78 | metadata = Metadata (); 79 | metadata = { "", "", "", "", "", "", "", "", "" }; 80 | 81 | } 82 | 83 | 84 | 85 | public Player () {} 86 | 87 | 88 | 89 | public void load_uri (string uri) { 90 | 91 | string[] cmd = {"loadfile", uri }; 92 | check ("1", ctx.command (cmd) ); 93 | } 94 | 95 | 96 | 97 | public void destroy_context () { 98 | 99 | ctx.terminate_destroy (); 100 | ctx = null; 101 | } 102 | 103 | 104 | 105 | private void set_options () { 106 | 107 | check ("2", ctx.set_option_string ("video", "no") ); 108 | check ("3", ctx.set_option_string ("audio-client-name", APP_ID) ); 109 | check ("4", ctx.set_option_string ("config", "no") ); 110 | check ("5", ctx.set_option_string ("title", "-") ); 111 | //check ("6", ctx.set_option_string ("log-file", "libmpv-log.txt") ); // for debug 112 | } 113 | 114 | 115 | 116 | private void set_observed_properties () { 117 | 118 | check (" 7", ctx.observe_property (0, "duration", Mpv.Format.DOUBLE) ); 119 | check (" 8", ctx.observe_property (0, "time-pos", Mpv.Format.DOUBLE) ); 120 | check (" 9", ctx.observe_property (0, "ao-volume", Mpv.Format.DOUBLE) ); 121 | check ("10", ctx.observe_property (0, "ao-mute", Mpv.Format.FLAG) ); 122 | check ("11", ctx.observe_property (0, "pause", Mpv.Format.FLAG) ); 123 | check ("12", ctx.observe_property (0, "speed", Mpv.Format.DOUBLE) ); 124 | check ("13", ctx.observe_property (0, "metadata", Mpv.Format.NODE) ); 125 | check ("14", ctx.observe_property (0, "chapter", Mpv.Format.INT64) ); 126 | } 127 | 128 | 129 | 130 | private void wakeup_callback () { 131 | 132 | GLib.Idle.add ( (GLib.SourceFunc) event_handler); 133 | } 134 | 135 | 136 | 137 | private bool event_handler () { 138 | 139 | if (ctx == null) 140 | return false; 141 | 142 | Mpv.Event ev = null; 143 | 144 | while (true) { 145 | 146 | ev = ctx.wait_event (0); 147 | 148 | if (ev.event_id == Mpv.EventID.NONE) { break; } 149 | 150 | if(ev.event_id == Mpv.EventID.PROPERTY_CHANGE) { 151 | 152 | unowned Mpv.EventProperty prop = ( (Mpv.Event) ev).data; 153 | 154 | if ("duration" == prop.name) { 155 | if (prop.format == Mpv.Format.DOUBLE) { 156 | double d = *(double*) ( (Mpv.EventProperty) prop).data; 157 | duration_changed (d); // Emit signal 158 | } 159 | } 160 | 161 | if ("time-pos" == prop.name) { 162 | if (prop.format == Mpv.Format.DOUBLE) { 163 | double pos = *(double*) ( (Mpv.EventProperty) prop).data; 164 | position_updated (pos); // Emit signal 165 | } 166 | } 167 | 168 | if ("pause" == prop.name) { 169 | if (prop.format == Mpv.Format.FLAG) { 170 | int i = *(int*) ( (Mpv.EventProperty)prop).data; 171 | bool p = (i == 0) ? false : true; 172 | pause_changed (p); // Emit signal 173 | } 174 | } 175 | 176 | if ("ao-volume" == prop.name) { 177 | if (prop.format == Mpv.Format.DOUBLE) { 178 | double vol = *(double*) ( (Mpv.EventProperty) prop).data; 179 | volume_changed (vol); // Emit signal 180 | } 181 | } 182 | 183 | if ("ao-mute" == prop.name) { 184 | if (prop.format == Mpv.Format.FLAG) { 185 | int i = *(int*) ( (Mpv.EventProperty) prop).data; 186 | bool m = (i == 0) ? false : true; 187 | mute_changed (m); // Emit signal 188 | } 189 | } 190 | 191 | if ("metadata" == prop.name) { 192 | this.get_metadata (); 193 | metadata_updated (); // Emit signal 194 | } 195 | 196 | if ("chapter" == prop.name) { 197 | if (prop.format == Mpv.Format.INT64) { 198 | int index = (int) *(int64*) ( (Mpv.EventProperty) prop).data; 199 | chapter_changed (index); // Emit signal 200 | } 201 | } 202 | 203 | if ("speed" == prop.name) { 204 | if (prop.format == Mpv.Format.DOUBLE) { 205 | double s = *(double*) ( (Mpv.EventProperty) prop).data; 206 | rate_changed (s); // Emit signal 207 | } 208 | } 209 | } 210 | 211 | if(ev.event_id == Mpv.EventID.END_FILE) { 212 | Mpv.EventEndFile eef = *(Mpv.EventEndFile*) ( (Mpv.Event) ev).data; 213 | if (eef.reason == Mpv.EndFileReason.EOF) { 214 | end_of_stream (); // Emit signal 215 | } 216 | if (eef.reason == Mpv.EndFileReason.ERROR) { 217 | debug ("Playback was terminated abnormally. ERROR ID %d", 218 | eef.error ); 219 | } 220 | break; 221 | } 222 | 223 | if(ev.event_id == Mpv.EventID.IDLE) { 224 | //FIXME 225 | break; 226 | } 227 | 228 | if(ev.event_id == Mpv.EventID.SHUTDOWN) { 229 | shutdown (); // Emit signal 230 | break; 231 | } 232 | 233 | } 234 | return false; 235 | } 236 | 237 | 238 | 239 | public void set_previous_chapter () { 240 | 241 | string[] command = {"add", "chapter", "-1"}; 242 | check ("15", 243 | ctx.command_async (Mpv.EventID.COMMAND_REPLY, command) ); 244 | } 245 | 246 | 247 | 248 | public void set_next_chapter () { 249 | 250 | string[] command = {"add", "chapter", "1"}; 251 | check ("16", 252 | ctx.command_async (Mpv.EventID.COMMAND_REPLY, command) ); 253 | } 254 | 255 | 256 | 257 | 258 | public int get_chapter_count () { 259 | 260 | int64 i; 261 | check ("17", 262 | ctx.get_property_int64 ("chapters", 263 | Mpv.Format.INT64, 264 | out i) ); 265 | 266 | return (int) i; 267 | } 268 | 269 | 270 | 271 | public int get_chapter () { 272 | 273 | int64 i; 274 | check ("19", 275 | ctx.get_property_int64 ("chapter", 276 | Mpv.Format.INT64, 277 | out i) ); 278 | 279 | return (int) i; 280 | } 281 | 282 | 283 | 284 | public void set_chapter (int index) { 285 | 286 | int64? i = (int64) index; 287 | if (get_chapter() != index) { 288 | check ("20", 289 | ctx.set_property_async (Mpv.EventID.NONE, 290 | "chapter", 291 | Mpv.Format.INT64, 292 | i) ); 293 | } 294 | } 295 | 296 | 297 | 298 | public double get_position () { 299 | 300 | double p; 301 | check ("21", 302 | ctx.get_property_double ("time-pos", 303 | Mpv.Format.DOUBLE, 304 | out p) ); 305 | return (double) p; 306 | } 307 | 308 | 309 | 310 | public void set_position (double? p) { 311 | 312 | check ("22", 313 | ctx.set_property_async (Mpv.EventID.NONE, 314 | "time-pos", 315 | Mpv.Format.DOUBLE, 316 | p) ); 317 | } 318 | 319 | 320 | 321 | public double get_duration () { 322 | 323 | double d; 324 | check ("23", 325 | ctx.get_property_double ("duration", 326 | Mpv.Format.DOUBLE, 327 | out d) ); 328 | return (double) d; 329 | } 330 | 331 | 332 | 333 | public bool get_pause () { 334 | 335 | int i; 336 | check ("24", 337 | ctx.get_property_flag ("pause", 338 | Mpv.Format.FLAG, 339 | out i) ); 340 | return (bool) (i == 0) ? false : true; 341 | } 342 | 343 | 344 | 345 | public void play () { 346 | 347 | set_pause (false); 348 | } 349 | 350 | 351 | 352 | public void pause () { 353 | 354 | set_pause (true); 355 | } 356 | 357 | 358 | 359 | private void set_pause (bool pause) { 360 | 361 | int? i = (pause == false) ? 0 : 1; 362 | check ("25", 363 | ctx.set_property_async (Mpv.EventID.NONE, 364 | "pause", 365 | Mpv.Format.FLAG, 366 | i) ); 367 | } 368 | 369 | 370 | 371 | public double get_volume () { 372 | 373 | double v; 374 | check ("26", 375 | ctx.get_property_double ("ao-volume", 376 | Mpv.Format.DOUBLE, 377 | out v) ); 378 | 379 | return (double) v; 380 | } 381 | 382 | 383 | 384 | public void set_volume (double? vol) { 385 | 386 | check ("27", 387 | ctx.set_property_async (Mpv.EventID.NONE, 388 | "ao-volume", 389 | Mpv.Format.DOUBLE, 390 | vol) ); 391 | } 392 | 393 | 394 | 395 | public bool get_mute () { 396 | 397 | int i; 398 | check ("28", 399 | ctx.get_property_flag ("ao-mute", 400 | Mpv.Format.FLAG, 401 | out i) ); 402 | return (bool) (i == 0) ? false : true; 403 | } 404 | 405 | 406 | 407 | public void set_mute (bool? mute) { 408 | 409 | int m = (mute == false) ? 0 : 1; 410 | check ("29", 411 | ctx.set_property_async (Mpv.EventID.NONE, 412 | "ao-mute", 413 | Mpv.Format.FLAG, 414 | m) ); 415 | } 416 | 417 | 418 | 419 | public bool get_soft_mute () { 420 | 421 | int i; 422 | check ("30", 423 | ctx.get_property_flag ("mute", 424 | Mpv.Format.FLAG, 425 | out i) ); 426 | return (bool) (i == 0) ? false : true; 427 | } 428 | 429 | 430 | 431 | public void set_soft_mute (bool? mute) { 432 | 433 | int m = (mute == false) ? 0 : 1; 434 | check ("31", 435 | ctx.set_property_async (Mpv.EventID.NONE, 436 | "mute", 437 | Mpv.Format.FLAG, 438 | m) ); 439 | } 440 | 441 | 442 | 443 | public double get_rate () { 444 | 445 | double s; 446 | check ("32", 447 | ctx.get_property_double ("speed", 448 | Mpv.Format.DOUBLE, 449 | out s) ); 450 | return (double) s; 451 | } 452 | 453 | 454 | 455 | public void set_rate (double? speed) { 456 | 457 | check ("33", 458 | ctx.set_property_async (Mpv.EventID.NONE, 459 | "speed", 460 | Mpv.Format.DOUBLE, 461 | speed) ); 462 | } 463 | 464 | 465 | 466 | private void get_metadata () { 467 | 468 | Mpv.Node node; 469 | string key = ""; 470 | 471 | check ("34", 472 | ctx.get_property_node ("metadata", Mpv.Format.NODE, out node) ); 473 | 474 | if (node.format == Mpv.Format.NODE_MAP) { 475 | for (int i = 0; i < node.u_list.num; i++) { 476 | if (node.u_list.values[i].format == Mpv.Format.STRING) { 477 | 478 | key = node.u_list.keys[i]; 479 | 480 | if ("title" == key) { 481 | metadata.title = (string) node.u_list.values[i].u_string ?? ""; 482 | } 483 | if ("artist" == key) { 484 | metadata.artist = (string) node.u_list.values[i].u_string ?? ""; 485 | } 486 | if ("album" == key) { 487 | metadata.album = (string) node.u_list.values[i].u_string ?? ""; 488 | } 489 | if ("album_artist" == key) { 490 | metadata.album_artist = (string) node.u_list.values[i].u_string ?? ""; 491 | } 492 | if ("composer" == key) { 493 | metadata.composer = (string) node.u_list.values[i].u_string ?? ""; 494 | } 495 | if ("genre" == key) { 496 | metadata.genre = (string) node.u_list.values[i].u_string ?? ""; 497 | } 498 | if ("date" == key) { 499 | metadata.date = (string) node.u_list.values[i].u_string ?? ""; 500 | } 501 | if ("track" == key) { 502 | metadata.track = (string) node.u_list.values[i].u_string ?? ""; 503 | } 504 | if ("disc" == key) { 505 | metadata.disc = (string) node.u_list.values[i].u_string ?? ""; 506 | } 507 | } 508 | } 509 | } 510 | check ("35", ctx.set_option_string ("title", metadata.title) ); 511 | } 512 | 513 | 514 | 515 | public GLib.List? get_chapter_list () { 516 | 517 | Mpv.Node node; 518 | string title = ""; 519 | string t = ""; 520 | double offset = 0; 521 | double o = 0; 522 | int count = 0; 523 | bool ready = false; 524 | var list = new GLib.List (); 525 | check ("34", 526 | ctx.get_property_node ("chapter-list", Mpv.Format.NODE, out node) ); 527 | 528 | if(node.format == Mpv.Format.NODE_ARRAY) { 529 | count = node.u_list.num; 530 | for (int i = 0; i < count; i++) { 531 | if (node.u_list.values[i].format == Mpv.Format.NODE_MAP) { 532 | for (int n = 0; n < node.u_list.values[i].u_list.num; n++) { 533 | if ("title" == node.u_list.values[i].u_list.keys[n]) { 534 | if (node.u_list.values[i].u_list.values[n].format == Mpv.Format.STRING) { 535 | t = (string) node.u_list.values[i].u_list.values[n].u_string; 536 | } 537 | } else if ("time" == node.u_list.values[i].u_list.keys[n]) { 538 | if (node.u_list.values[i].u_list.values[n].format == Mpv.Format.DOUBLE) { 539 | o = (double) node.u_list.values[i].u_list.values[n].u_double; 540 | } 541 | } 542 | } 543 | if (ready) { 544 | 545 | Chapter chapter = Chapter (); 546 | chapter.title = title.strip () ; 547 | chapter.offset = offset; 548 | chapter.end = o; 549 | list.append (chapter); 550 | } else { 551 | 552 | ready = true; 553 | } 554 | 555 | title = t; 556 | offset = o; 557 | 558 | if (count - 1 == i) { 559 | 560 | Chapter chapter = Chapter (); 561 | chapter.title = t; 562 | chapter.offset = o; 563 | chapter.end = get_duration (); 564 | list.append (chapter); 565 | } 566 | } 567 | } 568 | } 569 | return list; 570 | } 571 | 572 | 573 | 574 | private void check (string id, Mpv.Error er) { 575 | 576 | if (er != Mpv.Error.SUCCESS) 577 | debug ("ERROR CODE: %d, id: %s", er, id ); 578 | } 579 | 580 | } 581 | } 582 | -------------------------------------------------------------------------------- /src/ui/window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | False 7 | 8 | 9 | True 10 | False 11 | 12 12 | vertical 13 | 14 | 15 | True 16 | True 17 | False 18 | app.open 19 | _Open 20 | 21 | 22 | False 23 | True 24 | 0 25 | 26 | 27 | 28 | 29 | True 30 | False 31 | 6 32 | 6 33 | 34 | 35 | False 36 | True 37 | 1 38 | 39 | 40 | 41 | 42 | True 43 | True 44 | False 45 | app.prefs 46 | _Preferences 47 | 48 | 49 | False 50 | True 51 | 2 52 | 53 | 54 | 55 | 56 | True 57 | False 58 | 6 59 | 6 60 | 61 | 62 | False 63 | True 64 | 3 65 | 66 | 67 | 68 | 69 | True 70 | True 71 | False 72 | app.shortcuts 73 | _Keyboard Shortcuts 74 | 75 | 76 | False 77 | True 78 | 4 79 | 80 | 81 | 82 | 83 | True 84 | True 85 | False 86 | app.about 87 | _About 88 | 89 | 90 | False 91 | True 92 | 5 93 | 94 | 95 | 96 | 97 | True 98 | True 99 | False 100 | app.quit 101 | _Quit 102 | 103 | 104 | False 105 | True 106 | 6 107 | 108 | 109 | 110 | 111 | main 112 | 1 113 | 114 | 115 | 116 | 389 | 390 | -------------------------------------------------------------------------------- /src/window.vala: -------------------------------------------------------------------------------- 1 | /* window.vala 2 | * 3 | * Copyright (C) 2018 Nick 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | 20 | 21 | namespace Balss { 22 | 23 | [GtkTemplate (ui = "/com/gitlab/nvlgit/Balss/window.ui")] 24 | public class PlayerWindow : Gtk.ApplicationWindow { 25 | 26 | public signal void new_notification (string title, string body); 27 | 28 | [GtkChild] private Gtk.Stack win_stack; 29 | [GtkChild] private Gtk.Stack header_stack; 30 | [GtkChild] private Gtk.HeaderBar header_main; 31 | [GtkChild] private Gtk.Button button_play; 32 | [GtkChild] private Gtk.Button seek_forward_button; 33 | [GtkChild] private Gtk.Button seek_backward_button; 34 | [GtkChild] private Gtk.Image pause_image; 35 | [GtkChild] private Gtk.Image play_image; 36 | [GtkChild] private Gtk.Box chapter_list_placeholder_box; 37 | [GtkChild] private Gtk.Box bottom_placeholder_box; 38 | [GtkChild] private Gtk.Box prefs_placeholder_box; 39 | [GtkChild] private Gtk.ModelButton open_menu_item; 40 | [GtkChild] private Gtk.ModelButton prefs_menu_item; 41 | [GtkChild] private Gtk.ModelButton shotcuts_menu_item; 42 | [GtkChild] private Gtk.ModelButton quit_menu_item; 43 | [GtkChild] private Gtk.Box control_button_placeholder_box; 44 | 45 | private Player? player; 46 | private GLib.List? list; 47 | private ChapterListBox? lb; 48 | private ChapterIndicator indicator; 49 | private ControlMenuButton control_button; 50 | private PrefsPage prefs_page; 51 | 52 | private double duration; 53 | private bool audiobook; 54 | private string basename; 55 | private int64 rounded_seconds; 56 | private double current_chapter_offset; 57 | private double current_chapter_duration; 58 | private bool speed_was_setted; 59 | private bool was_eos; 60 | private bool initial_volume_was_setted; 61 | private bool seek_needed; 62 | private bool chapter_changed_init; 63 | 64 | 65 | 66 | construct { 67 | 68 | this.duration = 0; 69 | this.rounded_seconds = -1; 70 | this.current_chapter_offset = 0; 71 | this.current_chapter_duration = 0; 72 | this.audiobook = false; 73 | temp = false; 74 | 75 | this.speed_was_setted = false; 76 | this.seek_needed = false; 77 | this.was_eos = false; 78 | this.initial_volume_was_setted = false; 79 | this.chapter_changed_init =false; 80 | 81 | indicator = new ChapterIndicator (); 82 | indicator.button_clicked.connect (indicator_button_clicked); 83 | bottom_placeholder_box.pack_start (this.indicator, true, true, 0); 84 | prefs_page = new PrefsPage (); 85 | prefs_page.update_gtk_theme (); 86 | prefs_placeholder_box.pack_start (this.prefs_page, true, true, 0); 87 | control_button = new ControlMenuButton (); 88 | control_button_placeholder_box.pack_start (this.control_button, true, true, 0); 89 | control_button.volume_changed.connect(control_button_volume_changed_cb); 90 | control_button.rate_changed.connect(control_button_rate_changed_cb); 91 | } 92 | 93 | private bool keypress_cb (Gdk.EventKey event) { 94 | 95 | if (event.type != Gdk.EventType.KEY_PRESS) 96 | return false; 97 | 98 | switch (event.hardware_keycode) { 99 | 100 | case 32: //Gdk.Key.o 101 | if (event.state == Gdk.ModifierType.CONTROL_MASK || 102 | event.state == Gdk.ModifierType.CONTROL_MASK + Gdk.ModifierType.LOCK_MASK) 103 | open_menu_item.clicked (); 104 | break; 105 | 106 | case 59: //Gdk.Key.comma 107 | if (event.state == Gdk.ModifierType.CONTROL_MASK || 108 | event.state == Gdk.ModifierType.CONTROL_MASK + Gdk.ModifierType.LOCK_MASK) 109 | prefs_menu_item.clicked (); 110 | else 111 | return false; 112 | break; 113 | 114 | case 61: //Gdk.Key.question 115 | if (event.state == Gdk.ModifierType.CONTROL_MASK + Gdk.ModifierType.SHIFT_MASK || 116 | event.state == Gdk.ModifierType.CONTROL_MASK + Gdk.ModifierType.SHIFT_MASK + Gdk.ModifierType.LOCK_MASK) 117 | shotcuts_menu_item.clicked (); 118 | else 119 | return false; 120 | break; 121 | 122 | case 24: //Gdk.Key.q: 123 | if (event.state == Gdk.ModifierType.CONTROL_MASK || 124 | event.state == Gdk.ModifierType.CONTROL_MASK + Gdk.ModifierType.LOCK_MASK) { 125 | debug ("Ctrl + Q"); 126 | quit_menu_item.clicked (); 127 | } else { 128 | return false; 129 | } 130 | break; 131 | 132 | case 75: // "F9" 133 | if (event.state == 0) 134 | control_button.clicked (); 135 | else 136 | return false; 137 | break; 138 | 139 | case 18: // "9" 140 | if (event.state == 0) 141 | decrease_volume (); 142 | else 143 | return false; 144 | break; 145 | 146 | case 19: // "0" 147 | if (event.state == 0) 148 | increase_volume (); 149 | else 150 | return false; 151 | break; 152 | 153 | case 33: // "P" 154 | case 65: // "Space" 155 | if (event.state == 0) 156 | button_play_clicked_cb (); 157 | else 158 | return false; 159 | break; 160 | 161 | case 80: //Gdk.Key.KP_Up: 162 | case 111: //Gdk.Key.Up: 163 | if (event.state == Gdk.ModifierType.CONTROL_MASK || 164 | event.state == Gdk.ModifierType.CONTROL_MASK + Gdk.ModifierType.LOCK_MASK) { 165 | debug ("Ctrl + UP"); 166 | player.set_previous_chapter (); 167 | } else { 168 | return false; 169 | } 170 | break; 171 | 172 | case 88: //Gdk.Key.KP_Down: 173 | case 116: //Gdk.Key.Down: 174 | if (event.state == Gdk.ModifierType.CONTROL_MASK || 175 | event.state == Gdk.ModifierType.CONTROL_MASK + Gdk.ModifierType.LOCK_MASK) { 176 | debug ("Ctrl + Down"); 177 | player.set_next_chapter (); 178 | } else { 179 | return false; 180 | } 181 | break; 182 | 183 | case 34: // "{" 184 | if (event.state == Gdk.ModifierType.CONTROL_MASK + Gdk.ModifierType.SHIFT_MASK || 185 | event.state == Gdk.ModifierType.CONTROL_MASK + Gdk.ModifierType.SHIFT_MASK + Gdk.ModifierType.LOCK_MASK) 186 | decrease_rate (); 187 | else 188 | return false; 189 | break; 190 | 191 | case 35: // "}" 192 | if (event.state == Gdk.ModifierType.CONTROL_MASK + Gdk.ModifierType.SHIFT_MASK || 193 | event.state == Gdk.ModifierType.CONTROL_MASK + Gdk.ModifierType.SHIFT_MASK + Gdk.ModifierType.LOCK_MASK) 194 | increase_rate (); 195 | else 196 | return false; 197 | break; 198 | 199 | case 22: //Gdk.Key.BackSpace: 200 | if (event.state == Gdk.ModifierType.CONTROL_MASK + Gdk.ModifierType.SHIFT_MASK || 201 | event.state == Gdk.ModifierType.CONTROL_MASK + Gdk.ModifierType.SHIFT_MASK + Gdk.ModifierType.LOCK_MASK) 202 | player.set_rate(1.0); 203 | else 204 | return false; 205 | break; 206 | 207 | default: 208 | return false; 209 | } 210 | debug ("Gtk.EventController"); 211 | return true; 212 | } 213 | 214 | public PlayerWindow (Gtk.Application app) { 215 | GLib.Object (application: app); 216 | 217 | this.set_default_size (App.preferences.window_width, App.preferences.window_height); 218 | this.delete_event.connect (window_delete_event_cb); 219 | this.key_press_event.connect (keypress_cb); 220 | 221 | if (App.preferences.play_last) { // when startup without argument 222 | if (App.preferences.last_uri.length > 6) { 223 | this.seek_needed = true; 224 | open (App.preferences.last_uri); 225 | } 226 | } 227 | } 228 | 229 | 230 | 231 | public void open (string uri) { 232 | 233 | if (App.preferences.play_last) { 234 | if (uri == App.preferences.last_uri) { 235 | this.seek_needed = true; 236 | } 237 | } 238 | if (App.preferences.last_uri != uri) { 239 | App.preferences.last_uri = uri; 240 | } 241 | this.basename = File.new_for_uri (uri).get_basename (); 242 | player_check_reinit (); 243 | activate_buttons (); 244 | this.speed_was_setted = false; 245 | player.load_uri (uri); 246 | } 247 | 248 | 249 | 250 | 251 | private void activate_buttons () { 252 | 253 | button_play.sensitive = true; 254 | seek_forward_button.sensitive = true; 255 | seek_backward_button.sensitive = true; 256 | control_button_placeholder_box.visible = true; 257 | player.rate_changed.disconnect (player_rate_changed_cb); 258 | control_button.rate = App.preferences.playback_rate; 259 | player.rate_changed.connect (player_rate_changed_cb); 260 | 261 | 262 | } 263 | public void show_page (string page_name) { 264 | 265 | win_stack.set_visible_child_name (page_name); 266 | header_stack.set_visible_child_name (page_name); 267 | } 268 | 269 | 270 | 271 | private void clear_ui_interface () { 272 | 273 | if (lb != null) { 274 | ( (Gtk. Container) chapter_list_placeholder_box).remove (lb); 275 | lb = null; 276 | } 277 | this.indicator.clear (); 278 | } 279 | 280 | 281 | 282 | private void connect_player_signals () { 283 | 284 | player.position_updated.connect (player_position_updated_cb); 285 | player.duration_changed.connect (player_duration_changed_cb); 286 | player.rate_changed.connect (player_rate_changed_cb); 287 | player.metadata_updated.connect (player_metadata_updated_cb); 288 | player.volume_changed.connect (player_volume_changed_cb); 289 | player.chapter_changed.connect (player_chapter_changed_cb); 290 | player.pause_changed.connect (player_pause_changed_cb); 291 | player.end_of_stream.connect (player_end_of_stream_cb); 292 | 293 | player.mute_changed.connect ( (m) => { //FIXME Needed? 294 | //debug ("Mute: %s\n", (m == false) ? "Unmuted" : "Muted" ); 295 | }); 296 | } 297 | 298 | 299 | 300 | private void show_chapter_list () { 301 | 302 | this.lb = new ChapterListBox (); 303 | chapter_list_placeholder_box.pack_end (lb, true, true, 0); 304 | this.lb.hexpand = true; 305 | int n = 0; 306 | 307 | this.list.foreach ( (c) => { 308 | 309 | var row = new ChapterRow (); 310 | n++; 311 | row.number = n.to_string (); 312 | row.title = c.title; 313 | row.duration = to_hhmmss (c.end - c.offset); 314 | this.lb.add_row (row); 315 | }); 316 | this.lb.chapter_row_activated.connect (lb_chapter_row_activated_cb); 317 | this.lb.seek_changed.connect (seekbar_value_changed_cb); 318 | } 319 | 320 | 321 | 322 | private void lb_chapter_row_activated_cb (int index){ 323 | 324 | if (this.player == null) return; 325 | 326 | this.player.set_chapter (index); 327 | 328 | if (this.player.get_pause () ) 329 | this.player.play (); 330 | } 331 | 332 | 333 | 334 | private void show_one_list_item () { 335 | 336 | this.lb = new ChapterListBox (); 337 | chapter_list_placeholder_box.pack_end (lb, true, true, 0); 338 | this.lb.hexpand = true; 339 | 340 | var row = new ChapterRow (); 341 | row.number = ""; 342 | row.title = this.basename; 343 | row.duration = to_hhmmss (this.duration); 344 | this.lb.add_row (row); 345 | this.lb.seek_changed.connect (seekbar_value_changed_cb); 346 | this.lb.mark_row_at_index (0); 347 | this.lb.set_range (0, this.duration ); 348 | } 349 | 350 | 351 | 352 | private void indicator_button_clicked (int direction){ 353 | 354 | if (this.player == null) return; 355 | 356 | if (direction < 0) 357 | player.set_previous_chapter (); 358 | else 359 | player.set_next_chapter (); 360 | } 361 | 362 | 363 | 364 | [GtkCallback] private void show_main_page_cb () { 365 | 366 | show_page ("main"); 367 | } 368 | 369 | 370 | 371 | private void control_button_rate_changed_cb (double val) { 372 | 373 | if (this.player == null) return; 374 | player.set_rate ( (double) val); 375 | } 376 | 377 | 378 | 379 | [GtkCallback] private void button_play_clicked_cb () { 380 | 381 | if (this.player == null) return; 382 | 383 | if (player.get_pause () == true) { 384 | player.play (); 385 | } else { 386 | player.pause (); 387 | } 388 | } 389 | 390 | 391 | 392 | [GtkCallback] private void seek_forward_button_clicked_cb () { 393 | 394 | if (this.player == null) return; 395 | 396 | double pos = player.get_position () + 5; 397 | if (pos < this.duration) 398 | this.player.set_position (pos); 399 | } 400 | 401 | 402 | 403 | [GtkCallback] private void seek_backward_button_clicked_cb () { 404 | 405 | if (this.player == null) return; 406 | 407 | double pos = player.get_position (); 408 | if (pos < 5) 409 | pos = 0; 410 | else 411 | pos = pos -5; 412 | this.player.set_position (pos); 413 | } 414 | 415 | 416 | 417 | private void control_button_volume_changed_cb (double val) { 418 | 419 | player.set_volume (val * 100); 420 | } 421 | 422 | 423 | 424 | private void seekbar_value_changed_cb (double val) { 425 | 426 | if (this.player == null) return; 427 | 428 | player.set_position (val); 429 | } 430 | 431 | 432 | 433 | private void player_check_reinit () { 434 | 435 | if (this.player != null) { 436 | this.player.destroy_context (); 437 | this.player = null; 438 | } 439 | this.player = new Player (); 440 | connect_player_signals (); 441 | } 442 | 443 | private bool window_delete_event_cb (Gtk.Widget widget, 444 | Gdk.EventAny event) { 445 | on_close_window (); 446 | return false; 447 | } 448 | 449 | 450 | 451 | public void on_close_window () { 452 | 453 | int w, h; 454 | player.pause (); 455 | App.preferences.last_position = player.get_position (); 456 | this.get_size (out w, out h); 457 | App.preferences.window_width = w; 458 | App.preferences.window_height = h; 459 | player.destroy_context (); 460 | player = null; 461 | } 462 | 463 | 464 | private void increase_rate () { 465 | 466 | if (this.player == null) return; 467 | var rate = this.player.get_rate () + 0.05; 468 | if (rate > 1.5) rate = 1.5; 469 | this.player.set_rate (rate); 470 | } 471 | 472 | 473 | private void decrease_rate () { 474 | 475 | if (this.player == null) return; 476 | var rate = this.player.get_rate () - 0.05; 477 | if (rate < 0.5) rate = 0.5; 478 | this.player.set_rate (rate); 479 | } 480 | 481 | private void increase_volume () { 482 | 483 | if (this.player == null) return; 484 | var val = this.player.get_volume () + 5; 485 | 486 | if (val > 100) 487 | val = 100; 488 | this.player.set_volume (val); 489 | } 490 | 491 | 492 | private void decrease_volume () { 493 | 494 | if (this.player == null) return; 495 | var val = this.player.get_volume () - 5; 496 | if (val < 0.0) 497 | val = 0; 498 | this.player.set_volume (val); 499 | } 500 | 501 | 502 | private void player_pause_changed_cb (bool p) { 503 | 504 | if (p == true) { 505 | pause_image.visible = false; 506 | play_image.visible = true; 507 | } else { 508 | play_image.visible = false; 509 | pause_image.visible = true; 510 | } 511 | debug ("pause_changed"); 512 | } 513 | 514 | 515 | 516 | private void player_duration_changed_cb (double dur) { 517 | 518 | clear_ui_interface (); 519 | this.duration = dur; 520 | 521 | if (0 < player.get_chapter_count () ) 522 | this.audiobook = true; 523 | else 524 | this.audiobook = false; 525 | 526 | if (this.audiobook) { 527 | this.list = new GLib.List (); 528 | this.list = player.get_chapter_list(); 529 | show_chapter_list (); 530 | } else { 531 | show_one_list_item (); 532 | } 533 | //this.lb.set_range (0, this.duration); 534 | debug ("duration_changed"); 535 | 536 | } 537 | 538 | private bool temp; 539 | 540 | private void player_position_updated_cb (double pos) { 541 | 542 | if (!temp) { 543 | debug ("position_changed"); 544 | temp = true; 545 | } 546 | check_and_apply_initial_stuffs (); 547 | 548 | int64 rounded_pos = GLib.Math.llround (pos); 549 | if (this.rounded_seconds != rounded_pos) { 550 | 551 | set_lb_position (pos); 552 | set_progress_fraction (pos); 553 | this.rounded_seconds = rounded_pos; 554 | } 555 | 556 | /* GLib.Idle.add ( () => { 557 | 558 | set_lb_position (pos); 559 | set_progress_fraction (pos); 560 | //this.rounded_seconds = rounded_pos; 561 | return false; 562 | }); 563 | */ 564 | } 565 | 566 | 567 | 568 | private void check_and_apply_initial_stuffs () { 569 | 570 | if (this.was_eos) { 571 | player.pause (); 572 | this.was_eos = false; 573 | } 574 | if (!this.speed_was_setted) { 575 | player.set_rate ( (double) App.preferences.playback_rate); 576 | this.speed_was_setted = true; 577 | } 578 | if (!initial_volume_was_setted) { 579 | this.control_button.volume = this.player.get_volume () / 100; 580 | initial_volume_was_setted = true; 581 | } 582 | if (this.seek_needed) { 583 | //this.player.set_soft_mute (true); 584 | player.pause (); 585 | player.set_position (App.preferences.last_position); 586 | this.seek_needed = false; 587 | player.play(); 588 | //this.player.set_soft_mute (false); 589 | } 590 | } 591 | 592 | 593 | 594 | private void player_chapter_changed_cb (int i) { 595 | 596 | debug ("chapter_changed"); 597 | 598 | if (i < 0) { //FIXME sametimes -1 599 | debug ("Negative chapter index: %d", i); 600 | i = 0; 601 | } 602 | 603 | int count = player.get_chapter_count (); 604 | 605 | this.current_chapter_offset = this.list.nth_data (i).offset; 606 | this.current_chapter_duration = 607 | this.list.nth_data (i).end - this.current_chapter_offset; 608 | 609 | this.lb.mark_row_at_index (i); 610 | this.lb.set_range (this.list.nth_data (i).offset, 611 | this.list.nth_data (i).end ); 612 | this.chapter_changed_init = true;//!!!!!!! 613 | 614 | this.indicator.set_info (i+1, count); 615 | 616 | bool notify = App.preferences.show_notifications; 617 | if (notify) { 618 | string body = "%d. %s".printf (i+1, this.list.nth_data (i).title); 619 | new_notification (player.metadata.title, body); // emit signal 620 | } 621 | } 622 | 623 | 624 | 625 | private void player_volume_changed_cb (double val) { 626 | 627 | debug ("volume changed"); 628 | this.control_button.volume = val / 100; 629 | } 630 | 631 | 632 | 633 | private void player_rate_changed_cb (double val) { 634 | 635 | debug ("rate_changed"); 636 | this.control_button.rate = val; 637 | //label = "%g×".printf (val); 638 | } 639 | 640 | 641 | 642 | private void player_metadata_updated_cb () { 643 | 644 | debug ("metadata_changed"); 645 | this.title = this.header_main.title = 646 | (player.metadata.title != "") ? player.metadata.title : this.basename; 647 | 648 | string artist = 649 | (player.metadata.artist != "") ? player.metadata.artist : ""; 650 | 651 | string genre = 652 | (player.metadata.genre != "") ? player.metadata.genre : ""; 653 | 654 | string year = ""; 655 | if (player.metadata.date.length > 3) { 656 | year = player.metadata.date.substring (0, 4); 657 | } 658 | this.header_main.subtitle = "%s%s%s".printf (artist, 659 | genre != "" ? " • " + genre : "", 660 | year != "" ? " • " + year : ""); 661 | } 662 | 663 | 664 | 665 | private void player_end_of_stream_cb () { 666 | 667 | debug ("end of stream"); 668 | was_eos = true; 669 | player_check_reinit (); 670 | this.player.load_uri (App.preferences.last_uri); 671 | } 672 | 673 | 674 | 675 | private void set_lb_position (double pos) { 676 | 677 | if (this.lb == null) return; 678 | 679 | this.lb.set_position (pos); 680 | 681 | if (audiobook) { 682 | this.lb.set_time (to_hhmmss (pos - this.current_chapter_offset) ); 683 | } else { 684 | this.lb.set_time (to_hhmmss (pos) ); 685 | } 686 | } 687 | 688 | private void set_progress_fraction (double pos) { 689 | 690 | if (this.duration == 0) return; 691 | 692 | if (pos == 0) { 693 | //var percent 694 | this.indicator.set_fraction (0); 695 | } else { 696 | var val = pos / duration; 697 | this.indicator.set_fraction (val); 698 | } 699 | int persent = calculate_percentage (pos, duration); 700 | this.indicator.set_tooltip (persent, 701 | to_hhmmss (pos), 702 | to_hhmmss(this.duration) ); 703 | } 704 | 705 | private int calculate_percentage (double p, double w) { 706 | 707 | int ret = 0; 708 | if (p > 0 && w > 0) 709 | ret = (int) GLib.Math.round (100 * p / w); 710 | return ret; 711 | } 712 | 713 | 714 | 715 | private string to_hhmmss (double seconds) { 716 | 717 | string ret = "0:00"; 718 | int hh = 0, mm = 0, ss = 0; 719 | 720 | if (seconds < 0.5) 721 | return ret; 722 | 723 | int64 secs = (int64) GLib.Math.llround (seconds); 724 | 725 | hh = (int) secs / (60 * 60); 726 | mm = (int) secs / 60 - (hh * 60); 727 | ss = (int) secs % 60; 728 | 729 | if (hh > 0) 730 | ret = "%d:%02d:%02d".printf (hh, mm, ss); 731 | else 732 | ret = "%d:%02d".printf (mm, ss); 733 | 734 | return ret; 735 | } 736 | 737 | /*************** TODO *****************/ 738 | 739 | public double get_rate () { 740 | 741 | return player.get_rate (); 742 | } 743 | 744 | public double get_volume () { 745 | 746 | return player.get_volume (); 747 | } 748 | 749 | public void set_volume (double vol) { 750 | 751 | player.set_volume (vol); 752 | } 753 | 754 | 755 | 756 | 757 | } 758 | } 759 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------