Merge pull request #2229 from clarkmoody/custom-qr-style
QR Code Styling
This commit is contained in:
commit
7ee00e751a
6 changed files with 187 additions and 73 deletions
|
|
@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- `hovered` styling for `Svg` widget. [#2163](https://github.com/iced-rs/iced/pull/2163)
|
- `hovered` styling for `Svg` widget. [#2163](https://github.com/iced-rs/iced/pull/2163)
|
||||||
- `height` method for `TextEditor`. [#2221](https://github.com/iced-rs/iced/pull/2221)
|
- `height` method for `TextEditor`. [#2221](https://github.com/iced-rs/iced/pull/2221)
|
||||||
- Customizable style for `TextEditor`. [#2159](https://github.com/iced-rs/iced/pull/2159)
|
- Customizable style for `TextEditor`. [#2159](https://github.com/iced-rs/iced/pull/2159)
|
||||||
|
- Customizable style for `QRCode`. [#2229](https://github.com/iced-rs/iced/pull/2229)
|
||||||
- Border width styling for `Toggler`. [#2219](https://github.com/iced-rs/iced/pull/2219)
|
- Border width styling for `Toggler`. [#2219](https://github.com/iced-rs/iced/pull/2219)
|
||||||
- `RawText` variant for `Primitive` in `iced_graphics`. [#2158](https://github.com/iced-rs/iced/pull/2158)
|
- `RawText` variant for `Primitive` in `iced_graphics`. [#2158](https://github.com/iced-rs/iced/pull/2158)
|
||||||
- `Stream` support for `Command`. [#2150](https://github.com/iced-rs/iced/pull/2150)
|
- `Stream` support for `Command`. [#2150](https://github.com/iced-rs/iced/pull/2150)
|
||||||
|
|
@ -116,6 +117,7 @@ Many thanks to...
|
||||||
- @Calastrophe
|
- @Calastrophe
|
||||||
- @casperstorm
|
- @casperstorm
|
||||||
- @cfrenette
|
- @cfrenette
|
||||||
|
- @clarkmoody
|
||||||
- @Davidster
|
- @Davidster
|
||||||
- @Decodetalkers
|
- @Decodetalkers
|
||||||
- @derezzedex
|
- @derezzedex
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use iced::widget::qr_code::{self, QRCode};
|
use iced::widget::qr_code::{self, QRCode};
|
||||||
use iced::widget::{column, container, text, text_input};
|
use iced::widget::{column, container, pick_list, row, text, text_input};
|
||||||
use iced::{Alignment, Color, Element, Length, Sandbox, Settings};
|
use iced::{Alignment, Element, Length, Sandbox, Settings, Theme};
|
||||||
|
|
||||||
pub fn main() -> iced::Result {
|
pub fn main() -> iced::Result {
|
||||||
QRGenerator::run(Settings::default())
|
QRGenerator::run(Settings::default())
|
||||||
|
|
@ -9,12 +9,14 @@ pub fn main() -> iced::Result {
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct QRGenerator {
|
struct QRGenerator {
|
||||||
data: String,
|
data: String,
|
||||||
qr_code: Option<qr_code::State>,
|
qr_code: Option<qr_code::Data>,
|
||||||
|
theme: Theme,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum Message {
|
enum Message {
|
||||||
DataChanged(String),
|
DataChanged(String),
|
||||||
|
ThemeChanged(Theme),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sandbox for QRGenerator {
|
impl Sandbox for QRGenerator {
|
||||||
|
|
@ -36,18 +38,19 @@ impl Sandbox for QRGenerator {
|
||||||
self.qr_code = if data.is_empty() {
|
self.qr_code = if data.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
qr_code::State::new(&data).ok()
|
qr_code::Data::new(&data).ok()
|
||||||
};
|
};
|
||||||
|
|
||||||
self.data = data;
|
self.data = data;
|
||||||
}
|
}
|
||||||
|
Message::ThemeChanged(theme) => {
|
||||||
|
self.theme = theme;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self) -> Element<Message> {
|
fn view(&self) -> Element<Message> {
|
||||||
let title = text("QR Code Generator")
|
let title = text("QR Code Generator").size(70);
|
||||||
.size(70)
|
|
||||||
.style(Color::from([0.5, 0.5, 0.5]));
|
|
||||||
|
|
||||||
let input =
|
let input =
|
||||||
text_input("Type the data of your QR code here...", &self.data)
|
text_input("Type the data of your QR code here...", &self.data)
|
||||||
|
|
@ -55,7 +58,18 @@ impl Sandbox for QRGenerator {
|
||||||
.size(30)
|
.size(30)
|
||||||
.padding(15);
|
.padding(15);
|
||||||
|
|
||||||
let mut content = column![title, input]
|
let choose_theme = row![
|
||||||
|
text("Theme:"),
|
||||||
|
pick_list(
|
||||||
|
Theme::ALL,
|
||||||
|
Some(self.theme.clone()),
|
||||||
|
Message::ThemeChanged,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.align_items(Alignment::Center);
|
||||||
|
|
||||||
|
let mut content = column![title, input, choose_theme]
|
||||||
.width(700)
|
.width(700)
|
||||||
.spacing(20)
|
.spacing(20)
|
||||||
.align_items(Alignment::Center);
|
.align_items(Alignment::Center);
|
||||||
|
|
@ -72,4 +86,8 @@ impl Sandbox for QRGenerator {
|
||||||
.center_y()
|
.center_y()
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn theme(&self) -> Theme {
|
||||||
|
self.theme.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ pub mod menu;
|
||||||
pub mod pane_grid;
|
pub mod pane_grid;
|
||||||
pub mod pick_list;
|
pub mod pick_list;
|
||||||
pub mod progress_bar;
|
pub mod progress_bar;
|
||||||
|
pub mod qr_code;
|
||||||
pub mod radio;
|
pub mod radio;
|
||||||
pub mod rule;
|
pub mod rule;
|
||||||
pub mod scrollable;
|
pub mod scrollable;
|
||||||
|
|
|
||||||
20
style/src/qr_code.rs
Normal file
20
style/src/qr_code.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
//! Change the appearance of a QR code.
|
||||||
|
use crate::core::Color;
|
||||||
|
|
||||||
|
/// The appearance of a QR code.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Appearance {
|
||||||
|
/// The color of the QR code data cells
|
||||||
|
pub cell: Color,
|
||||||
|
/// The color of the QR code background
|
||||||
|
pub background: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A set of rules that dictate the style of a QR code.
|
||||||
|
pub trait StyleSheet {
|
||||||
|
/// The supported style of the [`StyleSheet`].
|
||||||
|
type Style: Default;
|
||||||
|
|
||||||
|
/// Produces the style of a QR code.
|
||||||
|
fn appearance(&self, style: &Self::Style) -> Appearance;
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ use crate::menu;
|
||||||
use crate::pane_grid;
|
use crate::pane_grid;
|
||||||
use crate::pick_list;
|
use crate::pick_list;
|
||||||
use crate::progress_bar;
|
use crate::progress_bar;
|
||||||
|
use crate::qr_code;
|
||||||
use crate::radio;
|
use crate::radio;
|
||||||
use crate::rule;
|
use crate::rule;
|
||||||
use crate::scrollable;
|
use crate::scrollable;
|
||||||
|
|
@ -956,6 +957,46 @@ impl<T: Fn(&Theme) -> progress_bar::Appearance> progress_bar::StyleSheet for T {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The style of a QR Code.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub enum QRCode {
|
||||||
|
/// The default style.
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
/// A custom style.
|
||||||
|
Custom(Box<dyn qr_code::StyleSheet<Style = Theme>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Fn(&Theme) -> qr_code::Appearance + 'static> From<T> for QRCode {
|
||||||
|
fn from(f: T) -> Self {
|
||||||
|
Self::Custom(Box::new(f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl qr_code::StyleSheet for Theme {
|
||||||
|
type Style = QRCode;
|
||||||
|
|
||||||
|
fn appearance(&self, style: &Self::Style) -> qr_code::Appearance {
|
||||||
|
let palette = self.palette();
|
||||||
|
|
||||||
|
match style {
|
||||||
|
QRCode::Default => qr_code::Appearance {
|
||||||
|
cell: palette.text,
|
||||||
|
background: palette.background,
|
||||||
|
},
|
||||||
|
QRCode::Custom(custom) => custom.appearance(self),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Fn(&Theme) -> qr_code::Appearance> qr_code::StyleSheet for T {
|
||||||
|
type Style = Theme;
|
||||||
|
|
||||||
|
fn appearance(&self, style: &Self::Style) -> qr_code::Appearance {
|
||||||
|
(self)(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The style of a rule.
|
/// The style of a rule.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub enum Rule {
|
pub enum Rule {
|
||||||
|
|
|
||||||
|
|
@ -3,53 +3,71 @@ use crate::canvas;
|
||||||
use crate::core::layout;
|
use crate::core::layout;
|
||||||
use crate::core::mouse;
|
use crate::core::mouse;
|
||||||
use crate::core::renderer::{self, Renderer as _};
|
use crate::core::renderer::{self, Renderer as _};
|
||||||
use crate::core::widget::Tree;
|
use crate::core::widget::tree::{self, Tree};
|
||||||
use crate::core::{
|
use crate::core::{
|
||||||
Color, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget,
|
Element, Layout, Length, Point, Rectangle, Size, Vector, Widget,
|
||||||
};
|
};
|
||||||
use crate::graphics::geometry::Renderer as _;
|
use crate::graphics::geometry::Renderer as _;
|
||||||
use crate::Renderer;
|
use crate::Renderer;
|
||||||
|
|
||||||
|
use std::cell::RefCell;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub use crate::style::qr_code::{Appearance, StyleSheet};
|
||||||
|
|
||||||
const DEFAULT_CELL_SIZE: u16 = 4;
|
const DEFAULT_CELL_SIZE: u16 = 4;
|
||||||
const QUIET_ZONE: usize = 2;
|
const QUIET_ZONE: usize = 2;
|
||||||
|
|
||||||
/// A type of matrix barcode consisting of squares arranged in a grid which
|
/// A type of matrix barcode consisting of squares arranged in a grid which
|
||||||
/// can be read by an imaging device, such as a camera.
|
/// can be read by an imaging device, such as a camera.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct QRCode<'a> {
|
pub struct QRCode<'a, Theme = crate::Theme>
|
||||||
state: &'a State,
|
where
|
||||||
dark: Color,
|
Theme: StyleSheet,
|
||||||
light: Color,
|
{
|
||||||
|
data: &'a Data,
|
||||||
cell_size: u16,
|
cell_size: u16,
|
||||||
|
style: Theme::Style,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> QRCode<'a> {
|
impl<'a, Theme> QRCode<'a, Theme>
|
||||||
/// Creates a new [`QRCode`] with the provided [`State`].
|
where
|
||||||
pub fn new(state: &'a State) -> Self {
|
Theme: StyleSheet,
|
||||||
|
{
|
||||||
|
/// Creates a new [`QRCode`] with the provided [`Data`].
|
||||||
|
pub fn new(data: &'a Data) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
data,
|
||||||
cell_size: DEFAULT_CELL_SIZE,
|
cell_size: DEFAULT_CELL_SIZE,
|
||||||
dark: Color::BLACK,
|
style: Default::default(),
|
||||||
light: Color::WHITE,
|
|
||||||
state,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets both the dark and light [`Color`]s of the [`QRCode`].
|
|
||||||
pub fn color(mut self, dark: Color, light: Color) -> Self {
|
|
||||||
self.dark = dark;
|
|
||||||
self.light = light;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the size of the squares of the grid cell of the [`QRCode`].
|
/// Sets the size of the squares of the grid cell of the [`QRCode`].
|
||||||
pub fn cell_size(mut self, cell_size: u16) -> Self {
|
pub fn cell_size(mut self, cell_size: u16) -> Self {
|
||||||
self.cell_size = cell_size;
|
self.cell_size = cell_size;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the style of the [`QRCode`].
|
||||||
|
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self {
|
||||||
|
self.style = style.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Message, Theme> Widget<Message, Theme, Renderer> for QRCode<'a> {
|
impl<'a, Message, Theme> Widget<Message, Theme, Renderer> for QRCode<'a, Theme>
|
||||||
|
where
|
||||||
|
Theme: StyleSheet,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
tree::Tag::of::<State>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
tree::State::new(State::default())
|
||||||
|
}
|
||||||
|
|
||||||
fn size(&self) -> Size<Length> {
|
fn size(&self) -> Size<Length> {
|
||||||
Size {
|
Size {
|
||||||
width: Length::Shrink,
|
width: Length::Shrink,
|
||||||
|
|
@ -63,7 +81,7 @@ impl<'a, Message, Theme> Widget<Message, Theme, Renderer> for QRCode<'a> {
|
||||||
_renderer: &Renderer,
|
_renderer: &Renderer,
|
||||||
_limits: &layout::Limits,
|
_limits: &layout::Limits,
|
||||||
) -> layout::Node {
|
) -> layout::Node {
|
||||||
let side_length = (self.state.width + 2 * QUIET_ZONE) as f32
|
let side_length = (self.data.width + 2 * QUIET_ZONE) as f32
|
||||||
* f32::from(self.cell_size);
|
* f32::from(self.cell_size);
|
||||||
|
|
||||||
layout::Node::new(Size::new(side_length, side_length))
|
layout::Node::new(Size::new(side_length, side_length))
|
||||||
|
|
@ -71,53 +89,60 @@ impl<'a, Message, Theme> Widget<Message, Theme, Renderer> for QRCode<'a> {
|
||||||
|
|
||||||
fn draw(
|
fn draw(
|
||||||
&self,
|
&self,
|
||||||
_state: &Tree,
|
tree: &Tree,
|
||||||
renderer: &mut Renderer,
|
renderer: &mut Renderer,
|
||||||
_theme: &Theme,
|
theme: &Theme,
|
||||||
_style: &renderer::Style,
|
_style: &renderer::Style,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'_>,
|
||||||
_cursor: mouse::Cursor,
|
_cursor: mouse::Cursor,
|
||||||
_viewport: &Rectangle,
|
_viewport: &Rectangle,
|
||||||
) {
|
) {
|
||||||
|
let state = tree.state.downcast_ref::<State>();
|
||||||
|
|
||||||
let bounds = layout.bounds();
|
let bounds = layout.bounds();
|
||||||
let side_length = self.state.width + 2 * QUIET_ZONE;
|
let side_length = self.data.width + 2 * QUIET_ZONE;
|
||||||
|
|
||||||
|
let appearance = theme.appearance(&self.style);
|
||||||
|
let mut last_appearance = state.last_appearance.borrow_mut();
|
||||||
|
|
||||||
|
if Some(appearance) != *last_appearance {
|
||||||
|
self.data.cache.clear();
|
||||||
|
|
||||||
|
*last_appearance = Some(appearance);
|
||||||
|
}
|
||||||
|
|
||||||
// Reuse cache if possible
|
// Reuse cache if possible
|
||||||
let geometry =
|
let geometry = self.data.cache.draw(renderer, bounds.size(), |frame| {
|
||||||
self.state.cache.draw(renderer, bounds.size(), |frame| {
|
// Scale units to cell size
|
||||||
// Scale units to cell size
|
frame.scale(self.cell_size);
|
||||||
frame.scale(self.cell_size);
|
|
||||||
|
|
||||||
// Draw background
|
// Draw background
|
||||||
frame.fill_rectangle(
|
frame.fill_rectangle(
|
||||||
Point::ORIGIN,
|
Point::ORIGIN,
|
||||||
Size::new(side_length as f32, side_length as f32),
|
Size::new(side_length as f32, side_length as f32),
|
||||||
self.light,
|
appearance.background,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Avoid drawing on the quiet zone
|
// Avoid drawing on the quiet zone
|
||||||
frame.translate(Vector::new(
|
frame.translate(Vector::new(QUIET_ZONE as f32, QUIET_ZONE as f32));
|
||||||
QUIET_ZONE as f32,
|
|
||||||
QUIET_ZONE as f32,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Draw contents
|
// Draw contents
|
||||||
self.state
|
self.data
|
||||||
.contents
|
.contents
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter(|(_, value)| **value == qrcode::Color::Dark)
|
.filter(|(_, value)| **value == qrcode::Color::Dark)
|
||||||
.for_each(|(index, _)| {
|
.for_each(|(index, _)| {
|
||||||
let row = index / self.state.width;
|
let row = index / self.data.width;
|
||||||
let column = index % self.state.width;
|
let column = index % self.data.width;
|
||||||
|
|
||||||
frame.fill_rectangle(
|
frame.fill_rectangle(
|
||||||
Point::new(column as f32, row as f32),
|
Point::new(column as f32, row as f32),
|
||||||
Size::UNIT,
|
Size::UNIT,
|
||||||
self.dark,
|
appearance.cell,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
renderer.with_translation(
|
renderer.with_translation(
|
||||||
bounds.position() - Point::ORIGIN,
|
bounds.position() - Point::ORIGIN,
|
||||||
|
|
@ -128,26 +153,28 @@ impl<'a, Message, Theme> Widget<Message, Theme, Renderer> for QRCode<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Message, Theme> From<QRCode<'a>>
|
impl<'a, Message, Theme> From<QRCode<'a, Theme>>
|
||||||
for Element<'a, Message, Theme, Renderer>
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: StyleSheet + 'a,
|
||||||
{
|
{
|
||||||
fn from(qr_code: QRCode<'a>) -> Self {
|
fn from(qr_code: QRCode<'a, Theme>) -> Self {
|
||||||
Self::new(qr_code)
|
Self::new(qr_code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The state of a [`QRCode`].
|
/// The data of a [`QRCode`].
|
||||||
///
|
///
|
||||||
/// It stores the data that will be displayed.
|
/// It stores the contents that will be displayed.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct State {
|
pub struct Data {
|
||||||
contents: Vec<qrcode::Color>,
|
contents: Vec<qrcode::Color>,
|
||||||
width: usize,
|
width: usize,
|
||||||
cache: canvas::Cache,
|
cache: canvas::Cache,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl Data {
|
||||||
/// Creates a new [`State`] with the provided data.
|
/// Creates a new [`Data`] with the provided data.
|
||||||
///
|
///
|
||||||
/// This method uses an [`ErrorCorrection::Medium`] and chooses the smallest
|
/// This method uses an [`ErrorCorrection::Medium`] and chooses the smallest
|
||||||
/// size to display the data.
|
/// size to display the data.
|
||||||
|
|
@ -157,7 +184,7 @@ impl State {
|
||||||
Ok(Self::build(encoded))
|
Ok(Self::build(encoded))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new [`State`] with the provided [`ErrorCorrection`].
|
/// Creates a new [`Data`] with the provided [`ErrorCorrection`].
|
||||||
pub fn with_error_correction(
|
pub fn with_error_correction(
|
||||||
data: impl AsRef<[u8]>,
|
data: impl AsRef<[u8]>,
|
||||||
error_correction: ErrorCorrection,
|
error_correction: ErrorCorrection,
|
||||||
|
|
@ -170,7 +197,7 @@ impl State {
|
||||||
Ok(Self::build(encoded))
|
Ok(Self::build(encoded))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new [`State`] with the provided [`Version`] and
|
/// Creates a new [`Data`] with the provided [`Version`] and
|
||||||
/// [`ErrorCorrection`].
|
/// [`ErrorCorrection`].
|
||||||
pub fn with_version(
|
pub fn with_version(
|
||||||
data: impl AsRef<[u8]>,
|
data: impl AsRef<[u8]>,
|
||||||
|
|
@ -249,7 +276,7 @@ impl From<ErrorCorrection> for qrcode::EcLevel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An error that occurred when building a [`State`] for a [`QRCode`].
|
/// An error that occurred when building a [`Data`] for a [`QRCode`].
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
/// The data is too long to encode in a QR code for the chosen [`Version`].
|
/// The data is too long to encode in a QR code for the chosen [`Version`].
|
||||||
|
|
@ -298,3 +325,8 @@ impl From<qrcode::types::QrError> for Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct State {
|
||||||
|
last_appearance: RefCell<Option<Appearance>>,
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue