mirror of
https://gitlab.gnome.org/World/Authenticator.git
synced 2025-03-04 08:44:40 +01:00
parent
3c5898fbe9
commit
e40843c102
14 changed files with 2658 additions and 27 deletions
77
Cargo.lock
generated
77
Cargo.lock
generated
|
@ -252,6 +252,8 @@ dependencies = [
|
|||
"diesel_migrations",
|
||||
"futures",
|
||||
"gettext-rs",
|
||||
"gstreamer",
|
||||
"gstreamer-base",
|
||||
"gtk-macros",
|
||||
"gtk4",
|
||||
"hex",
|
||||
|
@ -1254,6 +1256,63 @@ dependencies = [
|
|||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gstreamer"
|
||||
version = "0.17.0"
|
||||
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#beee75dabec062d35533c3d785b55d6c9301ca73"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if 1.0.0",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"glib",
|
||||
"gstreamer-sys",
|
||||
"libc",
|
||||
"muldiv",
|
||||
"num-rational",
|
||||
"once_cell",
|
||||
"paste",
|
||||
"pretty-hex",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gstreamer-base"
|
||||
version = "0.17.0"
|
||||
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#beee75dabec062d35533c3d785b55d6c9301ca73"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if 1.0.0",
|
||||
"glib",
|
||||
"gstreamer",
|
||||
"gstreamer-base-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gstreamer-base-sys"
|
||||
version = "0.17.0"
|
||||
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#beee75dabec062d35533c3d785b55d6c9301ca73"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gstreamer-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gstreamer-sys"
|
||||
version = "0.17.0"
|
||||
source = "git+https://gitlab.freedesktop.org/gstreamer/gstreamer-rs#beee75dabec062d35533c3d785b55d6c9301ca73"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gtk-macros"
|
||||
version = "0.2.0"
|
||||
|
@ -1747,6 +1806,12 @@ dependencies = [
|
|||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "muldiv"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5136edda114182728ccdedb9f5eda882781f35fa6e80cc360af12a8932507f3"
|
||||
|
||||
[[package]]
|
||||
name = "nb-connect"
|
||||
version = "1.0.2"
|
||||
|
@ -1982,6 +2047,12 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5d65c4d95931acda4498f675e332fcbdc9a06705cd07086c510e9b6009cd1c1"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.1.0"
|
||||
|
@ -2074,6 +2145,12 @@ version = "0.2.10"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
|
||||
|
||||
[[package]]
|
||||
name = "pretty-hex"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc5c99d529f0d30937f6f4b8a86d988047327bb88d04d2c4afc356de74722131"
|
||||
|
||||
[[package]]
|
||||
name = "pretty_env_logger"
|
||||
version = "0.4.0"
|
||||
|
|
|
@ -13,6 +13,8 @@ diesel = {version = "1.4", features = ["sqlite", "r2d2"]}
|
|||
diesel_migrations = {version = "1.4", features = ["sqlite"]}
|
||||
futures = "0.3"
|
||||
gettext-rs = {version = "0.5", features = ["gettext-system"]}
|
||||
gst = {package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs"}
|
||||
gst_base = {package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs"}
|
||||
gtk = {git = "https://github.com/gtk-rs/gtk4-rs", package = "gtk4"}
|
||||
gtk-macros = "0.2"
|
||||
hex = "0.4"
|
||||
|
|
1952
build-aux/767.patch
Normal file
1952
build-aux/767.patch
Normal file
File diff suppressed because it is too large
Load diff
|
@ -6,15 +6,17 @@
|
|||
"sdk-extensions": ["org.freedesktop.Sdk.Extension.rust-stable"],
|
||||
"command": "authenticator",
|
||||
"finish-args": [
|
||||
"--socket=pulseaudio",
|
||||
"--device=all",
|
||||
"--share=network",
|
||||
"--share=ipc",
|
||||
"--device=dri",
|
||||
"--socket=fallback-x11",
|
||||
"--socket=wayland",
|
||||
"--talk-name=org.a11y.Bus",
|
||||
"--talk-name=org.freedesktop.secrets",
|
||||
"--env=RUST_LOG=authenticator=debug",
|
||||
"--env=G_MESSAGES_DEBUG=none"
|
||||
"--env=G_MESSAGES_DEBUG=none",
|
||||
"--env=GST_PLUGIN_SYSTEM_PATH=/app/lib/gstreamer-1.0"
|
||||
],
|
||||
"build-options": {
|
||||
"append-path": "/usr/lib/sdk/rust-stable/bin",
|
||||
|
@ -30,7 +32,7 @@
|
|||
},
|
||||
"modules": [
|
||||
{
|
||||
"name": "libhandy",
|
||||
"name": "libadwaita",
|
||||
"buildsystem": "meson",
|
||||
"config-opts": [
|
||||
"-Dintrospection=disabled",
|
||||
|
@ -47,8 +49,7 @@
|
|||
"branch": "main"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
},{
|
||||
"name": "zbar",
|
||||
"config-opts": [
|
||||
"--without-qt",
|
||||
|
@ -76,6 +77,82 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : "gstreamer",
|
||||
"buildsystem" : "meson",
|
||||
"config-opts" : [
|
||||
"-Dexamples=disabled",
|
||||
"-Dtests=disabled",
|
||||
"-Ddoc=disabled"
|
||||
],
|
||||
"sources" : [
|
||||
{
|
||||
"type" : "git",
|
||||
"branch" : "1.18",
|
||||
"url" : "https://gitlab.freedesktop.org/gstreamer/gstreamer.git"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : "gst-plugins-base",
|
||||
"buildsystem" : "meson",
|
||||
"config-opts" : [
|
||||
"-Ddoc=disabled",
|
||||
"-Dexamples=disabled",
|
||||
"-Dtests=disabled",
|
||||
"-Dorc=enabled",
|
||||
"--wrap-mode=nodownload"
|
||||
],
|
||||
"sources" : [
|
||||
{
|
||||
"type" : "git",
|
||||
"branch" : "1.18",
|
||||
"url" : "https://gitlab.freedesktop.org/gstreamer/gst-plugins-base.git"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : "gst-plugins-good",
|
||||
"buildsystem" : "meson",
|
||||
"config-opts" : [
|
||||
"-Dgtk3=disabled",
|
||||
"-Dgtk4=enabled",
|
||||
"-Dgtk4-experiments=true",
|
||||
"-Ddoc=disabled",
|
||||
"-Dexamples=disabled",
|
||||
"-Dtests=disabled"
|
||||
],
|
||||
"sources" : [
|
||||
{
|
||||
"type" : "git",
|
||||
"branch" : "1.18",
|
||||
"url" : "https://gitlab.freedesktop.org/gstreamer/gst-plugins-good.git"
|
||||
},
|
||||
{
|
||||
"type" : "patch",
|
||||
"path" : "767.patch"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : "gst-bad-plugins",
|
||||
"buildsystem": "meson",
|
||||
"config-opts" : [
|
||||
"-Dzbar=enabled",
|
||||
"-Drsvg=disabled",
|
||||
"-Dvulkan=disabled",
|
||||
"-Dexamples=disabled",
|
||||
"-Dtests=disabled",
|
||||
"-Dintrospection=disabled"
|
||||
],
|
||||
"sources" : [
|
||||
{
|
||||
"type" : "git",
|
||||
"url" : "https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad.git",
|
||||
"branch" : "1.18"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "authenticator",
|
||||
"buildsystem": "meson",
|
||||
|
|
|
@ -18,6 +18,9 @@
|
|||
<!-- UI Files -->
|
||||
<file compressed="true" preprocess="xml-stripblanks" alias="preferences.ui">resources/ui/preferences.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks" alias="preferences_password_page.ui">resources/ui/preferences_password_page.ui</file>
|
||||
|
||||
<file compressed="true" preprocess="xml-stripblanks" alias="camera.ui">resources/ui/camera.ui</file>
|
||||
|
||||
<file compressed="true" preprocess="xml-stripblanks" alias="shortcuts.ui">resources/ui/shortcuts.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks" alias="window.ui">resources/ui/window.ui</file>
|
||||
</gresource>
|
||||
|
|
|
@ -1,5 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<menu id="scan_menu">
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Camera</attribute>
|
||||
<attribute name="action">add.camera</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Screenshot</attribute>
|
||||
<attribute name="action">add.screenshot</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</menu>
|
||||
<object class="GtkAdjustment" id="counter_adjustment">
|
||||
<property name="upper">60</property>
|
||||
<property name="step-increment">1</property>
|
||||
|
@ -11,9 +23,12 @@
|
|||
<property name="default-height">550</property>
|
||||
<property name="title" translatable="yes">Add a new account</property>
|
||||
<child>
|
||||
<object class="AdwLeaflet">
|
||||
<object class="AdwLeaflet" id="deck">
|
||||
<property name="can-unfold">False</property>
|
||||
<property name="can-swipe-back">True</property>
|
||||
<child>
|
||||
<object class="AdwLeafletPage">
|
||||
<property name="name">main</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
|
@ -41,10 +56,9 @@
|
|||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="scan_btn">
|
||||
<property name="receives-default">True</property>
|
||||
<object class="GtkMenuButton" id="scan_btn">
|
||||
<property name="tooltip-text" translatable="yes">Scan QR Code</property>
|
||||
<property name="action-name">add.scan-qr</property>
|
||||
<property name="menu-model">scan_menu</property>
|
||||
<property name="icon-name">qrscanner-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
|
@ -219,6 +233,14 @@
|
|||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwLeafletPage">
|
||||
<property name="name">camera</property>
|
||||
<property name="child">
|
||||
<object class="Camera" id="camera" />
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
|
87
data/resources/ui/camera.ui
Normal file
87
data/resources/ui/camera.ui
Normal file
|
@ -0,0 +1,87 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template parent="GtkWidget" class="Camera">
|
||||
<child>
|
||||
<object class="GtkStack" id="stack">
|
||||
<property name="transition-type">crossfade</property>
|
||||
<property name="hhomogeneous">False</property>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">loading</property>
|
||||
<property name="child">
|
||||
<object class="GtkSpinner" id="spinner">
|
||||
<property name="halign">center</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="width-request">48</property>
|
||||
<property name="height-request">48</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">not-found</property>
|
||||
<property name="child">
|
||||
<object class="AdwStatusPage">
|
||||
<property name="icon-name">camera-hardware-disabled-symbolic</property>
|
||||
<property name="title" translatable="yes">No Camera Found</property>
|
||||
<property name="description" translatable="yes"></property>
|
||||
<property name="child">
|
||||
<object class="GtkButton">
|
||||
<property name="label" translatable="yes">_From A Screenshot</property>
|
||||
<property name="action-name">add.screenshot</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="margin-top">24</property>
|
||||
<property name="use-underline">True</property>
|
||||
<style>
|
||||
<class name="large-button" />
|
||||
<class name="pill-button" />
|
||||
<class name="suggested-action" />
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">stream</property>
|
||||
<property name="child">
|
||||
<object class="GtkOverlay" id="overlay">
|
||||
<child type="overlay">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="valign">end</property>
|
||||
<property name="halign">fill</property>
|
||||
<property name="margin-start">18</property>
|
||||
<property name="margin-end">18</property>
|
||||
<property name="margin-bottom">18</property>
|
||||
<property name="spacing">8</property>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="icon-name">qrscanner-symbolic</property>
|
||||
<property name="tooltip-text" translatable="yes">Capture from a screenshot</property>
|
||||
<property name="width-request">44</property>
|
||||
<property name="height-request">44</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="action-name">camera.screenshot-scan</property>
|
||||
<style>
|
||||
<class name="osd" />
|
||||
<class name="circular-button" />
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
|
@ -9,6 +9,12 @@ dependency('gio-2.0', version: '>= 2.56')
|
|||
dependency('gdk-pixbuf-2.0')
|
||||
dependency('gtk4', version: '>= 4.0.0')
|
||||
dependency('libadwaita-1', version: '>=1.0.0')
|
||||
dependency('zbar', version: '>= 0.20')
|
||||
dependency('gstreamer-1.0', version: '>= 1.18')
|
||||
dependency('gstreamer-base-1.0', version: '>= 1.18')
|
||||
dependency('gstreamer-plugins-base-1.0', version: '>= 1.18')
|
||||
dependency('gstreamer-plugins-bad-1.0', version: '>= 1.18')
|
||||
|
||||
|
||||
cargo = find_program('cargo', required: true)
|
||||
glib_compile_schemas = find_program('glib-compile-schemas', required: true)
|
||||
|
|
|
@ -4,6 +4,7 @@ data/com.belmoussaoui.Authenticator.metainfo.xml.in.in
|
|||
data/resources/ui/account_add.ui
|
||||
data/resources/ui/account_details_page.ui
|
||||
data/resources/ui/account_row.ui
|
||||
data/resources/ui/camera.ui
|
||||
data/resources/ui/preferences_password_page.ui
|
||||
data/resources/ui/preferences.ui
|
||||
data/resources/ui/provider_page.ui
|
||||
|
|
|
@ -22,6 +22,7 @@ use config::{GETTEXT_PACKAGE, LOCALEDIR};
|
|||
fn main() {
|
||||
pretty_env_logger::init();
|
||||
gtk::init().expect("failed to init gtk4 ");
|
||||
gst::init().expect("failed to init gstreamer");
|
||||
// Prepare i18n
|
||||
setlocale(LocaleCategory::LcAll, "");
|
||||
bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR);
|
||||
|
|
|
@ -71,6 +71,7 @@ sources = files(
|
|||
'widgets/providers/mod.rs',
|
||||
'widgets/providers/page.rs',
|
||||
'widgets/providers/row.rs',
|
||||
'widgets/camera.rs',
|
||||
'widgets/mod.rs',
|
||||
'widgets/url_row.rs',
|
||||
'widgets/window.rs',
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
use crate::{
|
||||
helpers::qrcode,
|
||||
models::{Account, OTPMethod, OTPUri, Provider, ProvidersModel},
|
||||
widgets::{ProviderImage, UrlRow},
|
||||
widgets::{Camera, ProviderImage, UrlRow},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use glib::{clone, signal::Inhibit};
|
||||
use gtk::{gio, glib, prelude::*, subclass::prelude::*, CompositeTemplate};
|
||||
use gtk_macros::{action, get_action};
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::str::FromStr;
|
||||
|
||||
mod imp {
|
||||
use super::*;
|
||||
|
@ -20,6 +20,10 @@ mod imp {
|
|||
pub selected_provider: RefCell<Option<Provider>>,
|
||||
pub actions: gio::SimpleActionGroup,
|
||||
#[template_child]
|
||||
pub camera: TemplateChild<Camera>,
|
||||
#[template_child]
|
||||
pub deck: TemplateChild<adw::Leaflet>,
|
||||
#[template_child]
|
||||
pub image: TemplateChild<ProviderImage>,
|
||||
#[template_child]
|
||||
pub provider_website_row: TemplateChild<UrlRow>,
|
||||
|
@ -79,12 +83,15 @@ mod imp {
|
|||
algorithm_label: TemplateChild::default(),
|
||||
counter_row: TemplateChild::default(),
|
||||
period_row: TemplateChild::default(),
|
||||
deck: TemplateChild::default(),
|
||||
camera: TemplateChild::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
UrlRow::static_type();
|
||||
ProviderImage::static_type();
|
||||
Camera::static_type();
|
||||
klass.set_template_from_resource("/com/belmoussaoui/Authenticator/account_add.ui");
|
||||
Self::bind_template_children(klass);
|
||||
klass.add_signal("added", glib::SignalFlags::ACTION, &[], glib::Type::Unit);
|
||||
|
@ -137,20 +144,21 @@ impl AccountAddDialog {
|
|||
.connect_changed(clone!(@weak self as win => move |_| win.validate()));
|
||||
}
|
||||
|
||||
fn scan_qr(&self) -> Result<()> {
|
||||
qrcode::screenshot_area(
|
||||
self.clone().upcast::<gtk::Window>(),
|
||||
clone!(@weak self as dialog => move |screenshot| {
|
||||
if let Ok(otp_uri) = qrcode::scan(&screenshot) {
|
||||
dialog.set_from_otp_uri(otp_uri);
|
||||
fn scan_from_screenshot(&self) {
|
||||
let self_ = imp::AccountAddDialog::from_instance(self);
|
||||
self_.camera.from_screenshot();
|
||||
}
|
||||
}),
|
||||
)?;
|
||||
Ok(())
|
||||
|
||||
fn scan_from_camera(&self) {
|
||||
let self_ = imp::AccountAddDialog::from_instance(self);
|
||||
self_.deck.set_visible_child_name("camera");
|
||||
|
||||
self_.camera.start();
|
||||
}
|
||||
|
||||
fn set_from_otp_uri(&self, otp_uri: OTPUri) {
|
||||
let self_ = imp::AccountAddDialog::from_instance(self);
|
||||
self_.deck.set_visible_child_name("main"); // Switch back the form view
|
||||
|
||||
self_.token_entry.set_text(&otp_uri.secret);
|
||||
self_.username_entry.set_text(&otp_uri.label);
|
||||
|
@ -234,7 +242,7 @@ impl AccountAddDialog {
|
|||
self_.actions,
|
||||
"back",
|
||||
clone!(@weak self as dialog => move |_, _| {
|
||||
dialog.destroy();
|
||||
dialog.close();
|
||||
})
|
||||
);
|
||||
action!(
|
||||
|
@ -249,11 +257,17 @@ impl AccountAddDialog {
|
|||
|
||||
action!(
|
||||
self_.actions,
|
||||
"scan-qr",
|
||||
"camera",
|
||||
clone!(@weak self as dialog => move |_, _| {
|
||||
if let Err(err) = dialog.scan_qr() {
|
||||
warn!("Failed to scan a QR code {}", err)
|
||||
}
|
||||
dialog.scan_from_camera();
|
||||
})
|
||||
);
|
||||
|
||||
action!(
|
||||
self_.actions,
|
||||
"screenshot",
|
||||
clone!(@weak self as dialog => move |_, _| {
|
||||
dialog.scan_from_screenshot();
|
||||
})
|
||||
);
|
||||
self.insert_action_group("add", Some(&self_.actions));
|
||||
|
@ -268,12 +282,28 @@ impl AccountAddDialog {
|
|||
|
||||
self_.provider_completion.connect_match_selected(
|
||||
clone!(@weak self as dialog, @strong self_.model as model => move |_, store, iter| {
|
||||
let provider_id = store.get_value(iter, 0). get_some::<i32>().unwrap();
|
||||
let provider_id = store.get_value(iter, 0).get_some::<i32>().unwrap();
|
||||
let provider = model.get().unwrap().find_by_id(provider_id).unwrap();
|
||||
dialog.set_provider(provider);
|
||||
|
||||
Inhibit(false)
|
||||
}),
|
||||
);
|
||||
|
||||
self_
|
||||
.camera
|
||||
.connect_local(
|
||||
"code-detected",
|
||||
false,
|
||||
clone!(@weak self as dialog => move |args| {
|
||||
let code = args.get(1).unwrap().get::<String>().unwrap().unwrap();
|
||||
if let Ok(otp_uri) = OTPUri::from_str(&code) {
|
||||
dialog.set_from_otp_uri(otp_uri);
|
||||
}
|
||||
|
||||
None
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
|
370
src/widgets/camera.rs
Normal file
370
src/widgets/camera.rs
Normal file
|
@ -0,0 +1,370 @@
|
|||
use glib::{Receiver, Sender};
|
||||
use gst::prelude::*;
|
||||
use gtk::{
|
||||
gio,
|
||||
glib::{self, clone},
|
||||
prelude::*,
|
||||
subclass::prelude::*,
|
||||
CompositeTemplate,
|
||||
};
|
||||
use gtk_macros::send;
|
||||
use once_cell::sync::Lazy;
|
||||
/// Fancy Camera with QR code detection using ZBar
|
||||
///
|
||||
/// Pipeline:
|
||||
/// queue -- videoconvert -- zbar -- fakesink
|
||||
/// /
|
||||
/// device sink -- tee
|
||||
/// \
|
||||
/// queue -- glsinkbin
|
||||
///
|
||||
///
|
||||
|
||||
static PIPELINE_NAME: Lazy<glib::GString> = Lazy::new(|| glib::GString::from("camera"));
|
||||
|
||||
mod screenshot {
|
||||
use anyhow::Result;
|
||||
use ashpd::{
|
||||
desktop::screenshot::{Screenshot, ScreenshotOptions, ScreenshotProxy},
|
||||
zbus, RequestProxy, Response, WindowIdentifier,
|
||||
};
|
||||
use gtk::{gio, prelude::*};
|
||||
use image::GenericImageView;
|
||||
use zbar_rust::ZBarImageScanner;
|
||||
|
||||
pub fn scan(screenshot: &gio::File) -> Result<String> {
|
||||
let (data, _) = screenshot.load_contents(gio::NONE_CANCELLABLE)?;
|
||||
|
||||
let img = image::load_from_memory(&data)?;
|
||||
|
||||
let (width, height) = img.dimensions();
|
||||
let img_data: Vec<u8> = img.to_luma8().to_vec();
|
||||
|
||||
let mut scanner = ZBarImageScanner::new();
|
||||
|
||||
let results = scanner
|
||||
.scan_y800(&img_data, width, height)
|
||||
.map_err(|e| anyhow::format_err!(e))?;
|
||||
|
||||
if let Some(ref result) = results.get(0) {
|
||||
let content = String::from_utf8(result.data.clone())?;
|
||||
return Ok(content);
|
||||
}
|
||||
anyhow::bail!("Invalid QR code")
|
||||
}
|
||||
|
||||
pub fn capture<F: FnOnce(gio::File)>(window: gtk::Window, callback: F) -> Result<()> {
|
||||
let connection = zbus::Connection::new_session()?;
|
||||
let proxy = ScreenshotProxy::new(&connection)?;
|
||||
let handle = proxy.screenshot(
|
||||
WindowIdentifier::from(window),
|
||||
ScreenshotOptions::default().interactive(true).modal(true),
|
||||
)?;
|
||||
let request = RequestProxy::new(&connection, &handle)?;
|
||||
request.on_response(move |response: Response<Screenshot>| {
|
||||
if let Ok(screenshot) = response {
|
||||
callback(gio::File::new_for_uri(&screenshot.uri));
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CameraEvent {
|
||||
CodeDetected(String),
|
||||
DeviceAdded(gst::Device),
|
||||
DeviceRemoved(gst::Device),
|
||||
StreamStarted,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CameraState {
|
||||
Loading,
|
||||
NotFound,
|
||||
Ready,
|
||||
Paused,
|
||||
}
|
||||
|
||||
mod imp {
|
||||
use super::*;
|
||||
use glib::subclass;
|
||||
use std::cell::RefCell;
|
||||
|
||||
#[derive(Debug, CompositeTemplate)]
|
||||
pub struct Camera {
|
||||
pub actions: gio::SimpleActionGroup,
|
||||
pub sender: Sender<CameraEvent>,
|
||||
pub receiver: RefCell<Option<Receiver<CameraEvent>>>,
|
||||
pub pipeline: gst::Pipeline,
|
||||
pub sink: gst::Element,
|
||||
pub devices: gio::ListStore,
|
||||
pub monitor: gst::DeviceMonitor,
|
||||
#[template_child]
|
||||
pub stack: TemplateChild<gtk::Stack>,
|
||||
#[template_child]
|
||||
pub overlay: TemplateChild<gtk::Overlay>,
|
||||
#[template_child]
|
||||
pub spinner: TemplateChild<gtk::Spinner>,
|
||||
}
|
||||
|
||||
impl ObjectSubclass for Camera {
|
||||
const NAME: &'static str = "Camera";
|
||||
type Type = super::Camera;
|
||||
type ParentType = gtk::Widget;
|
||||
type Instance = subclass::simple::InstanceStruct<Self>;
|
||||
type Class = subclass::simple::ClassStruct<Self>;
|
||||
|
||||
glib::object_subclass!();
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.set_template_from_resource("/com/belmoussaoui/Authenticator/camera.ui");
|
||||
Self::bind_template_children(klass);
|
||||
klass.set_layout_manager_type::<gtk::BinLayout>();
|
||||
klass.add_signal(
|
||||
"code-detected",
|
||||
glib::SignalFlags::RUN_FIRST,
|
||||
&[String::static_type()],
|
||||
glib::Type::Unit,
|
||||
);
|
||||
}
|
||||
|
||||
fn instance_init(obj: &subclass::InitializingObject<Self::Type>) {
|
||||
obj.init_template();
|
||||
}
|
||||
|
||||
fn new() -> Self {
|
||||
let pipeline = gst::Pipeline::new(Some(&*PIPELINE_NAME));
|
||||
let sink = gst::ElementFactory::make("gtk4glsink", None).unwrap();
|
||||
let (sender, r) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
let receiver = RefCell::new(Some(r));
|
||||
|
||||
Self {
|
||||
actions: gio::SimpleActionGroup::new(),
|
||||
sink,
|
||||
sender,
|
||||
receiver,
|
||||
pipeline,
|
||||
spinner: TemplateChild::default(),
|
||||
stack: TemplateChild::default(),
|
||||
overlay: TemplateChild::default(),
|
||||
monitor: gst::DeviceMonitor::new(),
|
||||
devices: gio::ListStore::new(gst::Device::static_type()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for Camera {
|
||||
fn constructed(&self, obj: &Self::Type) {
|
||||
obj.init_widgets();
|
||||
obj.init_monitor();
|
||||
self.parent_constructed(obj);
|
||||
}
|
||||
fn dispose(&self, _obj: &Self::Type) {
|
||||
self.monitor.stop();
|
||||
self.pipeline.set_state(gst::State::Null).unwrap();
|
||||
}
|
||||
}
|
||||
impl WidgetImpl for Camera {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct Camera(ObjectSubclass<imp::Camera>) @extends gtk::Widget;
|
||||
}
|
||||
|
||||
impl Camera {
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new(&[]).expect("Failed to create a Camera")
|
||||
}
|
||||
|
||||
fn init_monitor(&self) {
|
||||
let self_ = imp::Camera::from_instance(self);
|
||||
let caps = gst::Caps::new_simple("video/x-raw", &[]);
|
||||
self_.monitor.add_filter(Some("Video/Source"), Some(&caps));
|
||||
|
||||
self_.monitor.start().unwrap();
|
||||
let bus = self_.monitor.get_bus();
|
||||
bus.add_watch_local(clone!(@strong self_.sender as sender => move |_, msg| {
|
||||
use gst::MessageView;
|
||||
match msg.view() {
|
||||
MessageView::DeviceAdded(event) => {
|
||||
let device = event.get_device();
|
||||
send!(sender, CameraEvent::DeviceAdded(device));
|
||||
}
|
||||
MessageView::DeviceRemoved(event) => {
|
||||
let device = event.get_device();
|
||||
send!(sender, CameraEvent::DeviceRemoved(device));
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
glib::Continue(true)
|
||||
}))
|
||||
.expect("Failed to attach a monitor");
|
||||
}
|
||||
|
||||
fn init_pipelines(&self, source_element: gst::Element) {
|
||||
let self_ = imp::Camera::from_instance(self);
|
||||
|
||||
let tee = gst::ElementFactory::make("tee", None).unwrap();
|
||||
let queue = gst::ElementFactory::make("queue", None).unwrap();
|
||||
let videoconvert = gst::ElementFactory::make("videoconvert", None).unwrap();
|
||||
let zbar = gst::ElementFactory::make("zbar", None).unwrap();
|
||||
let fakesink = gst::ElementFactory::make("fakesink", None).unwrap();
|
||||
let queue2 = gst::ElementFactory::make("queue", None).unwrap();
|
||||
let glsinkbin = gst::ElementFactory::make("glsinkbin", None).unwrap();
|
||||
glsinkbin.set_property("sink", &self_.sink).unwrap();
|
||||
|
||||
self_
|
||||
.pipeline
|
||||
.add_many(&[
|
||||
&source_element,
|
||||
&tee,
|
||||
&queue,
|
||||
&videoconvert,
|
||||
&zbar,
|
||||
&fakesink,
|
||||
&queue2,
|
||||
&glsinkbin,
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
gst::Element::link_many(&[
|
||||
&source_element,
|
||||
&tee,
|
||||
&queue,
|
||||
&videoconvert,
|
||||
&zbar,
|
||||
&fakesink,
|
||||
])
|
||||
.unwrap();
|
||||
tee.link_pads(None, &queue2, None).unwrap();
|
||||
gst::Element::link_many(&[&queue2, &glsinkbin]).unwrap();
|
||||
|
||||
let bus = self_.pipeline.get_bus().unwrap();
|
||||
bus.add_watch_local(clone!(@strong self_.sender as sender => move |_, msg| {
|
||||
use gst::MessageView;
|
||||
match msg.view() {
|
||||
MessageView::StateChanged(state) => {
|
||||
if Some(&*PIPELINE_NAME) == state.get_src().map(|s| s.get_name()).as_ref() {
|
||||
let structure = state.get_structure().unwrap();
|
||||
let new_state = structure.get::<gst::State>("new-state")
|
||||
.unwrap().unwrap();
|
||||
if new_state == gst::State::Playing {
|
||||
send!(sender, CameraEvent::StreamStarted);
|
||||
}
|
||||
}
|
||||
}
|
||||
MessageView::Element(e) => {
|
||||
if let Some(s) = e.get_structure() {
|
||||
if let Ok(Some(symbol)) = s.get::<String>("symbol") {
|
||||
send!(sender, CameraEvent::CodeDetected(symbol));
|
||||
}
|
||||
}
|
||||
}
|
||||
MessageView::Error(err) => {
|
||||
error!(
|
||||
"Error from {:?}: {} ({:?})",
|
||||
err.get_src().map(|s| s.get_path_string()),
|
||||
err.get_error(),
|
||||
err.get_debug()
|
||||
);
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
glib::Continue(true)
|
||||
}))
|
||||
.expect("Failed to add bus watch");
|
||||
}
|
||||
|
||||
fn set_state(&self, state: CameraState) {
|
||||
let self_ = imp::Camera::from_instance(self);
|
||||
info!("The camera state changed to {:#?}", state);
|
||||
match state {
|
||||
CameraState::NotFound => {
|
||||
self_.stack.get().set_visible_child_name("not-found");
|
||||
}
|
||||
CameraState::Ready => {
|
||||
self_.stack.get().set_visible_child_name("stream");
|
||||
self_.spinner.get().stop();
|
||||
}
|
||||
CameraState::Loading => {
|
||||
self_.stack.get().set_visible_child_name("loading");
|
||||
self_.spinner.get().start();
|
||||
}
|
||||
CameraState::Paused => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn do_event(&self, event: CameraEvent) -> glib::Continue {
|
||||
let self_ = imp::Camera::from_instance(self);
|
||||
match event {
|
||||
CameraEvent::CodeDetected(code) => {
|
||||
self.emit("code-detected", &[&code]).unwrap();
|
||||
}
|
||||
CameraEvent::DeviceAdded(device) => {
|
||||
// TODO: allow selecting a device and update the sink on the pipeline
|
||||
info!("Camera source added: {}", device.get_display_name());
|
||||
self.set_state(CameraState::Loading);
|
||||
let element = device.create_element(None).unwrap();
|
||||
self.init_pipelines(element);
|
||||
self_.devices.append(&device);
|
||||
}
|
||||
CameraEvent::DeviceRemoved(device) => {
|
||||
info!("Camera source removed: {}", device.get_display_name());
|
||||
self_.devices.append(&device);
|
||||
}
|
||||
CameraEvent::StreamStarted => {
|
||||
self.set_state(CameraState::Ready);
|
||||
}
|
||||
}
|
||||
|
||||
glib::Continue(true)
|
||||
}
|
||||
|
||||
pub fn start(&self) {
|
||||
let self_ = imp::Camera::from_instance(self);
|
||||
self_.pipeline.set_state(gst::State::Playing).unwrap();
|
||||
}
|
||||
|
||||
pub fn stop(&self) {
|
||||
let self_ = imp::Camera::from_instance(self);
|
||||
self.set_state(CameraState::Paused);
|
||||
self_.pipeline.set_state(gst::State::Null).unwrap();
|
||||
}
|
||||
|
||||
pub fn from_screenshot(&self) {
|
||||
let self_ = imp::Camera::from_instance(self);
|
||||
let window = self.get_root().unwrap().downcast::<gtk::Window>().unwrap();
|
||||
screenshot::capture(
|
||||
window,
|
||||
clone!(@strong self_.sender as sender => move |file| {
|
||||
if let Ok(code) = screenshot::scan(&file) {
|
||||
send!(sender, CameraEvent::CodeDetected(code));
|
||||
}
|
||||
}),
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn init_widgets(&self) {
|
||||
let self_ = imp::Camera::from_instance(self);
|
||||
self.set_state(CameraState::NotFound);
|
||||
let receiver = self_.receiver.borrow_mut().take().unwrap();
|
||||
receiver.attach(
|
||||
None,
|
||||
clone!(@weak self as camera => move |action| camera.do_event(action)),
|
||||
);
|
||||
|
||||
let widget = self_
|
||||
.sink
|
||||
.get_property("widget")
|
||||
.unwrap()
|
||||
.get::<gtk::Widget>()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
widget.set_property("force-aspect-ratio", &false).unwrap();
|
||||
self_.overlay.get().set_child(Some(&widget));
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
mod accounts;
|
||||
mod camera;
|
||||
mod preferences;
|
||||
mod providers;
|
||||
mod url_row;
|
||||
|
@ -6,6 +7,7 @@ mod window;
|
|||
|
||||
pub use self::{
|
||||
accounts::{AccountAddDialog, QRCodeData},
|
||||
camera::Camera,
|
||||
preferences::PreferencesWindow,
|
||||
providers::{ProviderImage, ProvidersDialog, ProvidersList},
|
||||
url_row::UrlRow,
|
||||
|
|
Loading…
Add table
Reference in a new issue