diff --git a/Cargo.lock b/Cargo.lock
index f32f074..62c9c51 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1412,7 +1412,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016"
dependencies = [
"log",
- "phf",
+ "phf 0.10.1",
"phf_codegen",
"string_cache",
"string_cache_codegen",
@@ -1723,7 +1723,17 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
dependencies = [
- "phf_shared",
+ "phf_shared 0.10.0",
+]
+
+[[package]]
+name = "phf"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
+dependencies = [
+ "phf_macros",
+ "phf_shared 0.11.2",
]
[[package]]
@@ -1732,8 +1742,8 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
dependencies = [
- "phf_generator",
- "phf_shared",
+ "phf_generator 0.10.0",
+ "phf_shared 0.10.0",
]
[[package]]
@@ -1742,10 +1752,33 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
dependencies = [
- "phf_shared",
+ "phf_shared 0.10.0",
"rand",
]
+[[package]]
+name = "phf_generator"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
+dependencies = [
+ "phf_shared 0.11.2",
+ "rand",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
+dependencies = [
+ "phf_generator 0.11.2",
+ "phf_shared 0.11.2",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.55",
+]
+
[[package]]
name = "phf_shared"
version = "0.10.0"
@@ -1755,6 +1788,15 @@ dependencies = [
"siphasher",
]
+[[package]]
+name = "phf_shared"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
+dependencies = [
+ "siphasher",
+]
+
[[package]]
name = "pin-project"
version = "1.1.5"
@@ -2248,6 +2290,17 @@ version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+[[package]]
+name = "smart-default"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.55",
+]
+
[[package]]
name = "socket2"
version = "0.5.6"
@@ -2273,7 +2326,7 @@ dependencies = [
"new_debug_unreachable",
"once_cell",
"parking_lot",
- "phf_shared",
+ "phf_shared 0.10.0",
"precomputed-hash",
"serde",
]
@@ -2284,8 +2337,8 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988"
dependencies = [
- "phf_generator",
- "phf_shared",
+ "phf_generator 0.10.0",
+ "phf_shared 0.10.0",
"proc-macro2",
"quote",
]
@@ -2369,6 +2422,7 @@ name = "televido"
version = "0.3.0"
dependencies = [
"eyre",
+ "futures-util",
"gettext-rs",
"gsettings-macro",
"gtk4",
@@ -2378,9 +2432,11 @@ dependencies = [
"libadwaita",
"mediathekviewweb",
"once_cell",
+ "phf 0.11.2",
"reqwest",
"serde",
"serde_json",
+ "smart-default",
"time",
"tokio",
"tracing",
diff --git a/Cargo.toml b/Cargo.toml
index be311f4..18ef1ac 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,6 +7,7 @@ publish = false
[dependencies]
adw = { package = "libadwaita", version = "0.6.0", features = ["v1_5"] }
eyre = "0.6.12"
+futures-util = "0.3.30"
gettext-rs = { version = "0.7", features = ["gettext-system"] }
gsettings-macro = "0.2.0"
gtk = { version = "0.8.1", package = "gtk4", features = ["gnome_46", "blueprint"] }
@@ -15,9 +16,11 @@ html2pango = "0.6.0"
indexmap = { version = "2.2.6", features = ["serde"] }
mediathekviewweb = "0.3.0"
once_cell = "1.19.0"
+phf = { version = "0.11.2", features = ["macros"] }
reqwest = { version = "0.12.2", features = ["json"] }
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.115"
+smart-default = "0.7.1"
time = { version = "0.3.34", features = ["parsing", "serde"] }
tokio = { version = "1.36.0", features = ["time", "rt-multi-thread", "macros"] }
tracing = "0.1.40"
diff --git a/data/de.k_bo.Televido.gschema.xml b/data/de.k_bo.Televido.gschema.xml
index 98fe531..0caec55 100644
--- a/data/de.k_bo.Televido.gschema.xml
+++ b/data/de.k_bo.Televido.gschema.xml
@@ -50,5 +50,8 @@
"high"
+
+ []
+
\ No newline at end of file
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 8dc7023..281ebab 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -2,6 +2,7 @@ data/de.k_bo.Televido.desktop.in
data/de.k_bo.Televido.gschema.xml
data/de.k_bo.Televido.metainfo.xml.in
src/application.rs
+src/channel_icons.rs
src/config.rs
src/help-overlay.blp
src/launcher/application_proxy.rs
@@ -14,18 +15,23 @@ src/live/channels.rs
src/live/mod.rs
src/live/view.blp
src/live/view.rs
-src/live/zapp.rs
src/main.rs
src/mediathek/card.blp
src/mediathek/card.rs
-src/mediathek/channels.rs
src/mediathek/mod.rs
src/mediathek/shows.rs
src/mediathek/view.blp
src/mediathek/view.rs
-src/preferences.blp
-src/preferences.rs
+src/preferences/dialog.blp
+src/preferences/dialog.rs
+src/preferences/live/mod.rs
+src/preferences/live/selector.blp
+src/preferences/live/selector_row.blp
+src/preferences/live/selector_row.rs
+src/preferences/live/selector.rs
+src/preferences/mod.rs
src/settings.rs
src/utils.rs
src/window.blp
-src/window.rs
\ No newline at end of file
+src/window.rs
+src/zapp.rs
\ No newline at end of file
diff --git a/po/televido.pot b/po/televido.pot
index 74b0305..7c5318b 100644
--- a/po/televido.pot
+++ b/po/televido.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: televido\n"
"Report-Msgid-Bugs-To: https://github.com/d-k-bo/televido/issues\n"
-"POT-Creation-Date: 2024-04-04 20:08+0200\n"
+"POT-Creation-Date: 2024-04-06 13:15+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -86,11 +86,15 @@ msgstr ""
msgid "Initital release"
msgstr ""
-#: src/application.rs:134
+#: src/application.rs:98 src/live/view.rs:126
+msgid "Failed to load livestream channels"
+msgstr ""
+
+#: src/application.rs:183
msgid "Failed to play video stream"
msgstr ""
-#: src/application.rs:177
+#: src/application.rs:226
msgid "Failed to launch video downloader"
msgstr ""
@@ -114,15 +118,15 @@ msgstr ""
msgid "Quit application"
msgstr ""
-#: src/launcher/selector.blp:19
+#: src/launcher/selector.blp:18
msgid "Cancel"
msgstr ""
-#: src/launcher/selector.blp:25
+#: src/launcher/selector.blp:24
msgid "Confirm"
msgstr ""
-#: src/launcher/selector.blp:41
+#: src/launcher/selector.blp:40
msgid "Custom program"
msgstr ""
@@ -135,7 +139,7 @@ msgstr ""
msgid "Failed to load external applications"
msgstr ""
-#: src/launcher/selector.rs:173 src/utils.rs:183
+#: src/launcher/selector.rs:173 src/utils.rs:101
msgid "See the terminal output for details."
msgstr ""
@@ -167,14 +171,10 @@ msgid "Play"
msgstr ""
#. translators: `{}` is replaced by the channel_id, e.g. `das_erste`
-#: src/live/view.rs:61
+#: src/live/view.rs:63
msgid "Failed load shows for channel “{}”"
msgstr ""
-#: src/live/view.rs:105
-msgid "Failed to load livestream channels"
-msgstr ""
-
#: src/mediathek/card.blp:106
msgid "Copy video URL"
msgstr ""
@@ -199,7 +199,7 @@ msgstr ""
msgid "Low Quality"
msgstr ""
-#: src/mediathek/card.rs:178
+#: src/mediathek/card.rs:177
msgid "Failed to open website in browser"
msgstr ""
@@ -279,34 +279,58 @@ msgstr ""
msgid "Failed to query the MediathekViewWeb API"
msgstr ""
-#: src/preferences.blp:11
+#: src/preferences/dialog.blp:9
msgid "External Programs"
msgstr ""
-#: src/preferences.blp:15
+#: src/preferences/dialog.blp:13
msgid "Video Player"
msgstr ""
-#: src/preferences.blp:19 src/preferences.blp:30
+#: src/preferences/dialog.blp:17 src/preferences/dialog.blp:28
msgid "Change"
msgstr ""
-#: src/preferences.blp:26
+#: src/preferences/dialog.blp:24
msgid "Video Downloader"
msgstr ""
+#: src/preferences/dialog.blp:36
+msgid "Live Channels"
+msgstr ""
+
+#: src/preferences/dialog.blp:40 src/preferences/live/selector.blp:7
+msgid "Select and reorder channels"
+msgstr ""
+
+#: src/preferences/live/selector.blp:15
+msgid "Visible channels"
+msgstr ""
+
+#: src/preferences/live/selector.blp:35
+msgid "Hidden channels"
+msgstr ""
+
+#: src/preferences/live/selector.blp:51
+msgid "Sort alphabetically"
+msgstr ""
+
+#: src/preferences/live/selector.blp:56
+msgid "Sort using default order"
+msgstr ""
+
#. translators: %b is Month name (short)
#. %-e is the Day number
#. %Y is the year (with century)
#. %H is the hours (24h format)
#. %M is the minutes
-#: src/utils.rs:55
+#: src/utils.rs:60
msgid "%b %-e, %Y %H:%M"
msgstr ""
#. translators: %H is the hours (24h format)
#. %M is the minutes
-#: src/utils.rs:65
+#: src/utils.rs:70
msgid "%H:%M"
msgstr ""
diff --git a/src/application.rs b/src/application.rs
index da488fd..d7e7ed2 100644
--- a/src/application.rs
+++ b/src/application.rs
@@ -1,25 +1,36 @@
// Copyright 2023 David Cabot
// SPDX-License-Identifier: GPL-3.0-or-later
-use std::{cell::OnceCell, sync::OnceLock};
+use std::{
+ cell::OnceCell,
+ rc::Rc,
+ sync::{Arc, OnceLock},
+};
use adw::{gio, glib, prelude::*, subclass::prelude::*};
+use eyre::WrapErr;
use gettextrs::gettext;
+use smart_default::SmartDefault;
use crate::{
config::{APP_ID, APP_NAME, AUTHOR, ISSUE_URL, PROJECT_URL, VERSION},
launcher::{ExternalProgram, ExternalProgramType, ProgramSelector},
preferences::TvPreferencesDialog,
settings::TvSettings,
- utils::{show_error, spawn_clone, tokio},
+ utils::{show_error, spawn, spawn_clone, tokio, AsyncResource},
window::TvWindow,
+ zapp::Zapp,
};
mod imp {
use super::*;
- #[derive(Debug, Default)]
- pub struct TvApplication {}
+ #[derive(Debug, SmartDefault)]
+ pub struct TvApplication {
+ #[default(Arc::new(Zapp::new().expect("failed to initialize Zapp client")))]
+ pub(super) zapp: Arc,
+ pub(super) live_channels: AsyncResource>,
+ }
#[glib::object_subclass]
impl ObjectSubclass for TvApplication {
@@ -52,27 +63,57 @@ glib::wrapper! {
@implements gio::ActionGroup, gio::ActionMap;
}
+thread_local! {
+ static APPLICATION: OnceCell = const { OnceCell::new() };
+}
+
impl TvApplication {
+ pub fn new() -> Self {
+ let slf: Self = glib::Object::builder()
+ .property("application-id", APP_ID)
+ .property("flags", gio::ApplicationFlags::FLAGS_NONE)
+ .property("resource-base-path", "/de/k_bo/televido")
+ .build();
+
+ APPLICATION.with(|app| {
+ app.set(slf.clone())
+ .expect("TvApplication may only be created once")
+ });
+
+ let live_channels = slf.live_channels();
+ live_channels.set_load_fn({
+ let zapp = slf.zapp();
+ move || {
+ let zapp = zapp.clone();
+ Box::pin(async move {
+ match tokio(async move {
+ zapp.channel_info_list()
+ .await
+ .wrap_err("Failed to load channel info list")
+ })
+ .await
+ {
+ Ok(channels) => Rc::new(channels),
+ Err(e) => {
+ show_error(e.wrap_err(gettext("Failed to load livestream channels")));
+ Default::default()
+ }
+ }
+ })
+ }
+ });
+ spawn(async move { live_channels.load() });
+
+ slf
+ }
pub fn get() -> Self {
- thread_local! {
- static APPLICATION: OnceCell = const { OnceCell::new() };
- }
APPLICATION.with(|app| {
- app.get_or_init(|| {
- glib::Object::builder()
- .property("application-id", APP_ID)
- .property("flags", gio::ApplicationFlags::FLAGS_NONE)
- .property("resource-base-path", "/de/k_bo/televido")
- .build()
- })
- .clone()
+ app.get()
+ .expect("TvApplication::get() may only be called from the main thread")
+ .clone()
})
}
- pub fn new() -> Self {
- Self::get()
- }
-
pub async fn dbus() -> zbus::Connection {
static DBUS: OnceLock = OnceLock::new();
match DBUS.get() {
@@ -96,6 +137,14 @@ impl TvApplication {
})
}
+ pub fn zapp(&self) -> Arc {
+ self.imp().zapp.clone()
+ }
+
+ pub fn live_channels(&self) -> AsyncResource> {
+ self.imp().live_channels.clone()
+ }
+
pub async fn play(&self, uri: String) {
let settings = TvSettings::get();
let player_name = settings.video_player_name();
diff --git a/src/channel_icons.rs b/src/channel_icons.rs
new file mode 100644
index 0000000..e26e93c
--- /dev/null
+++ b/src/channel_icons.rs
@@ -0,0 +1,143 @@
+// Copyright 2024 David Cabot
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+use adw::{gdk, gdk::gdk_pixbuf, glib, prelude::*};
+use eyre::WrapErr;
+use phf::phf_map;
+use tracing::error;
+
+use crate::application::TvApplication;
+
+pub fn load_channel_icon(channel_id: Option<&str>, image: >k::Image, size: i32) {
+ let Some(icon_name) = channel_id.and_then(|id| ICON_NAMES.get(id)) else {
+ image.set_icon_name(Some("image-missing-symbolic"));
+ return;
+ };
+
+ let application = TvApplication::get();
+ let style_manager = application.style_manager();
+ let scale_factor = application.window().surface().unwrap().scale_factor();
+ let size = scale_factor * size;
+
+ set_icon(&style_manager, image, icon_name, size);
+
+ style_manager.connect_dark_notify(glib::clone!(@weak image => move |style_manager| {
+ set_icon(style_manager, &image, icon_name, size)
+ }));
+
+ fn set_icon(style_manager: &adw::StyleManager, image: >k::Image, icon_name: &str, size: i32) {
+ match load_icon(
+ icon_name,
+ size,
+ ColorScheme::for_style_manager(style_manager),
+ ) {
+ Ok(texture) => image.set_from_paintable(Some(&texture)),
+ Err(e) => {
+ error!("{e:?}");
+ image.set_icon_name(Some("image-missing-symbolic"));
+ }
+ }
+ }
+
+ fn load_icon(
+ icon_name: &str,
+ size: i32,
+ color_scheme: ColorScheme,
+ ) -> eyre::Result {
+ let resource =
+ format!("/de/k_bo/televido/icons/scalable/channels/{color_scheme}/{icon_name}");
+
+ // load image manually with given size to avoid blurriness caused by scaling after rasterization
+ gdk_pixbuf::Pixbuf::from_resource_at_scale(&resource, size, size, true)
+ .map(|pixbuf| gdk::Texture::for_pixbuf(&pixbuf))
+ .wrap_err_with(|| format!("failed to load channel logo from {resource}"))
+ }
+}
+
+static ICON_NAMES: phf::Map<&'static str, &'static str> = phf_map! {
+ // live
+ "ard_alpha" => "ard-alpha.svg",
+ "arte" => "arte.svg",
+ "br_nord" => "br.svg",
+ "br_sued" => "br.svg",
+ "das_erste" => "das-erste.svg",
+ "deutsche_welle" => "deutsche-welle.svg",
+ "deutsche_welle_plus" => "deutsche-welle.svg",
+ "dreisat" => "3sat.svg",
+ "hr" => "hr.svg",
+ "kika" => "kika.svg",
+ "mdr_sachsen" => "mdr.svg",
+ "mdr_sachsen_anhalt" => "mdr.svg",
+ "mdr_thueringen" => "mdr.svg",
+ "ndr_hh" => "ndr.svg",
+ "ndr_mv" => "ndr.svg",
+ "ndr_nds" => "ndr.svg",
+ "ndr_sh" => "ndr.svg",
+ "one" => "one.svg",
+ "parlamentsfernsehen_1" => "parlamentsfernsehen.svg",
+ "parlamentsfernsehen_2" => "parlamentsfernsehen.svg",
+ "phoenix" => "phoenix.svg",
+ "rb" => "rb.svg",
+ "rbb_berlin" => "rbb.svg",
+ "rbb_brandenburg" => "rbb.svg",
+ "sr" => "sr.svg",
+ "swr_bw" => "swr.svg",
+ "swr_rp" => "swr.svg",
+ "tagesschau24" => "tagesschau24.svg",
+ "wdr" => "wdr.svg",
+ "zdf" => "zdf.svg",
+ "zdf_info" => "zdf-info.svg",
+ "zdf_neo" => "zdf-neo.svg",
+ // mediathek
+ "3Sat" => "3sat.svg",
+ "ARD" => "ard.svg",
+ "ARTE.DE" => "arte.svg",
+ "ARTE.EN" => "arte.svg",
+ "ARTE.ES" => "arte.svg",
+ "ARTE.FR" => "arte.svg",
+ "ARTE.IT" => "arte.svg",
+ "ARTE.PL" => "arte.svg",
+ "BR" => "br.svg",
+ "DW" => "deutsche-welle.svg",
+ "Funk.net" => "funk.svg",
+ "HR" => "hr.svg",
+ "KiKA" => "kika.svg",
+ "MDR" => "mdr.svg",
+ "NDR" => "ndr.svg",
+ "ORF" => "orf.svg",
+ "PHOENIX" => "phoenix.svg",
+ "Radio Bremen TV" => "rb.svg",
+ "RBB" => "rbb.svg",
+ "rbtv" => "rb.svg",
+ "SR" => "sr.svg",
+ "SRF" => "srf.svg",
+ "SWR" => "swr.svg",
+ "WDR" => "wdr.svg",
+ "ZDF" => "zdf.svg",
+ "ZDF-tivi" => "zdf.svg",
+};
+
+#[derive(Clone, Copy, Debug)]
+enum ColorScheme {
+ Light,
+ Dark,
+}
+
+impl ColorScheme {
+ fn for_style_manager(style_manager: &adw::StyleManager) -> Self {
+ if style_manager.is_dark() {
+ Self::Dark
+ } else {
+ Self::Light
+ }
+ }
+}
+
+impl std::fmt::Display for ColorScheme {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_str(match self {
+ Self::Light => "light",
+ Self::Dark => "dark",
+ })
+ }
+}
diff --git a/src/launcher/selector.blp b/src/launcher/selector.blp
index 330dbc9..4e1269f 100644
--- a/src/launcher/selector.blp
+++ b/src/launcher/selector.blp
@@ -4,7 +4,6 @@ using Gtk 4.0;
using Adw 1;
template $ProgramSelector: Adw.Dialog {
- follows-content-size: true;
can-close: false;
Gtk.Box {
diff --git a/src/live/card.rs b/src/live/card.rs
index 43175e4..fcb681a 100644
--- a/src/live/card.rs
+++ b/src/live/card.rs
@@ -8,9 +8,12 @@ use std::{
use adw::{glib, gtk, prelude::*, subclass::prelude::*};
-use crate::utils::{load_channel_icon, spawn, tokio};
+use crate::{
+ channel_icons::load_channel_icon,
+ utils::{spawn, tokio},
+};
-use super::channels::{Channel, ChannelObject};
+use super::channels::ChannelObject;
mod imp {
use super::*;
@@ -41,13 +44,11 @@ mod imp {
impl TvLiveCard {
fn set_icon(&self) {
- let icon_name = self
- .obj()
- .channel()
- .and_then(|c| c.id().parse::().ok())
- .map(|c| c.icon_name());
-
- load_channel_icon(&self.icon, icon_name);
+ load_channel_icon(
+ self.obj().channel().map(|c| c.id()).as_deref(),
+ &self.icon,
+ 64,
+ )
}
}
diff --git a/src/live/channels.rs b/src/live/channels.rs
index b7e711b..2637239 100644
--- a/src/live/channels.rs
+++ b/src/live/channels.rs
@@ -5,7 +5,7 @@ use std::cell::{Cell, RefCell};
use adw::{glib, prelude::*, subclass::prelude::*};
-use crate::utils::{channel_mapping, format_timestamp_time};
+use crate::utils::format_timestamp_time;
mod imp {
use super::*;
@@ -108,73 +108,3 @@ impl ChannelObject {
.build()
}
}
-
-channel_mapping! {
- #[derive(Clone, Copy, Debug, PartialEq)]
- pub enum Channel {
- #[channel(name = "ard_alpha", icon = "ard-alpha.svg")]
- ArdAlpha,
- #[channel(name = "arte", icon = "arte.svg")]
- Arte,
- #[channel(name = "br_nord", icon = "br.svg")]
- BrNord,
- #[channel(name = "br_sued", icon = "br.svg")]
- BrSüd,
- #[channel(name = "das_erste", icon = "das-erste.svg")]
- DasErste,
- #[channel(name = "deutsche_welle", icon = "deutsche-welle.svg")]
- DeutscheWelle,
- #[channel(name = "deutsche_welle_plus", icon = "deutsche-welle.svg")]
- DeutscheWellePlus,
- #[channel(name = "dreisat", icon = "3sat.svg")]
- DreiSat,
- #[channel(name = "hr", icon = "hr.svg")]
- Hr,
- #[channel(name = "kika", icon = "kika.svg")]
- KiKa,
- #[channel(name = "mdr_sachsen", icon = "mdr.svg")]
- MdrSachsen,
- #[channel(name = "mdr_sachsen_anhalt", icon = "mdr.svg")]
- MdrSachsenAnhalt,
- #[channel(name = "mdr_thueringen", icon = "mdr.svg")]
- MdrThüringen,
- #[channel(name = "ndr_hh", icon = "ndr.svg")]
- NdrHH,
- #[channel(name = "ndr_mv", icon = "ndr.svg")]
- NdrMV,
- #[channel(name = "ndr_nds", icon = "ndr.svg")]
- NdrNds,
- #[channel(name = "ndr_sh", icon = "ndr.svg")]
- NdrSH,
- #[channel(name = "one", icon = "one.svg")]
- One,
- #[channel(name = "parlamentsfernsehen_1", icon = "parlamentsfernsehen.svg")]
- Parlamentsfernsehen1,
- #[channel(name = "parlamentsfernsehen_2", icon = "parlamentsfernsehen.svg")]
- Parlamentsfernsehen2,
- #[channel(name = "phoenix", icon = "phoenix.svg")]
- Phoenix,
- #[channel(name = "rb", icon = "rb.svg")]
- Rb,
- #[channel(name = "rbb_berlin", icon = "rbb.svg")]
- RbbBerlin,
- #[channel(name = "rbb_brandenburg", icon = "rbb.svg")]
- RbbBrandenburg,
- #[channel(name = "sr", icon = "sr.svg")]
- Sr,
- #[channel(name = "swr_bw", icon = "swr.svg")]
- SwrBW,
- #[channel(name = "swr_rp", icon = "swr.svg")]
- SwrRP,
- #[channel(name = "tagesschau24", icon = "tagesschau24.svg")]
- Tagesschau24,
- #[channel(name = "wdr", icon = "wdr.svg")]
- Wdr,
- #[channel(name = "zdf", icon = "zdf.svg")]
- Zdf,
- #[channel(name = "zdf_info", icon = "zdf-info.svg")]
- ZdfInfo,
- #[channel(name = "zdf_neo", icon = "zdf-neo.svg")]
- ZdfNeo,
- }
-}
diff --git a/src/live/mod.rs b/src/live/mod.rs
index f451d4b..b7f4e6e 100644
--- a/src/live/mod.rs
+++ b/src/live/mod.rs
@@ -4,6 +4,5 @@
mod card;
mod channels;
mod view;
-mod zapp;
pub use self::view::TvLiveView;
diff --git a/src/live/view.blp b/src/live/view.blp
index 56af22c..f3aa45a 100644
--- a/src/live/view.blp
+++ b/src/live/view.blp
@@ -30,6 +30,7 @@ template $TvLiveView: Adw.Bin {
margin-bottom: 6;
margin-start: 6;
margin-end: 6;
+ valign: start;
styles [
"boxed-list"
diff --git a/src/live/view.rs b/src/live/view.rs
index acca844..e289294 100644
--- a/src/live/view.rs
+++ b/src/live/view.rs
@@ -1,24 +1,24 @@
// Copyright 2023 David Cabot
// SPDX-License-Identifier: GPL-3.0-or-later
-use std::{cell::OnceCell, sync::Arc};
-
use adw::{gio, glib, gtk, prelude::*, subclass::prelude::*};
use eyre::WrapErr;
use gettextrs::gettext;
+use smart_default::SmartDefault;
-use crate::utils::{show_error, spawn, tokio};
-
-use super::{
- card::TvLiveCard,
- channels::ChannelObject,
- zapp::{ChannelId, ChannelInfo, Show, ShowsResult, Zapp},
+use crate::{
+ application::TvApplication,
+ settings::TvSettings,
+ utils::{show_error, spawn, tokio},
+ zapp::{ChannelId, ChannelInfo, Show, ShowsResult},
};
+use super::{card::TvLiveCard, channels::ChannelObject};
+
mod imp {
use super::*;
- #[derive(Debug, Default, gtk::CompositeTemplate)]
+ #[derive(Debug, SmartDefault, gtk::CompositeTemplate)]
#[template(file = "src/live/view.blp")]
pub struct TvLiveView {
#[template_child]
@@ -27,35 +27,37 @@ mod imp {
spinner: TemplateChild,
#[template_child]
channels_list: TemplateChild,
-
- client: OnceCell>,
- channels_model: OnceCell,
}
impl TvLiveView {
- fn client(&self) -> Arc {
- self.client
- .get_or_init(|| Arc::new(Zapp::new().expect("failed to initialize HTTP client")))
- .clone()
- }
- fn channels_model(&self) -> gio::ListStore {
- self.channels_model
- .get_or_init(gio::ListStore::new::)
- .clone()
- }
- async fn load(&self) {
- let client = self.client();
- let load_channels = tokio(async move {
- let list = client
- .channel_info_list()
- .await
- .wrap_err("Failed to load channel info list")?;
+ async fn load_channels(&self) -> eyre::Result {
+ let client = TvApplication::get().zapp();
+ let settings = TvSettings::get();
+ let visible_channels = settings.live_channels();
+
+ let live_channels = TvApplication::get().live_channels().await;
+
+ let live_channels: Vec<(ChannelId, ChannelInfo)> = if visible_channels.is_empty() {
+ live_channels
+ .iter()
+ .map(|(channel_id, channel_info)| (channel_id.clone(), channel_info.clone()))
+ .collect()
+ } else {
+ visible_channels
+ .into_iter()
+ .filter_map(|channel_id| {
+ let channel_info = live_channels.get(&channel_id)?;
+ Some((channel_id, channel_info.clone()))
+ })
+ .collect()
+ };
+ let load_channels = tokio(async move {
let mut channels: Vec<(ChannelId, ChannelInfo, Option>)> =
- Vec::with_capacity(list.len());
+ Vec::with_capacity(live_channels.len());
- for (channel_id, channel_info) in list {
- match client.shows(&channel_id).await.wrap_err_with(|| {
+ for (channel_id, channel_info) in live_channels.iter() {
+ match client.shows(channel_id).await.wrap_err_with(|| {
eyre::Report::msg(
// translators: `{}` is replaced by the channel_id, e.g. `das_erste`
gettext("Failed load shows for channel “{}”")
@@ -63,54 +65,67 @@ mod imp {
)
})? {
ShowsResult::Shows(shows) => {
- channels.push((channel_id, channel_info, Some(shows)))
+ channels.push((channel_id.clone(), channel_info.clone(), Some(shows)))
+ }
+ ShowsResult::Error(_) => {
+ channels.push((channel_id.clone(), channel_info.clone(), None))
}
- ShowsResult::Error(_) => channels.push((channel_id, channel_info, None)),
}
}
Ok::<_, eyre::Report>(channels)
});
- match load_channels.await {
- Ok(channels) => {
- for (channel_id, channel_info, shows) in channels {
- let channel = ChannelObject::new(
- channel_id.as_ref(),
- &channel_info.name,
- &channel_info.stream_url,
+ let channel_objects = load_channels
+ .await?
+ .into_iter()
+ .map(|(channel_id, channel_info, shows)| {
+ let channel = ChannelObject::new(
+ channel_id.as_ref(),
+ &channel_info.name,
+ &channel_info.stream_url,
+ );
+ if let Some(Show {
+ title,
+ subtitle,
+ description,
+ channel: _,
+ start_time,
+ end_time,
+ }) = shows.as_ref().and_then(|shows| shows.first())
+ {
+ channel.set_title(Some(title.as_str()));
+ channel.set_subtitle(subtitle.as_deref());
+ channel.set_description(
+ description.as_deref().map(html2pango::markup).as_deref(),
);
- if let Some(Show {
- title,
- subtitle,
- description,
- channel: _,
- start_time,
- end_time,
- }) = shows.as_ref().and_then(|shows| shows.first())
- {
- channel.set_title(Some(title.as_str()));
- channel.set_subtitle(subtitle.as_deref());
- channel.set_description(
- description.as_deref().map(html2pango::markup).as_deref(),
- );
- channel.set_start_time(start_time.unix_timestamp());
- channel.set_end_time(end_time.unix_timestamp());
- }
- self.channels_model().append(&channel)
+ channel.set_start_time(start_time.unix_timestamp());
+ channel.set_end_time(end_time.unix_timestamp());
}
+ channel
+ })
+ .collect::();
+
+ Ok(channel_objects)
+ }
+ pub(super) async fn reload(&self) {
+ self.spinner.set_spinning(true);
+ self.stack.set_visible_child_name("spinner");
+
+ match self.load_channels().await {
+ Ok(channels) => {
+ self.channels_list.bind_model(Some(&channels), |channel| {
+ glib::Object::builder::()
+ .property("channel", channel)
+ .build()
+ .upcast()
+ });
self.stack.set_visible_child_name("channels");
self.spinner.set_spinning(false);
}
Err(e) => show_error(e.wrap_err(gettext("Failed to load livestream channels"))),
}
}
- pub(super) async fn reload(&self) {
- self.spinner.set_spinning(true);
- self.stack.set_visible_child_name("spinner");
- self.channels_model().remove_all();
- self.load().await;
- }
}
#[glib::object_subclass]
@@ -132,13 +147,7 @@ mod imp {
fn constructed(&self) {
self.parent_constructed();
- self.channels_list
- .bind_model(Some(&self.channels_model()), |channel| {
- glib::Object::builder::()
- .property("channel", channel)
- .build()
- .upcast()
- });
+ let settings = TvSettings::get();
self.channels_list.connect_row_activated(|_, row| {
let row = row
@@ -148,7 +157,11 @@ mod imp {
});
let slf = self.to_owned();
- spawn(async move { slf.load().await });
+ spawn(async move { slf.reload().await });
+
+ settings.connect_live_channels_changed(glib::clone!(@weak self as slf => move |_| {
+ spawn(async move { slf.reload().await });
+ }));
}
}
impl WidgetImpl for TvLiveView {}
diff --git a/src/main.rs b/src/main.rs
index 3d52125..f149aa7 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -18,6 +18,7 @@
#![allow(clippy::new_without_default)]
mod application;
+mod channel_icons;
mod config;
mod launcher;
mod live;
@@ -26,6 +27,7 @@ mod preferences;
mod settings;
mod utils;
mod window;
+mod zapp;
use self::{
application::TvApplication,
diff --git a/src/mediathek/card.rs b/src/mediathek/card.rs
index f594730..0c9819a 100644
--- a/src/mediathek/card.rs
+++ b/src/mediathek/card.rs
@@ -7,11 +7,12 @@ use adw::{gio, glib, gtk, prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use crate::{
+ channel_icons::load_channel_icon,
settings::{TvSettings, VideoQuality},
- utils::{load_channel_icon, show_error, spawn},
+ utils::{show_error, spawn},
};
-use super::{channels::Channel, shows::ShowObject};
+use super::shows::ShowObject;
mod imp {
use super::*;
@@ -33,13 +34,11 @@ mod imp {
}
impl TvMediathekCard {
fn set_icon(&self) {
- let icon_name = self
- .obj()
- .show()
- .and_then(|c| c.channel().parse::().ok())
- .map(|c| c.icon_name());
-
- load_channel_icon(&self.icon, icon_name);
+ load_channel_icon(
+ self.obj().show().map(|c| c.channel()).as_deref(),
+ &self.icon,
+ 64,
+ )
}
}
diff --git a/src/mediathek/channels.rs b/src/mediathek/channels.rs
deleted file mode 100644
index a26b97b..0000000
--- a/src/mediathek/channels.rs
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright 2023 David Cabot
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-use crate::utils::channel_mapping;
-
-channel_mapping! {
- #[derive(Clone, Copy, Debug, PartialEq)]
- pub enum Channel {
- #[channel(name = "3Sat", icon = "3sat.svg")]
- DreiSat,
- #[channel(name = "ARD", icon = "ard.svg")]
- Ard,
- #[channel(name = "ARTE.DE", icon = "arte.svg")]
- ArteDe,
- #[channel(name = "ARTE.EN", icon = "arte.svg")]
- ArteEn,
- #[channel(name = "ARTE.ES", icon = "arte.svg")]
- ArteEs,
- #[channel(name = "ARTE.FR", icon = "arte.svg")]
- ArteFr,
- #[channel(name = "ARTE.IT", icon = "arte.svg")]
- ArteIt,
- #[channel(name = "ARTE.PL", icon = "arte.svg")]
- ArtePl,
- #[channel(name = "BR", icon = "br.svg")]
- Br,
- #[channel(name = "DW", icon = "deutsche-welle.svg")]
- Dw,
- #[channel(name = "Funk.net", icon = "funk.svg")]
- FunkNet,
- #[channel(name = "HR", icon = "hr.svg")]
- Hr,
- #[channel(name = "KiKA", icon = "kika.svg")]
- Kika,
- #[channel(name = "MDR", icon = "mdr.svg")]
- Mdr,
- #[channel(name = "NDR", icon = "ndr.svg")]
- Ndr,
- #[channel(name = "ORF", icon = "orf.svg")]
- Orf,
- #[channel(name = "PHOENIX", icon = "phoenix.svg")]
- Phoenix,
- #[channel(name = "Radio Bremen TV", icon = "rb.svg")]
- RadioBremenTv,
- #[channel(name = "RBB", icon = "rbb.svg")]
- Rbb,
- #[channel(name = "rbtv", icon = "rb.svg")]
- Rbtv,
- #[channel(name = "SR", icon = "sr.svg")]
- Sr,
- #[channel(name = "SRF", icon = "srf.svg")]
- Srf,
- #[channel(name = "SWR", icon = "swr.svg")]
- Swr,
- #[channel(name = "WDR", icon = "wdr.svg")]
- Wdr,
- #[channel(name = "ZDF", icon = "zdf.svg")]
- Zdf,
- #[channel(name = "ZDF-tivi", icon = "zdf.svg")]
- ZdfTivi,
- }
-}
diff --git a/src/mediathek/mod.rs b/src/mediathek/mod.rs
index 11b44f4..b8589a4 100644
--- a/src/mediathek/mod.rs
+++ b/src/mediathek/mod.rs
@@ -2,7 +2,6 @@
// SPDX-License-Identifier: GPL-3.0-or-later
mod card;
-mod channels;
mod shows;
mod view;
diff --git a/src/preferences.blp b/src/preferences/dialog.blp
similarity index 72%
rename from src/preferences.blp
rename to src/preferences/dialog.blp
index 0625b65..af58f88 100644
--- a/src/preferences.blp
+++ b/src/preferences/dialog.blp
@@ -4,8 +4,6 @@ using Gtk 4.0;
using Adw 1;
template $TvPreferencesDialog: Adw.PreferencesDialog {
- follows-content-size: true;
-
Adw.PreferencesPage {
Adw.PreferencesGroup {
title: _("External Programs");
@@ -33,5 +31,20 @@ template $TvPreferencesDialog: Adw.PreferencesDialog {
}
}
}
+
+ Adw.PreferencesGroup {
+ title: _("Live Channels");
+ name: "live-channels";
+
+ Adw.ActionRow {
+ title: _("Select and reorder channels");
+ activatable: true;
+ activated => $select_live_channels() swapped;
+
+ Gtk.Image {
+ icon-name: "go-next-symbolic";
+ }
+ }
+ }
}
}
diff --git a/src/preferences.rs b/src/preferences/dialog.rs
similarity index 94%
rename from src/preferences.rs
rename to src/preferences/dialog.rs
index 8d1d47a..48db750 100644
--- a/src/preferences.rs
+++ b/src/preferences/dialog.rs
@@ -10,12 +10,13 @@ use crate::{
settings::TvSettings,
};
-mod imp {
+use super::live::TvLiveChannelSelector;
+mod imp {
use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
- #[template(file = "src/preferences.blp")]
+ #[template(file = "src/preferences/dialog.blp")]
#[properties(wrapper_type = super::TvPreferencesDialog)]
pub struct TvPreferencesDialog {
#[template_child]
@@ -57,6 +58,10 @@ mod imp {
self.settings.set_video_downloader_id(&downloader.id);
}
}
+ #[template_callback]
+ async fn select_live_channels(&self, #[rest] _: &[glib::Value]) {
+ self.obj().push_subpage(&TvLiveChannelSelector::new())
+ }
}
impl TvPreferencesDialog {
diff --git a/src/preferences/live/mod.rs b/src/preferences/live/mod.rs
new file mode 100644
index 0000000..2e0fa3d
--- /dev/null
+++ b/src/preferences/live/mod.rs
@@ -0,0 +1,4 @@
+mod selector;
+mod selector_row;
+
+pub use self::selector::TvLiveChannelSelector;
diff --git a/src/preferences/live/selector.blp b/src/preferences/live/selector.blp
new file mode 100644
index 0000000..9a61d35
--- /dev/null
+++ b/src/preferences/live/selector.blp
@@ -0,0 +1,60 @@
+// Copyright 2023 David Cabot
+// SPDX-License-Identifier: GPL-3.0-or-later
+using Gtk 4.0;
+using Adw 1;
+
+template $TvLiveChannelSelector: Adw.NavigationPage {
+ title: _("Select and reorder channels");
+
+ Adw.ToolbarView {
+ [top]
+ Adw.HeaderBar {}
+
+ Adw.PreferencesPage {
+ Adw.PreferencesGroup {
+ title: _("Visible channels");
+ name: "visible-channels";
+
+ header-suffix: Gtk.MenuButton {
+ icon-name: "view-more-symbolic";
+ menu-model: sort_menu;
+
+ styles [
+ "flat"
+ ]
+ };
+
+ Gtk.ListBox visible_channel_rows {
+ styles [
+ "boxed-list"
+ ]
+ }
+ }
+
+ Adw.PreferencesGroup {
+ title: _("Hidden channels");
+ name: "hidden-channels";
+
+ Gtk.ListBox hidden_channel_rows {
+ styles [
+ "boxed-list"
+ ]
+ }
+ }
+ }
+ }
+}
+
+menu sort_menu {
+ section {
+ item {
+ label: _("Sort alphabetically");
+ action: "live-channel-selector.sort-alphabetically";
+ }
+
+ item {
+ label: _("Sort using default order");
+ action: "live-channel-selector.sort-default-order";
+ }
+ }
+}
diff --git a/src/preferences/live/selector.rs b/src/preferences/live/selector.rs
new file mode 100644
index 0000000..470b234
--- /dev/null
+++ b/src/preferences/live/selector.rs
@@ -0,0 +1,209 @@
+// Copyright 2023 David Cabot
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+use std::cell::RefCell;
+
+use adw::{gio, glib, gtk, prelude::*, subclass::prelude::*};
+use indexmap::IndexMap;
+use smart_default::SmartDefault;
+
+use crate::{
+ application::TvApplication,
+ settings::TvSettings,
+ zapp::{ChannelId, ChannelInfo},
+};
+
+use super::selector_row::TvLiveChannelSelectorRow;
+
+mod imp {
+ use crate::utils::{spawn, ListStoreExtManual};
+
+ use super::*;
+
+ #[derive(Debug, SmartDefault, gtk::CompositeTemplate)]
+ #[template(file = "src/preferences/live/selector.blp")]
+ pub struct TvLiveChannelSelector {
+ #[template_child]
+ pub(super) visible_channel_rows: TemplateChild,
+ #[template_child]
+ pub(super) hidden_channel_rows: TemplateChild,
+
+ #[default(gio::ListStore::new::())]
+ pub(super) visible_channels: gio::ListStore,
+ #[default(gio::ListStore::new::())]
+ pub(super) hidden_channels: gio::ListStore,
+ pub(super) channel_rows: RefCell>,
+
+ pub(super) settings: TvSettings,
+ }
+
+ impl TvLiveChannelSelector {
+ async fn load(&self) {
+ let live_channels = TvApplication::get().live_channels().await;
+ let mut visible_channels = self.settings.live_channels();
+
+ self.channel_rows
+ .borrow_mut()
+ .extend(live_channels.iter().map(|(id, ChannelInfo { name, .. })| {
+ let row = TvLiveChannelSelectorRow::new(id, name);
+ row.connect_visible_notify(
+ glib::clone!(@weak self as slf => move |row: &TvLiveChannelSelectorRow| {
+ slf.remove_row(row);
+
+ if row.visible() {
+ slf.visible_channels.append(row);
+ } else {
+ let channel_rows = slf.channel_rows.borrow();
+ slf.hidden_channels
+ .typed_insert_sorted::(row, |a, b| {
+ channel_rows
+ .get_index_of(&a.channel_id())
+ .cmp(&channel_rows.get_index_of(&b.channel_id()))
+ });
+ }
+ }),
+ );
+ row.connect_received_drop(
+ glib::clone!(@weak self as slf => move |target_row, source_row| {
+ let target_visible = target_row.visible();
+ let source_visible = source_row.visible();
+
+ if target_visible && source_visible {
+ let src_pos = slf.visible_channels.find(source_row).unwrap();
+ let target_pos = slf.visible_channels.find(target_row).unwrap();
+
+ slf.visible_channels.remove(src_pos);
+ slf.visible_channels.insert(target_pos, source_row);
+ } else {
+ let target_rows = if target_row.visible() {
+ source_row.set_visible(true);
+ &slf.visible_channels
+ } else {
+ source_row.set_visible(false);
+ &slf.hidden_channels
+ };
+ slf.remove_row(source_row);
+
+ let target_pos = target_rows.find(target_row).unwrap();
+ target_rows.insert(target_pos, source_row)
+ }
+ }),
+ );
+ (id.clone(), row)
+ }));
+
+ if visible_channels.is_empty() {
+ visible_channels.extend(live_channels.keys().cloned());
+ }
+
+ let channel_rows = self.channel_rows.borrow();
+
+ for id in &visible_channels {
+ if let Some(row) = channel_rows.get(id) {
+ row.set_visible(true);
+ }
+ }
+
+ for (id, row) in channel_rows.iter() {
+ if !visible_channels.contains(id) {
+ row.set_visible(false);
+ }
+ }
+ }
+ fn remove_row(&self, row: &TvLiveChannelSelectorRow) {
+ if let Some(pos) = self.visible_channels.find(row) {
+ self.visible_channels.remove(pos)
+ }
+ if let Some(pos) = self.hidden_channels.find(row) {
+ self.hidden_channels.remove(pos)
+ }
+ }
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for TvLiveChannelSelector {
+ const NAME: &'static str = "TvLiveChannelSelector";
+ type Type = super::TvLiveChannelSelector;
+ type ParentType = adw::NavigationPage;
+
+ fn class_init(klass: &mut Self::Class) {
+ klass.bind_template();
+
+ klass.install_action(
+ "live-channel-selector.sort-alphabetically",
+ None,
+ |slf, _, _| {
+ let slf = slf.imp();
+ slf.visible_channels
+ .typed_sort::(|a, b| {
+ a.channel_name()
+ .to_lowercase()
+ .cmp(&b.channel_name().to_lowercase())
+ })
+ },
+ );
+ klass.install_action(
+ "live-channel-selector.sort-default-order",
+ None,
+ |slf, _, _| {
+ let slf = slf.imp();
+ let channel_rows = slf.channel_rows.borrow();
+
+ slf.visible_channels
+ .typed_sort::(|a, b| {
+ channel_rows
+ .get_index_of(&a.channel_id())
+ .cmp(&channel_rows.get_index_of(&b.channel_id()))
+ })
+ },
+ );
+ }
+
+ fn instance_init(obj: &glib::subclass::InitializingObject) {
+ obj.init_template();
+ }
+ }
+
+ impl ObjectImpl for TvLiveChannelSelector {
+ fn constructed(&self) {
+ self.parent_constructed();
+
+ self.visible_channel_rows
+ .bind_model(Some(&self.visible_channels), |row| {
+ row.clone().downcast().unwrap()
+ });
+ self.hidden_channel_rows
+ .bind_model(Some(&self.hidden_channels), |row| {
+ row.clone().downcast().unwrap()
+ });
+
+ spawn(glib::clone!(@weak self as slf => async move {
+ slf.load().await
+ }));
+
+ let settings = self.settings.clone();
+ self.visible_channels
+ .connect_items_changed(move |visible_channels, _, _, _| {
+ let channels = visible_channels
+ .iter::()
+ .map(|res| res.unwrap())
+ .map(|row| row.channel_id())
+ .collect::>();
+ settings.set_live_channels(channels);
+ });
+ }
+ }
+ impl WidgetImpl for TvLiveChannelSelector {}
+ impl NavigationPageImpl for TvLiveChannelSelector {}
+}
+
+glib::wrapper! {
+ pub struct TvLiveChannelSelector(ObjectSubclass)
+ @extends gtk::Widget, adw::NavigationPage;
+}
+
+impl TvLiveChannelSelector {
+ pub fn new() -> Self {
+ glib::Object::new()
+ }
+}
diff --git a/src/preferences/live/selector_row.blp b/src/preferences/live/selector_row.blp
new file mode 100644
index 0000000..02da6cd
--- /dev/null
+++ b/src/preferences/live/selector_row.blp
@@ -0,0 +1,36 @@
+// Copyright 2023 David Cabot
+// SPDX-License-Identifier: GPL-3.0-or-later
+using Gtk 4.0;
+using Adw 1;
+
+template $TvLiveChannelSelectorRow: Adw.ActionRow {
+ title: bind template.channel-name;
+ selectable: false;
+
+ [prefix]
+ Gtk.Image icon {}
+
+ [prefix]
+ Gtk.Image {
+ icon-name: "list-drag-handle-symbolic";
+ }
+
+ [suffix]
+ Gtk.Switch switch {
+ valign: center;
+ active: bind template.visible;
+ state: bind template.visible bidirectional;
+ }
+
+ Gtk.DragSource drag_source {
+ actions: move;
+ prepare => $drag_prepare() swapped;
+ drag-begin => $drag_begin() swapped;
+ }
+
+ Gtk.DropTarget drop_target {
+ actions: move;
+ formats: "TvLiveChannelSelectorRow";
+ drop => $drop() swapped;
+ }
+}
diff --git a/src/preferences/live/selector_row.rs b/src/preferences/live/selector_row.rs
new file mode 100644
index 0000000..5c454cd
--- /dev/null
+++ b/src/preferences/live/selector_row.rs
@@ -0,0 +1,138 @@
+// Copyright 2023 David Cabot
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+use std::{
+ cell::{Cell, RefCell},
+ sync::OnceLock,
+};
+
+use adw::{gdk, glib, glib::subclass::Signal, gtk, prelude::*, subclass::prelude::*};
+
+use crate::{channel_icons::load_channel_icon, zapp::ChannelId};
+
+mod imp {
+ use super::*;
+
+ #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
+ #[template(file = "src/preferences/live/selector_row.blp")]
+ #[properties(wrapper_type = super::TvLiveChannelSelectorRow)]
+ pub struct TvLiveChannelSelectorRow {
+ #[template_child]
+ icon: TemplateChild,
+ #[template_child]
+ switch: TemplateChild,
+ #[template_child]
+ drag_source: TemplateChild,
+ #[template_child]
+ drop_target: TemplateChild,
+
+ #[property(get, construct_only)]
+ channel_id: RefCell,
+ #[property(get, construct_only)]
+ channel_name: RefCell,
+ #[property(get, set)]
+ visible: Cell,
+
+ drag_x: Cell,
+ drag_y: Cell,
+ }
+
+ #[gtk::template_callbacks]
+ impl TvLiveChannelSelectorRow {
+ #[template_callback]
+ fn drag_prepare(&self, x: f64, y: f64) -> Option {
+ self.drag_x.set(x);
+ self.drag_y.set(y);
+ Some(gdk::ContentProvider::for_value(&self.obj().to_value()))
+ }
+ #[template_callback]
+ fn drag_begin(&self, drag: gdk::Drag) {
+ let drag_widget = gtk::ListBox::builder()
+ .width_request(self.obj().width())
+ .height_request(self.obj().height())
+ .build();
+
+ let drag_row: super::TvLiveChannelSelectorRow = glib::Object::builder()
+ .property("channel-id", &*self.channel_id.borrow())
+ .property("channel-name", &*self.channel_name.borrow())
+ .property("visible", true)
+ .build();
+ drag_widget.append(&drag_row);
+ drag_widget.drag_highlight_row(&drag_row);
+
+ let drag_icon: gtk::DragIcon = gtk::DragIcon::for_drag(&drag).downcast().unwrap();
+ drag_icon.set_child(Some(&drag_widget));
+ drag.set_hotspot(self.drag_x.get() as i32, self.drag_y.get() as i32)
+ }
+ #[template_callback]
+ fn drop(&self, value: glib::BoxedValue, #[rest] _: &[glib::Value]) -> bool {
+ match value.get::() {
+ Ok(row) => {
+ self.obj().emit_received_drop(&row);
+ true
+ }
+ _ => false,
+ }
+ }
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for TvLiveChannelSelectorRow {
+ const NAME: &'static str = "TvLiveChannelSelectorRow";
+ type Type = super::TvLiveChannelSelectorRow;
+ type ParentType = adw::ActionRow;
+
+ fn class_init(klass: &mut Self::Class) {
+ klass.bind_template();
+ klass.bind_template_callbacks()
+ }
+
+ fn instance_init(obj: &glib::subclass::InitializingObject) {
+ obj.init_template();
+ }
+ }
+
+ #[glib::derived_properties]
+ impl ObjectImpl for TvLiveChannelSelectorRow {
+ fn constructed(&self) {
+ self.parent_constructed();
+
+ load_channel_icon(Some(self.channel_id.borrow().as_ref()), &self.icon, 16);
+ }
+ fn signals() -> &'static [Signal] {
+ static SIGNALS: OnceLock<[Signal; 1]> = OnceLock::new();
+ SIGNALS.get_or_init(|| {
+ [Signal::builder("received-drop")
+ .param_types([super::TvLiveChannelSelectorRow::static_type()])
+ .build()]
+ })
+ }
+ }
+ impl WidgetImpl for TvLiveChannelSelectorRow {}
+ impl ListBoxRowImpl for TvLiveChannelSelectorRow {}
+ impl PreferencesRowImpl for TvLiveChannelSelectorRow {}
+ impl ActionRowImpl for TvLiveChannelSelectorRow {}
+}
+
+glib::wrapper! {
+ pub struct TvLiveChannelSelectorRow(ObjectSubclass)
+ @extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, adw::ActionRow;
+}
+
+impl TvLiveChannelSelectorRow {
+ pub fn new(id: &ChannelId, name: &str) -> Self {
+ glib::Object::builder()
+ .property("channel-id", id)
+ .property("channel-name", name)
+ .build()
+ }
+ pub fn emit_received_drop(&self, row: &TvLiveChannelSelectorRow) {
+ self.emit_by_name::<()>("received-drop", &[&row]);
+ }
+ pub fn connect_received_drop(&self, f: impl Fn(&Self, &TvLiveChannelSelectorRow) + 'static) {
+ self.connect_local("received-drop", false, move |args| {
+ f(&args[0].get().unwrap(), &args[1].get().unwrap());
+ None
+ });
+ }
+}
diff --git a/src/preferences/mod.rs b/src/preferences/mod.rs
new file mode 100644
index 0000000..36fa5d2
--- /dev/null
+++ b/src/preferences/mod.rs
@@ -0,0 +1,4 @@
+mod dialog;
+mod live;
+
+pub use self::dialog::TvPreferencesDialog;
diff --git a/src/settings.rs b/src/settings.rs
index 8a4b2ad..271a3ab 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -3,12 +3,17 @@
use std::{cell::OnceCell, fmt::Display, str::FromStr};
-use adw::{gio, glib};
+use adw::{gio, glib, prelude::*};
use gsettings_macro::gen_settings;
-use crate::config::BASE_APP_ID;
+use crate::{config::BASE_APP_ID, zapp::ChannelId};
#[gen_settings(file = "data/de.k_bo.Televido.gschema.xml")]
+#[gen_settings_define(
+ key_name = "live-channels",
+ arg_type = "Vec",
+ ret_type = "Vec"
+)]
pub struct TvSettings;
impl TvSettings {
@@ -30,6 +35,27 @@ impl Default for TvSettings {
}
}
+impl StaticVariantType for ChannelId {
+ fn static_variant_type() -> std::borrow::Cow<'static, glib::VariantTy> {
+ String::static_variant_type()
+ }
+}
+impl FromVariant for ChannelId {
+ fn from_variant(variant: &glib::Variant) -> Option {
+ String::from_variant(variant).map(ChannelId::from)
+ }
+}
+impl ToVariant for ChannelId {
+ fn to_variant(&self) -> glib::Variant {
+ AsRef::::as_ref(self).to_variant()
+ }
+}
+impl From for glib::Variant {
+ fn from(id: ChannelId) -> Self {
+ id.to_variant()
+ }
+}
+
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum VideoQuality {
High,
diff --git a/src/utils.rs b/src/utils.rs
index bfc35f4..fc45c96 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -1,19 +1,24 @@
// Copyright 2023 David Cabot
// SPDX-License-Identifier: GPL-3.0-or-later
-use std::{future::Future, sync::OnceLock, time::Duration};
-
-use adw::{
- glib,
- gtk::{self, gdk, gdk_pixbuf},
- prelude::*,
+use std::{
+ cell::{OnceCell, RefCell},
+ fmt::Debug,
+ future::Future,
+ pin::Pin,
+ rc::Rc,
+ sync::OnceLock,
+ task::{Poll, Waker},
+ time::Duration,
};
-use eyre::WrapErr;
+
+use adw::{gio, glib, gtk, prelude::*};
use gettextrs::gettext;
-use tracing::error;
use crate::{application::TvApplication, window::TvWindow};
+pub use self::async_resource::AsyncResource;
+
pub async fn tokio(fut: Fut) -> T
where
Fut: Future