0.7.1: Sea codec, Slint client redesign
Signed-off-by: Ivan Bushchik <ivabus@ivabus.dev>
2802
Cargo.lock
generated
|
@ -10,7 +10,7 @@ members = [
|
||||||
[package]
|
[package]
|
||||||
name = "lonelyradio"
|
name = "lonelyradio"
|
||||||
description = "TCP radio for lonely ones"
|
description = "TCP radio for lonely ones"
|
||||||
version = "0.7.0"
|
version = "0.7.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
|
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
|
||||||
|
@ -49,14 +49,16 @@ samplerate = "0.2.4"
|
||||||
flacenc = { version = "0.4.0", default-features = false, optional = true }
|
flacenc = { version = "0.4.0", default-features = false, optional = true }
|
||||||
alac-encoder = { version = "0.3.0", optional = true }
|
alac-encoder = { version = "0.3.0", optional = true }
|
||||||
vorbis_rs = {version = "0.5.4", optional = true }
|
vorbis_rs = {version = "0.5.4", optional = true }
|
||||||
|
sea-codec = { version = "0.5.2", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["all-lossless", "all-lossy"]
|
default = ["all-lossless", "all-lossy"]
|
||||||
all-lossless = ["alac", "flac"]
|
all-lossless = ["alac", "flac"]
|
||||||
all-lossy = ["vorbis"]
|
all-lossy = ["vorbis", "sea"]
|
||||||
alac = ["dep:alac-encoder"]
|
alac = ["dep:alac-encoder"]
|
||||||
flac = ["dep:flacenc"]
|
flac = ["dep:flacenc"]
|
||||||
vorbis = ["dep:vorbis_rs"]
|
vorbis = ["dep:vorbis_rs"]
|
||||||
|
sea = ["dep:sea-codec"]
|
||||||
|
|
||||||
[profile.distribute]
|
[profile.distribute]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
# Autogenerated by docker init
|
# Autogenerated by docker init
|
||||||
# https://docs.docker.com/go/dockerfile-reference/
|
# https://docs.docker.com/go/dockerfile-reference/
|
||||||
|
|
||||||
ARG RUST_VERSION=1.80.1
|
ARG RUST_VERSION=1.85
|
||||||
ARG APP_NAME=lonelyradio
|
ARG APP_NAME=lonelyradio
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
|
@ -4,12 +4,12 @@ Shuffles through your [XSPF playlists](https://www.xspf.org) or your entire libr
|
||||||
|
|
||||||
Decodes audio streams using [symphonia](https://github.com/pdeljanov/Symphonia) (supported [decoders](https://github.com/pdeljanov/Symphonia?tab=readme-ov-file#codecs-decoders) and [demuxers](https://github.com/pdeljanov/Symphonia?tab=readme-ov-file#formats-demuxers))
|
Decodes audio streams using [symphonia](https://github.com/pdeljanov/Symphonia) (supported [decoders](https://github.com/pdeljanov/Symphonia?tab=readme-ov-file#codecs-decoders) and [demuxers](https://github.com/pdeljanov/Symphonia?tab=readme-ov-file#formats-demuxers))
|
||||||
|
|
||||||
Streams music using [FLAC](https://crates.io/crates/flacenc), [ALAC](https://crates.io/crates/alac-encoder), [Vorbis](https://crates.io/crates/vorbis_rs) or raw PCM on client’s requests.
|
Streams music using [FLAC](https://crates.io/crates/flacenc), [ALAC](https://crates.io/crates/alac-encoder), [Vorbis](https://crates.io/crates/vorbis_rs), [Sea](https://github.com/Daninet/sea-codec) or raw PCM on client’s requests.
|
||||||
|
|
||||||
### Install server
|
### Install server
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.7.0 lonelyradio
|
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.7.1 lonelyradio
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run
|
### Run
|
||||||
|
@ -57,7 +57,7 @@ Only the `<location>` and (playlist's) element would be used and only `file://`
|
||||||
##### Install
|
##### Install
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.7.0 monoclient-s
|
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.7.1 monoclient-s
|
||||||
```
|
```
|
||||||
|
|
||||||
You may need to install some dependencies for Slint.
|
You may need to install some dependencies for Slint.
|
||||||
|
@ -71,7 +71,7 @@ Desktop integration will be added later.
|
||||||
##### Install monoclient
|
##### Install monoclient
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.7.0 monoclient
|
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.7.1 monoclient
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Usage
|
#### Usage
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub const HELLO_MAGIC: &[u8; 8] = b"lonelyra";
|
pub const HELLO_MAGIC: &[u8; 8] = b"lonelyra";
|
||||||
|
@ -99,6 +101,7 @@ pub enum Encoder {
|
||||||
Opus = 5,
|
Opus = 5,
|
||||||
Aac = 6,
|
Aac = 6,
|
||||||
Vorbis = 7,
|
Vorbis = 7,
|
||||||
|
Sea = 8,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
|
@ -119,3 +122,19 @@ pub struct FragmentMetadata {
|
||||||
fn none() -> Option<Vec<u8>> {
|
fn none() -> Option<Vec<u8>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for Encoder {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
Self::Pcm16 => "PCM s16",
|
||||||
|
Self::PcmFloat => "PCM f32",
|
||||||
|
Self::Flac => "FLAC",
|
||||||
|
Self::Alac => "ALAC",
|
||||||
|
Self::WavPack => "WavPack",
|
||||||
|
Self::Opus => "Opus",
|
||||||
|
Self::Aac => "AAC",
|
||||||
|
Self::Vorbis => "Vorbis",
|
||||||
|
Self::Sea => "Sea",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,16 +3,20 @@ name = "monoclient-s"
|
||||||
description = "Client for lonelyradio built with Slint"
|
description = "Client for lonelyradio built with Slint"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
build = "build.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
slint = { version = "1.6" }
|
slint = { version = "1.8" }
|
||||||
monolib = { path = "../monolib", version = "0.7.1" }
|
monolib = { path = "../monolib", version = "0.7.1" }
|
||||||
zune-jpeg = "0.4.13"
|
zune-jpeg = "0.4.13"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
slint-build = "1.8"
|
||||||
|
|
||||||
[package.metadata.bundle]
|
[package.metadata.bundle]
|
||||||
name = "monoclient-s"
|
name = "monoclient-s"
|
||||||
identifier = "dev.ivabus.monoclient-s"
|
identifier = "dev.ivabus.monoclient-s"
|
||||||
icon = ["lonelyradio.png", "lonelyradio.icns"]
|
icon = ["lonelyradio.png", "lonelyradio.icns"]
|
||||||
version = "0.5.0"
|
version = "0.7.1"
|
||||||
copyright = "Copyright (c) 2024 Ivan Bushchik."
|
copyright = "Copyright (c) 2024 Ivan Bushchik."
|
||||||
category = "Music"
|
category = "Music"
|
||||||
|
|
3
monoclient-s/build.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
fn main() {
|
||||||
|
slint_build::compile("ui/ui.slint").unwrap();
|
||||||
|
}
|
|
@ -6,146 +6,20 @@ use slint::{
|
||||||
Image, ModelRc, Rgb8Pixel, Rgba8Pixel, SharedPixelBuffer, SharedString, VecModel, Weak,
|
Image, ModelRc, Rgb8Pixel, Rgba8Pixel, SharedPixelBuffer, SharedString, VecModel, Weak,
|
||||||
};
|
};
|
||||||
|
|
||||||
slint::slint! {
|
slint::include_modules!();
|
||||||
import { ComboBox, Button, VerticalBox, GroupBox, Slider } from "std-widgets.slint";
|
|
||||||
export component MainWindow inherits Window {
|
|
||||||
max-height: self.preferred-height;
|
|
||||||
callback play;
|
|
||||||
callback stop;
|
|
||||||
callback next;
|
|
||||||
callback refreshp;
|
|
||||||
callback change_volume(float);
|
|
||||||
callback text_edited;
|
|
||||||
|
|
||||||
in-out property <string> addr: address.text;
|
|
||||||
in-out property <string> mtitle: "";
|
|
||||||
in-out property <string> malbum: "";
|
|
||||||
in-out property <string> martist: "";
|
|
||||||
in-out property <float> volume: svolume.value;
|
|
||||||
in-out property <bool> start_enabled: false;
|
|
||||||
in-out property <bool> playing: false;
|
|
||||||
in-out property <bool> paused: false;
|
|
||||||
in property <image> cover: @image-url("lonelyradio.png");
|
|
||||||
in property <[string]> playlists: ["All tracks"];
|
|
||||||
in-out property <string> selected_playlist: selected.current-value;
|
|
||||||
|
|
||||||
title: "monoclient-s";
|
|
||||||
min-width: 192px;
|
|
||||||
max-width: 768px;
|
|
||||||
VerticalBox {
|
|
||||||
alignment: center;
|
|
||||||
spacing: 0px;
|
|
||||||
|
|
||||||
Image {
|
|
||||||
source: cover;
|
|
||||||
max-height: 192px;
|
|
||||||
max-width: 192px;
|
|
||||||
min-height: 192px;
|
|
||||||
min-width: 192px;
|
|
||||||
}
|
|
||||||
|
|
||||||
GroupBox{
|
|
||||||
max-width: 768px;
|
|
||||||
address := TextInput {
|
|
||||||
text: "";
|
|
||||||
horizontal-alignment: center;
|
|
||||||
height: 1.25rem;
|
|
||||||
|
|
||||||
accepted => {
|
|
||||||
self.clear_focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
edited => {
|
|
||||||
text_edited();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VerticalLayout {
|
|
||||||
max-width: 512px;
|
|
||||||
|
|
||||||
VerticalLayout {
|
|
||||||
spacing: 4px;
|
|
||||||
Button {
|
|
||||||
max-width: 256px;
|
|
||||||
text: playing ? (paused ? "Play" : "Pause") : "Start";
|
|
||||||
enabled: start_enabled || playing;
|
|
||||||
clicked => {
|
|
||||||
play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HorizontalLayout {
|
|
||||||
spacing: 4px;
|
|
||||||
max-width: 256px;
|
|
||||||
Button {
|
|
||||||
text: "Stop";
|
|
||||||
enabled: playing && !paused;
|
|
||||||
clicked => {
|
|
||||||
stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
text: "Next";
|
|
||||||
enabled: playing && !paused;
|
|
||||||
clicked => {
|
|
||||||
next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HorizontalLayout {
|
|
||||||
selected := ComboBox {
|
|
||||||
model: playlists;
|
|
||||||
current-value: "All tracks";
|
|
||||||
selected() => {
|
|
||||||
self.clear_focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
svolume := Slider {
|
|
||||||
value: 255;
|
|
||||||
maximum: 255;
|
|
||||||
changed(f) => {
|
|
||||||
change_volume(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VerticalLayout {
|
|
||||||
padding: 4px;
|
|
||||||
tartist := Text {
|
|
||||||
height: 1.25rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text: martist;
|
|
||||||
overflow: elide;
|
|
||||||
}
|
|
||||||
talbum := Text {
|
|
||||||
height: 1.25rem;
|
|
||||||
text: malbum;
|
|
||||||
overflow: elide;
|
|
||||||
}
|
|
||||||
ttitle := Text {
|
|
||||||
height: 1.25rem;
|
|
||||||
text: mtitle;
|
|
||||||
overflow: elide;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn start_playback(window_weak: Weak<MainWindow>) {
|
fn start_playback(window_weak: Weak<MainWindow>) {
|
||||||
let window = window_weak.upgrade().unwrap();
|
let window = window_weak.upgrade().unwrap();
|
||||||
let addr = window.get_addr().to_string();
|
let addr = window.get_addr().to_string();
|
||||||
let playlist = window.get_selected_playlist();
|
let playlist = window.get_selected_playlist();
|
||||||
|
let encoder = monolib::SUPPORTED_DECODERS[window.get_selected_encoder() as usize];
|
||||||
let handle = std::thread::spawn(move || {
|
let handle = std::thread::spawn(move || {
|
||||||
monolib::run(
|
monolib::run(
|
||||||
&addr,
|
&addr,
|
||||||
lonelyradio_types::Settings {
|
lonelyradio_types::Settings {
|
||||||
encoder: lonelyradio_types::Encoder::Flac,
|
encoder,
|
||||||
cover: 512,
|
cover: 2048,
|
||||||
},
|
},
|
||||||
if playlist == "All tracks" {
|
if playlist == "All tracks" {
|
||||||
""
|
""
|
||||||
|
@ -206,6 +80,14 @@ pub fn main() {
|
||||||
)));
|
)));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.set_supported_encoders(ModelRc::new(VecModel::from(
|
||||||
|
monolib::SUPPORTED_DECODERS
|
||||||
|
.iter()
|
||||||
|
.map(|x| x.to_string())
|
||||||
|
.map(SharedString::from)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)));
|
||||||
|
|
||||||
let window_weak = window.as_weak();
|
let window_weak = window.as_weak();
|
||||||
window.on_next(move || {
|
window.on_next(move || {
|
||||||
monolib::stop();
|
monolib::stop();
|
||||||
|
|
20
monoclient-s/ui/icons/LICENSE
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Ivan Bushchik
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished
|
||||||
|
to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
|
||||||
|
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
||||||
|
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
5
monoclient-s/ui/icons/README.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# Monoclient Icons Set
|
||||||
|
|
||||||
|
Copyright (c) 2024 Ivan Bushchik
|
||||||
|
|
||||||
|
License: [MIT](./LICENSE)
|
69
monoclient-s/ui/icons/eject.svg
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="10mm"
|
||||||
|
height="10mm"
|
||||||
|
viewBox="0 0 10 10"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
||||||
|
sodipodi:docname="eject.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff00"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:zoom="13.894537"
|
||||||
|
inkscape:cx="13.674439"
|
||||||
|
inkscape:cy="19.288156"
|
||||||
|
inkscape:window-width="1536"
|
||||||
|
inkscape:window-height="1041"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="43"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Слой 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<rect
|
||||||
|
style="fill:#000000;stroke-width:0"
|
||||||
|
id="rect2"
|
||||||
|
width="7.5"
|
||||||
|
height="1.5"
|
||||||
|
x="1.25"
|
||||||
|
y="7.8000002"
|
||||||
|
rx="0.75"
|
||||||
|
ry="0.75" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="fill:#000000;stroke-width:0"
|
||||||
|
id="path3"
|
||||||
|
inkscape:flatsided="true"
|
||||||
|
sodipodi:sides="3"
|
||||||
|
sodipodi:cx="1.7518876"
|
||||||
|
sodipodi:cy="1.4281693"
|
||||||
|
sodipodi:r1="1.9958065"
|
||||||
|
sodipodi:r2="3.9916129"
|
||||||
|
sodipodi:arg1="0.51914611"
|
||||||
|
sodipodi:arg2="1.5663437"
|
||||||
|
inkscape:rounded="0.07"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 3.4847328,2.4183665 C 3.3646778,2.6284629 0.14985015,2.6427775 0.02792892,2.4337586 -0.09399232,2.2247398 1.5010247,-0.56653997 1.7430009,-0.56761742 1.9849772,-0.56869487 3.6047879,2.2082702 3.4847328,2.4183665 Z"
|
||||||
|
inkscape:transform-center-x="-0.017735456"
|
||||||
|
inkscape:transform-center-y="-1.8312623"
|
||||||
|
transform="matrix(2.0174137,0,0,2.0174137,1.4570468,2.1451198)" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
89
monoclient-s/ui/icons/first.svg
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="10mm"
|
||||||
|
height="10mm"
|
||||||
|
viewBox="0 0 10 10"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
||||||
|
sodipodi:docname="first.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:zoom="14.983016"
|
||||||
|
inkscape:cx="18.621084"
|
||||||
|
inkscape:cy="16.618817"
|
||||||
|
inkscape:window-width="1712"
|
||||||
|
inkscape:window-height="1013"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="43"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Слой 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<g
|
||||||
|
id="g2"
|
||||||
|
transform="matrix(-1,0,0,1,9.9524816,0.592)">
|
||||||
|
<g
|
||||||
|
id="g1">
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:1;stroke-opacity:1"
|
||||||
|
id="path1"
|
||||||
|
sodipodi:type="arc"
|
||||||
|
sodipodi:cx="5.2266302"
|
||||||
|
sodipodi:cy="5.0154533"
|
||||||
|
sodipodi:rx="3"
|
||||||
|
sodipodi:ry="3"
|
||||||
|
sodipodi:start="1.5707963"
|
||||||
|
sodipodi:end="4.712389"
|
||||||
|
sodipodi:arc-type="arc"
|
||||||
|
d="M 5.2266302,8.0154533 A 3,3 0 0 1 2.628554,6.5154534 a 3,3 0 0 1 0,-3.0000001 3,3 0 0 1 2.5980762,-1.5"
|
||||||
|
sodipodi:open="true" />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="fill:#000000;stroke-width:0"
|
||||||
|
id="path3"
|
||||||
|
inkscape:flatsided="true"
|
||||||
|
sodipodi:sides="3"
|
||||||
|
sodipodi:cx="1.7518876"
|
||||||
|
sodipodi:cy="1.4281693"
|
||||||
|
sodipodi:r1="1.9958065"
|
||||||
|
sodipodi:r2="3.9916129"
|
||||||
|
sodipodi:arg1="0.51914611"
|
||||||
|
sodipodi:arg2="1.5663437"
|
||||||
|
inkscape:rounded="0.07"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 3.4847328,2.4183665 C 3.3646778,2.6284629 0.14985015,2.6427775 0.02792892,2.4337586 -0.09399232,2.2247398 1.5010247,-0.56653997 1.7430009,-0.56761742 1.9849772,-0.56869487 3.6047879,2.2082702 3.4847328,2.4183665 Z"
|
||||||
|
inkscape:transform-center-x="-0.9156312"
|
||||||
|
inkscape:transform-center-y="0.008867619"
|
||||||
|
transform="matrix(0,1.0087068,-1.0087069,0,7.6059217,0.2785234)" />
|
||||||
|
<rect
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-opacity:1"
|
||||||
|
id="rect1"
|
||||||
|
width="2"
|
||||||
|
height="1"
|
||||||
|
x="4.7523584"
|
||||||
|
y="7.5159998"
|
||||||
|
rx="0.5"
|
||||||
|
ry="0.5" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.9 KiB |
60
monoclient-s/ui/icons/gear.svg
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="10mm"
|
||||||
|
height="10mm"
|
||||||
|
viewBox="0 0 10 10"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
|
||||||
|
sodipodi:docname="gear.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff00"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:zoom="13.894537"
|
||||||
|
inkscape:cx="13.74641"
|
||||||
|
inkscape:cy="19.288156"
|
||||||
|
inkscape:window-width="1536"
|
||||||
|
inkscape:window-height="1013"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="43"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Слой 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="fill:#000000;stroke-width:1"
|
||||||
|
id="path1"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
sodipodi:sides="7"
|
||||||
|
sodipodi:cx="2.2279439"
|
||||||
|
sodipodi:cy="0.64743674"
|
||||||
|
sodipodi:r1="5.8304453"
|
||||||
|
sodipodi:r2="4.3145294"
|
||||||
|
sodipodi:arg1="0.77846987"
|
||||||
|
sodipodi:arg2="1.2272688"
|
||||||
|
inkscape:rounded="0.68"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 6.3791557,4.7415218 C 5.0907843,6.0478706 5.4087092,4.0919025 3.6811232,4.7098786 1.953537,5.3278548 3.4399287,6.6383994 1.6152975,6.4456051 -0.20933355,6.2528109 1.5181269,5.2818485 -0.04215863,4.3164691 -1.6024442,3.3510897 -1.7003191,5.3303087 -2.6872255,3.7835494 -3.6741317,2.2367901 -1.8379488,2.9819879 -2.0560069,1.1602034 -2.274065,-0.66158125 -3.8825048,0.49591998 -3.2885257,-1.2400632 c 0.5939791,-1.735983 1.1562014,0.1642259 2.44457285,-1.1421229 1.28837157,-1.3063488 -0.61944515,-1.8421875 1.10814107,-2.4601637 1.72758608,-0.6179761 0.59248281,1.0063478 2.41711388,1.199142 1.8246312,0.1927943 1.0540625,-1.6328868 2.6143481,-0.6675074 1.5602855,0.9653794 -0.4173874,1.0906693 0.5695189,2.6374286 0.9869063,1.54675933 1.9338395,-0.1939891 2.1518976,1.62779553 0.2180582,1.82178457 -1.1129563,0.3536946 -1.7069354,2.08967757 -0.5939791,1.7359832 1.357396,1.3909864 0.069024,2.6973353 z"
|
||||||
|
inkscape:transform-center-x="0.47254043"
|
||||||
|
inkscape:transform-center-y="-0.49742099"
|
||||||
|
transform="matrix(0.6544318,0,0,0.6544318,3.4877435,4.5192689)" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.6 KiB |
89
monoclient-s/ui/icons/last.svg
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="10mm"
|
||||||
|
height="10mm"
|
||||||
|
viewBox="0 0 10 10"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
||||||
|
sodipodi:docname="last.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:zoom="14.983016"
|
||||||
|
inkscape:cx="18.621084"
|
||||||
|
inkscape:cy="16.618817"
|
||||||
|
inkscape:window-width="1712"
|
||||||
|
inkscape:window-height="1013"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="43"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Слой 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<g
|
||||||
|
id="g2"
|
||||||
|
transform="translate(0.04706873,0.592)">
|
||||||
|
<g
|
||||||
|
id="g1">
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:1;stroke-opacity:1"
|
||||||
|
id="path1"
|
||||||
|
sodipodi:type="arc"
|
||||||
|
sodipodi:cx="5.2266302"
|
||||||
|
sodipodi:cy="5.0154533"
|
||||||
|
sodipodi:rx="3"
|
||||||
|
sodipodi:ry="3"
|
||||||
|
sodipodi:start="1.5707963"
|
||||||
|
sodipodi:end="4.712389"
|
||||||
|
sodipodi:arc-type="arc"
|
||||||
|
d="M 5.2266302,8.0154533 A 3,3 0 0 1 2.628554,6.5154534 a 3,3 0 0 1 0,-3.0000001 3,3 0 0 1 2.5980762,-1.5"
|
||||||
|
sodipodi:open="true" />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="fill:#000000;stroke-width:0"
|
||||||
|
id="path3"
|
||||||
|
inkscape:flatsided="true"
|
||||||
|
sodipodi:sides="3"
|
||||||
|
sodipodi:cx="1.7518876"
|
||||||
|
sodipodi:cy="1.4281693"
|
||||||
|
sodipodi:r1="1.9958065"
|
||||||
|
sodipodi:r2="3.9916129"
|
||||||
|
sodipodi:arg1="0.51914611"
|
||||||
|
sodipodi:arg2="1.5663437"
|
||||||
|
inkscape:rounded="0.07"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 3.4847328,2.4183665 C 3.3646778,2.6284629 0.14985015,2.6427775 0.02792892,2.4337586 -0.09399232,2.2247398 1.5010247,-0.56653997 1.7430009,-0.56761742 1.9849772,-0.56869487 3.6047879,2.2082702 3.4847328,2.4183665 Z"
|
||||||
|
inkscape:transform-center-x="-0.9156312"
|
||||||
|
inkscape:transform-center-y="0.008867619"
|
||||||
|
transform="matrix(0,1.0087068,-1.0087069,0,7.6059217,0.2785234)" />
|
||||||
|
<rect
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-opacity:1"
|
||||||
|
id="rect1"
|
||||||
|
width="2"
|
||||||
|
height="1"
|
||||||
|
x="4.7523584"
|
||||||
|
y="7.5159998"
|
||||||
|
rx="0.5"
|
||||||
|
ry="0.5" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.9 KiB |
70
monoclient-s/ui/icons/next.svg
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="10mm"
|
||||||
|
height="10mm"
|
||||||
|
viewBox="0 0 10 10"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
||||||
|
sodipodi:docname="next.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff00"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:zoom="13.894537"
|
||||||
|
inkscape:cx="13.674439"
|
||||||
|
inkscape:cy="19.288156"
|
||||||
|
inkscape:window-width="1536"
|
||||||
|
inkscape:window-height="1041"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="43"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Слой 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<rect
|
||||||
|
style="fill:#000000;stroke-width:0"
|
||||||
|
id="rect2"
|
||||||
|
width="7.5"
|
||||||
|
height="1.5"
|
||||||
|
x="1.25"
|
||||||
|
y="-9"
|
||||||
|
rx="0.75"
|
||||||
|
ry="0.75"
|
||||||
|
transform="rotate(90)" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="fill:#000000;stroke-width:0"
|
||||||
|
id="path3"
|
||||||
|
inkscape:flatsided="true"
|
||||||
|
sodipodi:sides="3"
|
||||||
|
sodipodi:cx="1.7518876"
|
||||||
|
sodipodi:cy="1.4281693"
|
||||||
|
sodipodi:r1="1.9958065"
|
||||||
|
sodipodi:r2="3.9916129"
|
||||||
|
sodipodi:arg1="0.51914611"
|
||||||
|
sodipodi:arg2="1.5663437"
|
||||||
|
inkscape:rounded="0.07"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 3.4847328,2.4183665 C 3.3646778,2.6284629 0.14985015,2.6427775 0.02792892,2.4337586 -0.09399232,2.2247398 1.5010247,-0.56653997 1.7430009,-0.56761742 1.9849772,-0.56869487 3.6047879,2.2082702 3.4847328,2.4183665 Z"
|
||||||
|
inkscape:transform-center-x="-1.8312623"
|
||||||
|
inkscape:transform-center-y="0.017735426"
|
||||||
|
transform="matrix(0,2.0174137,-2.0174137,0,6.2118431,1.4570468)" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
60
monoclient-s/ui/icons/pause.svg
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="10mm"
|
||||||
|
height="10mm"
|
||||||
|
viewBox="0 0 10 10"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
||||||
|
sodipodi:docname="pause.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff00"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:zoom="13.894537"
|
||||||
|
inkscape:cx="13.674439"
|
||||||
|
inkscape:cy="19.288156"
|
||||||
|
inkscape:window-width="1536"
|
||||||
|
inkscape:window-height="1013"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="43"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Слой 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<rect
|
||||||
|
style="fill:#000000;stroke-width:0"
|
||||||
|
id="rect2"
|
||||||
|
width="1.5"
|
||||||
|
height="7.5"
|
||||||
|
x="2.75"
|
||||||
|
y="1.25"
|
||||||
|
rx="0.75"
|
||||||
|
ry="0.75" />
|
||||||
|
<rect
|
||||||
|
style="fill:#000000;stroke-width:0"
|
||||||
|
id="rect2-9"
|
||||||
|
width="1.5"
|
||||||
|
height="7.5"
|
||||||
|
x="5.75"
|
||||||
|
y="1.25"
|
||||||
|
rx="0.75"
|
||||||
|
ry="0.75" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
58
monoclient-s/ui/icons/play.svg
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="10mm"
|
||||||
|
height="10mm"
|
||||||
|
viewBox="0 0 10 10"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
||||||
|
sodipodi:docname="play.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff00"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:zoom="8.3757958"
|
||||||
|
inkscape:cx="29.489735"
|
||||||
|
inkscape:cy="38.324717"
|
||||||
|
inkscape:window-width="1536"
|
||||||
|
inkscape:window-height="1041"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="43"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Слой 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="fill:#000000;stroke-width:0"
|
||||||
|
id="path2"
|
||||||
|
inkscape:flatsided="true"
|
||||||
|
sodipodi:sides="3"
|
||||||
|
sodipodi:cx="3.4656906"
|
||||||
|
sodipodi:cy="3.2943101"
|
||||||
|
sodipodi:r1="3.8732622"
|
||||||
|
sodipodi:r2="3.8401258"
|
||||||
|
sodipodi:arg1="0.92139558"
|
||||||
|
sodipodi:arg2="1.9685931"
|
||||||
|
inkscape:rounded="0.04"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 5.807888,6.3791555 C 5.5941637,6.5414277 -0.3432929,4.0465167 -0.37696257,3.7802899 -0.41063223,3.5140631 4.7187523,-0.38046969 4.9661464,-0.27651507 5.2135404,-0.17256045 6.0216124,6.2168833 5.807888,6.3791555 Z"
|
||||||
|
transform="matrix(0.55578837,-0.76724136,0.73199924,0.58254681,0.05250644,5.7392433)" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
70
monoclient-s/ui/icons/previous.svg
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="10mm"
|
||||||
|
height="10mm"
|
||||||
|
viewBox="0 0 10 10"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
||||||
|
sodipodi:docname="previous.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff00"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:zoom="13.894537"
|
||||||
|
inkscape:cx="13.674439"
|
||||||
|
inkscape:cy="19.288156"
|
||||||
|
inkscape:window-width="1536"
|
||||||
|
inkscape:window-height="1013"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="43"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Слой 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<rect
|
||||||
|
style="fill:#000000;stroke-width:0"
|
||||||
|
id="rect2"
|
||||||
|
width="7.5"
|
||||||
|
height="1.5"
|
||||||
|
x="1.25"
|
||||||
|
y="-2.5"
|
||||||
|
rx="0.75"
|
||||||
|
ry="0.75"
|
||||||
|
transform="rotate(90)" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="fill:#000000;stroke-width:0"
|
||||||
|
id="path3"
|
||||||
|
inkscape:flatsided="true"
|
||||||
|
sodipodi:sides="3"
|
||||||
|
sodipodi:cx="1.7518876"
|
||||||
|
sodipodi:cy="1.4281693"
|
||||||
|
sodipodi:r1="1.9958065"
|
||||||
|
sodipodi:r2="3.9916129"
|
||||||
|
sodipodi:arg1="0.51914611"
|
||||||
|
sodipodi:arg2="1.5663437"
|
||||||
|
inkscape:rounded="0.07"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 3.4847328,2.4183665 C 3.3646778,2.6284629 0.14985015,2.6427775 0.02792892,2.4337586 -0.09399232,2.2247398 1.5010247,-0.56653997 1.7430009,-0.56761742 1.9849772,-0.56869487 3.6047879,2.2082702 3.4847328,2.4183665 Z"
|
||||||
|
inkscape:transform-center-x="1.8312623"
|
||||||
|
transform="matrix(0,-2.0174137,2.0174137,0,3.7881198,8.5429532)"
|
||||||
|
inkscape:transform-center-y="-0.017735441" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
110
monoclient-s/ui/icons/random.svg
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="10mm"
|
||||||
|
height="10mm"
|
||||||
|
viewBox="0 0 10 10"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
||||||
|
sodipodi:docname="random.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:zoom="12.005348"
|
||||||
|
inkscape:cx="13.993764"
|
||||||
|
inkscape:cy="26.987973"
|
||||||
|
inkscape:window-width="1536"
|
||||||
|
inkscape:window-height="1013"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="43"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1">
|
||||||
|
<inkscape:perspective
|
||||||
|
sodipodi:type="inkscape:persp3d"
|
||||||
|
inkscape:vp_x="0 : 5 : 1"
|
||||||
|
inkscape:vp_y="0 : 1000 : 0"
|
||||||
|
inkscape:vp_z="10 : 5 : 1"
|
||||||
|
inkscape:persp3d-origin="5 : 3.3333333 : 1"
|
||||||
|
id="perspective1" />
|
||||||
|
</defs>
|
||||||
|
<g
|
||||||
|
inkscape:label="Слой 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<g
|
||||||
|
id="g6"
|
||||||
|
transform="translate(-0.41149989)">
|
||||||
|
<rect
|
||||||
|
style="fill:#000000;stroke-width:0"
|
||||||
|
id="rect6"
|
||||||
|
width="10"
|
||||||
|
height="1"
|
||||||
|
x="2.0706227"
|
||||||
|
y="-0.50035352"
|
||||||
|
rx="0.5"
|
||||||
|
ry="0.5"
|
||||||
|
transform="rotate(45)" />
|
||||||
|
<rect
|
||||||
|
style="fill:#000000;stroke-width:0"
|
||||||
|
id="rect6-7"
|
||||||
|
width="10"
|
||||||
|
height="1"
|
||||||
|
x="-5"
|
||||||
|
y="6.5702691"
|
||||||
|
rx="0.5"
|
||||||
|
ry="0.5"
|
||||||
|
transform="rotate(-45)" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="fill:#000000;stroke-width:0"
|
||||||
|
id="path3"
|
||||||
|
inkscape:flatsided="true"
|
||||||
|
sodipodi:sides="3"
|
||||||
|
sodipodi:cx="1.7518876"
|
||||||
|
sodipodi:cy="1.4281693"
|
||||||
|
sodipodi:r1="1.9958065"
|
||||||
|
sodipodi:r2="3.9916129"
|
||||||
|
sodipodi:arg1="0.51914611"
|
||||||
|
sodipodi:arg2="1.5663437"
|
||||||
|
inkscape:rounded="0.07"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 3.4847328,2.4183665 C 3.3646778,2.6284629 0.14985015,2.6427775 0.02792892,2.4337586 -0.09399232,2.2247398 1.5010247,-0.56653997 1.7430009,-0.56761742 1.9849772,-0.56869487 3.6047879,2.2082702 3.4847328,2.4183665 Z"
|
||||||
|
inkscape:transform-center-x="-0.0861241"
|
||||||
|
inkscape:transform-center-y="-0.087606847"
|
||||||
|
transform="matrix(0.63138587,0.63138587,-0.63138587,0.63138587,8.0200992,-0.32204334)" />
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="fill:#000000;stroke-width:0"
|
||||||
|
id="path3-6"
|
||||||
|
inkscape:flatsided="true"
|
||||||
|
sodipodi:sides="3"
|
||||||
|
sodipodi:cx="1.7518876"
|
||||||
|
sodipodi:cy="1.4281693"
|
||||||
|
sodipodi:r1="1.9958065"
|
||||||
|
sodipodi:r2="3.9916129"
|
||||||
|
sodipodi:arg1="0.51914611"
|
||||||
|
sodipodi:arg2="1.5663437"
|
||||||
|
inkscape:rounded="0.07"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 3.4847328,2.4183665 C 3.3646778,2.6284629 0.14985015,2.6427775 0.02792892,2.4337586 -0.09399232,2.2247398 1.5010247,-0.56653997 1.7430009,-0.56761742 1.9849772,-0.56869487 3.6047879,2.2082702 3.4847328,2.4183665 Z"
|
||||||
|
inkscape:transform-center-x="-0.087607003"
|
||||||
|
inkscape:transform-center-y="0.086124089"
|
||||||
|
transform="matrix(-0.63138587,0.63138587,-0.63138587,-0.63138587,10.22831,8.1200992)" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.7 KiB |
96
monoclient-s/ui/icons/repeat-one.svg
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="10mm"
|
||||||
|
height="10mm"
|
||||||
|
viewBox="0 0 10 10"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="repeat-one.svg"
|
||||||
|
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:zoom="6.1285977"
|
||||||
|
inkscape:cx="9.4638289"
|
||||||
|
inkscape:cy="10.116507"
|
||||||
|
inkscape:window-width="1536"
|
||||||
|
inkscape:window-height="1041"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="43"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Слой 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="fill:#000000;stroke-width:0"
|
||||||
|
id="path3"
|
||||||
|
inkscape:flatsided="true"
|
||||||
|
sodipodi:sides="3"
|
||||||
|
sodipodi:cx="1.7518876"
|
||||||
|
sodipodi:cy="1.4281693"
|
||||||
|
sodipodi:r1="1.9958065"
|
||||||
|
sodipodi:r2="3.9916129"
|
||||||
|
sodipodi:arg1="0.51914611"
|
||||||
|
sodipodi:arg2="1.5663437"
|
||||||
|
inkscape:rounded="0.07"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 3.4847328,2.4183665 C 3.3646778,2.6284629 0.14985015,2.6427775 0.02792892,2.4337586 -0.09399232,2.2247398 1.5010247,-0.56653997 1.7430009,-0.56761742 1.9849772,-0.56869487 3.6047879,2.2082702 3.4847328,2.4183665 Z"
|
||||||
|
inkscape:transform-center-x="0.75466106"
|
||||||
|
inkscape:transform-center-y="-0.0073088053"
|
||||||
|
transform="matrix(0,-0.83137379,0.83137379,0,3.7731711,3.93391)" />
|
||||||
|
<path
|
||||||
|
style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path1"
|
||||||
|
sodipodi:type="arc"
|
||||||
|
sodipodi:cx="5.1258287"
|
||||||
|
sodipodi:cy="5.2289081"
|
||||||
|
sodipodi:rx="3"
|
||||||
|
sodipodi:ry="3"
|
||||||
|
sodipodi:start="4.712389"
|
||||||
|
sodipodi:end="3.8397244"
|
||||||
|
sodipodi:arc-type="arc"
|
||||||
|
d="M 5.1258287,2.2289081 A 3,3 0 0 1 8.0547168,4.5795893 3,3 0 0 1 6.3936834,7.9478315 3,3 0 0 1 2.7457687,7.0551923 3,3 0 0 1 2.8276955,3.3005451"
|
||||||
|
sodipodi:open="true" />
|
||||||
|
<g
|
||||||
|
id="g1"
|
||||||
|
transform="translate(-0.33961362,-0.08264361)">
|
||||||
|
<rect
|
||||||
|
style="fill:#000000;stroke-width:0.7;stroke-dasharray:none"
|
||||||
|
id="rect1"
|
||||||
|
width="0.5"
|
||||||
|
height="3"
|
||||||
|
x="5.347394"
|
||||||
|
y="4.0818858"
|
||||||
|
rx="0.25"
|
||||||
|
ry="0.25" />
|
||||||
|
<rect
|
||||||
|
style="fill:#000000;stroke-width:0.7;stroke-dasharray:none"
|
||||||
|
id="rect1-2"
|
||||||
|
width="0.5"
|
||||||
|
height="1.5"
|
||||||
|
x="6.767818"
|
||||||
|
y="-1.1417361"
|
||||||
|
rx="0.25"
|
||||||
|
ry="0.25"
|
||||||
|
transform="rotate(45)" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
73
monoclient-s/ui/icons/repeat.svg
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="10mm"
|
||||||
|
height="10mm"
|
||||||
|
viewBox="0 0 10 10"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="repeat.svg"
|
||||||
|
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:zoom="21.325417"
|
||||||
|
inkscape:cx="18.897637"
|
||||||
|
inkscape:cy="18.897637"
|
||||||
|
inkscape:window-width="1536"
|
||||||
|
inkscape:window-height="1041"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="43"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Слой 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="fill:#000000;stroke-width:0"
|
||||||
|
id="path3"
|
||||||
|
inkscape:flatsided="true"
|
||||||
|
sodipodi:sides="3"
|
||||||
|
sodipodi:cx="1.7518876"
|
||||||
|
sodipodi:cy="1.4281693"
|
||||||
|
sodipodi:r1="1.9958065"
|
||||||
|
sodipodi:r2="3.9916129"
|
||||||
|
sodipodi:arg1="0.51914611"
|
||||||
|
sodipodi:arg2="1.5663437"
|
||||||
|
inkscape:rounded="0.07"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="M 3.4847328,2.4183665 C 3.3646778,2.6284629 0.14985015,2.6427775 0.02792892,2.4337586 -0.09399232,2.2247398 1.5010247,-0.56653997 1.7430009,-0.56761742 1.9849772,-0.56869487 3.6047879,2.2082702 3.4847328,2.4183665 Z"
|
||||||
|
inkscape:transform-center-x="0.75466106"
|
||||||
|
inkscape:transform-center-y="-0.0073088053"
|
||||||
|
transform="matrix(0,-0.83137379,0.83137379,0,3.7731711,3.93391)" />
|
||||||
|
<path
|
||||||
|
style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:0.9;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path1"
|
||||||
|
sodipodi:type="arc"
|
||||||
|
sodipodi:cx="5.1258287"
|
||||||
|
sodipodi:cy="5.2289081"
|
||||||
|
sodipodi:rx="3"
|
||||||
|
sodipodi:ry="3"
|
||||||
|
sodipodi:start="4.712389"
|
||||||
|
sodipodi:end="3.8397244"
|
||||||
|
sodipodi:arc-type="arc"
|
||||||
|
d="M 5.1258287,2.2289081 A 3,3 0 0 1 8.0547168,4.5795893 3,3 0 0 1 6.3936834,7.9478315 3,3 0 0 1 2.7457687,7.0551923 3,3 0 0 1 2.8276955,3.3005451"
|
||||||
|
sodipodi:open="true" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
51
monoclient-s/ui/icons/stop.svg
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="10mm"
|
||||||
|
height="10mm"
|
||||||
|
viewBox="0 0 10 10"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
|
||||||
|
sodipodi:docname="stop.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff00"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:zoom="13.894537"
|
||||||
|
inkscape:cx="13.710424"
|
||||||
|
inkscape:cy="19.288156"
|
||||||
|
inkscape:window-width="1536"
|
||||||
|
inkscape:window-height="1013"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="43"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Слой 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<rect
|
||||||
|
style="fill:#000000;stroke-width:0"
|
||||||
|
id="rect2"
|
||||||
|
width="7.5"
|
||||||
|
height="7.5"
|
||||||
|
x="1.25"
|
||||||
|
y="1.25"
|
||||||
|
rx="0.75"
|
||||||
|
ry="0.75" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
286
monoclient-s/ui/ui.slint
Normal file
|
@ -0,0 +1,286 @@
|
||||||
|
import { Palette, Slider, ComboBox } from "std-widgets.slint";
|
||||||
|
component Button {
|
||||||
|
in-out property icon <=> img.source;
|
||||||
|
in property <bool> wanted;
|
||||||
|
in property <bool> enabled: false;
|
||||||
|
callback clicked;
|
||||||
|
VerticalLayout {
|
||||||
|
alignment: center;
|
||||||
|
spacing: 8px;
|
||||||
|
img := Image {
|
||||||
|
// What the actual fuck
|
||||||
|
x: 7px;
|
||||||
|
colorize: enabled ? Palette.foreground : Palette.foreground.darker(10%);
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
horizontal-alignment: center;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
rect := Rectangle {
|
||||||
|
touch := TouchArea {
|
||||||
|
clicked => {
|
||||||
|
if enabled {
|
||||||
|
clicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
border-radius: root.height / 2;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: Palette.border;
|
||||||
|
background: enabled ? (touch.pressed ? Palette.control-background.darker(45%) : (touch.has-hover ? Palette.control-background.darker(35%) : Palette.control-background)) : Palette.control-background.darker(35%);
|
||||||
|
animate background { duration: 166ms; }
|
||||||
|
|
||||||
|
drop-shadow-blur: wanted ? pow(abs(mod(animation-tick() / 1s - 3, 6) - 3) / 3, 2) * 128.0 * 1px : 0px;
|
||||||
|
drop-shadow-color: self.border-color;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export component MainWindow inherits Window {
|
||||||
|
callback play;
|
||||||
|
callback stop;
|
||||||
|
callback next;
|
||||||
|
callback refreshp;
|
||||||
|
callback change_volume(float);
|
||||||
|
callback text_edited;
|
||||||
|
|
||||||
|
in-out property <string> addr: address.text;
|
||||||
|
in-out property <string> mtitle: "";
|
||||||
|
in-out property <string> malbum: "";
|
||||||
|
in-out property <string> martist: "";
|
||||||
|
in-out property <float> volume: svolume.value;
|
||||||
|
in-out property <bool> start_enabled: false;
|
||||||
|
in-out property <bool> playing: false;
|
||||||
|
in-out property <bool> paused: false;
|
||||||
|
in property <image> cover: @image-url("../lonelyradio.png");
|
||||||
|
in property <[string]> playlists: ["All tracks"];
|
||||||
|
in property <[string]> supported_encoders: [];
|
||||||
|
in-out property <string> selected_playlist: selected.current-value;
|
||||||
|
in-out property <int> selected_encoder: encoder.current-index;
|
||||||
|
|
||||||
|
property <bool> settings: false;
|
||||||
|
|
||||||
|
title: "monoclient-s";
|
||||||
|
min-width: 448px;
|
||||||
|
max-width: 448px * 3;
|
||||||
|
preferred-width: 448px;
|
||||||
|
height: main.height;
|
||||||
|
animate background { duration: 166ms; }
|
||||||
|
|
||||||
|
panels := HorizontalLayout {
|
||||||
|
width: root.width * 2;
|
||||||
|
x: settings ? -root.width : 0.0;
|
||||||
|
height: main.height;
|
||||||
|
animate x {
|
||||||
|
easing: ease-in-out-expo;
|
||||||
|
duration: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
main := HorizontalLayout {
|
||||||
|
width: root.width;
|
||||||
|
height: rect.height + self.padding * 2;
|
||||||
|
spacing: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
rect := Rectangle {
|
||||||
|
opacity: playing ? 1.0 : 0.0;
|
||||||
|
animate opacity {
|
||||||
|
duration: 0.25s;
|
||||||
|
easing: ease-in-out;
|
||||||
|
}
|
||||||
|
clip: true;
|
||||||
|
border-radius: 6px;
|
||||||
|
animate background { duration: 166ms; }
|
||||||
|
background: Palette.foreground;
|
||||||
|
//width: 240px;
|
||||||
|
height: img.height + 12px * 2 + 1.5rem * 3;
|
||||||
|
max-width: parent.width;
|
||||||
|
border-width: 0px;
|
||||||
|
VerticalLayout {
|
||||||
|
img := Image {
|
||||||
|
vertical-alignment: top;
|
||||||
|
source: cover;
|
||||||
|
min-width: 240px;
|
||||||
|
height: self.width;
|
||||||
|
image-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
VerticalLayout {
|
||||||
|
padding: 12px;
|
||||||
|
tartist := Text {
|
||||||
|
color: Palette.background;
|
||||||
|
vertical-alignment: center;
|
||||||
|
height: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text: martist;
|
||||||
|
overflow: elide;
|
||||||
|
}
|
||||||
|
|
||||||
|
talbum := Text {
|
||||||
|
color: Palette.background;
|
||||||
|
vertical-alignment: center;
|
||||||
|
height: 1.5rem;
|
||||||
|
text: malbum;
|
||||||
|
overflow: elide;
|
||||||
|
}
|
||||||
|
|
||||||
|
ttitle := Text {
|
||||||
|
color: Palette.background;
|
||||||
|
vertical-alignment: center;
|
||||||
|
|
||||||
|
height: 1.5rem;
|
||||||
|
text: mtitle;
|
||||||
|
overflow: elide;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VerticalLayout {
|
||||||
|
alignment: center;
|
||||||
|
//max-width: 160px;
|
||||||
|
spacing: 16px;
|
||||||
|
VerticalLayout {
|
||||||
|
spacing: 16px;
|
||||||
|
HorizontalLayout {
|
||||||
|
padding: 8px;
|
||||||
|
alignment: center;
|
||||||
|
spacing: 16px;
|
||||||
|
//height: 96px;
|
||||||
|
Button {
|
||||||
|
icon: @image-url("icons/stop.svg");
|
||||||
|
enabled: playing && !paused;
|
||||||
|
clicked => {
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
icon: playing ? (paused ? @image-url("icons/play.svg") : @image-url("icons/pause.svg")) : @image-url("icons/random.svg");
|
||||||
|
enabled: start_enabled || playing;
|
||||||
|
wanted: start_enabled && !playing;
|
||||||
|
clicked => {
|
||||||
|
play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
icon: @image-url("icons/next.svg");
|
||||||
|
enabled: playing && !paused;
|
||||||
|
clicked => {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
padding: 16px;
|
||||||
|
clip: true;
|
||||||
|
background: Palette.background;
|
||||||
|
border-color: Palette.border;
|
||||||
|
animate background, border-color { duration: 166ms; }
|
||||||
|
border-width: 1px;
|
||||||
|
border-radius: 4px;
|
||||||
|
drop-shadow-blur: !start_enabled ? pow(abs(mod(animation-tick() / 1s - 3, 6) - 3) / 3, 2) * 128.0 * 1px : 0px;
|
||||||
|
drop-shadow-color: Palette.border;
|
||||||
|
VerticalLayout {
|
||||||
|
alignment: center;
|
||||||
|
padding: 8px;
|
||||||
|
address := TextInput {
|
||||||
|
text: "";
|
||||||
|
accepted => {
|
||||||
|
self.clear_focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
edited => {
|
||||||
|
text_edited();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
svolume := Slider {
|
||||||
|
value: 255;
|
||||||
|
maximum: 255;
|
||||||
|
changed(f) => {
|
||||||
|
change_volume(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalLayout {
|
||||||
|
alignment: center;
|
||||||
|
Button {
|
||||||
|
icon: @image-url("icons/gear.svg");
|
||||||
|
enabled: true;
|
||||||
|
wanted: false;
|
||||||
|
clicked => {
|
||||||
|
settings = !settings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VerticalLayout {
|
||||||
|
width: root.width;
|
||||||
|
height: root.height;
|
||||||
|
alignment: center;
|
||||||
|
|
||||||
|
VerticalLayout {
|
||||||
|
padding: 20px;
|
||||||
|
spacing: 20px;
|
||||||
|
HorizontalLayout {
|
||||||
|
alignment: center;
|
||||||
|
spacing: 8px;
|
||||||
|
Text {
|
||||||
|
horizontal-alignment: right;
|
||||||
|
vertical-alignment: center;
|
||||||
|
text: "Playlists";
|
||||||
|
}
|
||||||
|
|
||||||
|
selected := ComboBox {
|
||||||
|
model: playlists;
|
||||||
|
current-index: 0;
|
||||||
|
selected() => {
|
||||||
|
self.clear_focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalLayout {
|
||||||
|
alignment: center;
|
||||||
|
spacing: 8px;
|
||||||
|
Text {
|
||||||
|
horizontal-alignment: right;
|
||||||
|
vertical-alignment: center;
|
||||||
|
text: "Encoder";
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := ComboBox {
|
||||||
|
model: supported_encoders;
|
||||||
|
selected() => {
|
||||||
|
self.clear_focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalLayout {
|
||||||
|
alignment: center;
|
||||||
|
Button {
|
||||||
|
icon: @image-url("icons/first.svg");
|
||||||
|
enabled: true;
|
||||||
|
wanted: false;
|
||||||
|
clicked => {
|
||||||
|
settings = !settings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -352,12 +352,12 @@
|
||||||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||||
LIBRARY_SEARCH_PATHS = "";
|
LIBRARY_SEARCH_PATHS = "";
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||||
MARKETING_VERSION = 0.6.0;
|
MARKETING_VERSION = 0.7.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "dev.ivabus.monoclient-x";
|
PRODUCT_BUNDLE_IDENTIFIER = "dev.ivabus.monoclient-x";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
@ -405,12 +405,12 @@
|
||||||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||||
LIBRARY_SEARCH_PATHS = "${PROJECT_DIR/../target/aarch64-apple-darwin/release}";
|
LIBRARY_SEARCH_PATHS = "${PROJECT_DIR/../target/aarch64-apple-darwin/release}";
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||||
MARKETING_VERSION = 0.6.0;
|
MARKETING_VERSION = 0.7.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "dev.ivabus.monoclient-x";
|
PRODUCT_BUNDLE_IDENTIFIER = "dev.ivabus.monoclient-x";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|
|
@ -5,11 +5,10 @@
|
||||||
// Created by ivabus on 13.06.2024.
|
// Created by ivabus on 13.06.2024.
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import AVFAudio
|
import AVFAudio
|
||||||
import MediaPlayer
|
import MediaPlayer
|
||||||
import MonoLib
|
import MonoLib
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
enum PlayerState {
|
enum PlayerState {
|
||||||
case NotStarted
|
case NotStarted
|
||||||
|
@ -17,11 +16,12 @@ enum PlayerState {
|
||||||
case Paused
|
case Paused
|
||||||
|
|
||||||
mutating func update() {
|
mutating func update() {
|
||||||
self = switch c_get_state() {
|
self =
|
||||||
case 2: PlayerState.Playing
|
switch c_get_state() {
|
||||||
case 3: PlayerState.Paused
|
case 2: PlayerState.Playing
|
||||||
default: PlayerState.NotStarted
|
case 3: PlayerState.Paused
|
||||||
}
|
default: PlayerState.NotStarted
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,10 +30,11 @@ enum EncoderType: UInt8 {
|
||||||
case PCMFloat = 1
|
case PCMFloat = 1
|
||||||
case FLAC = 2
|
case FLAC = 2
|
||||||
case Alac = 3
|
case Alac = 3
|
||||||
//WavPack = 4,
|
//WavPack = 4,
|
||||||
//Opus = 5,
|
//Opus = 5,
|
||||||
//Aac = 6,
|
//Aac = 6,
|
||||||
case Vorbis = 7
|
case Vorbis = 7
|
||||||
|
case Sea = 8
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CoverSize: Int32 {
|
enum CoverSize: Int32 {
|
||||||
|
@ -51,10 +52,10 @@ struct PlayList: Identifiable, Hashable {
|
||||||
var name: String
|
var name: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
struct Settings {
|
struct Settings {
|
||||||
var encoder: EncoderType = EncoderType.FLAC
|
var encoder: EncoderType = EncoderType.FLAC
|
||||||
var cover_size: CoverSize = CoverSize.High/*
|
var cover_size: CoverSize = CoverSize
|
||||||
|
.High /*
|
||||||
init(enc: EncoderType, cov: CoverSize) {
|
init(enc: EncoderType, cov: CoverSize) {
|
||||||
encoder = enc
|
encoder = enc
|
||||||
cover_size = cov
|
cover_size = cov
|
||||||
|
@ -62,9 +63,9 @@ struct Settings {
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
typealias MyStack = HStack
|
typealias MyStack = HStack
|
||||||
#else
|
#else
|
||||||
typealias MyStack = VStack
|
typealias MyStack = VStack
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
struct Player: View {
|
struct Player: View {
|
||||||
|
@ -81,32 +82,30 @@ struct Player: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
||||||
|
|
||||||
MyStack(alignment: .center) {
|
MyStack(alignment: .center) {
|
||||||
|
|
||||||
VStack(alignment: .center) {
|
VStack(alignment: .center) {
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
Image(nsImage: cover.cover)
|
Image(nsImage: cover.cover)
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(minWidth: 256, maxWidth: 256, minHeight: 256, maxHeight: 256)
|
.frame(minWidth: 256, maxWidth: 256, minHeight: 256, maxHeight: 256)
|
||||||
.frame(width: 256.0, height: 256.0)
|
.frame(width: 256.0, height: 256.0)
|
||||||
.clipShape(.rect(cornerRadius: 24))
|
.clipShape(.rect(cornerRadius: 24))
|
||||||
.shadow(radius: 16)
|
.shadow(radius: 16)
|
||||||
.padding(16)
|
.padding(16)
|
||||||
#else
|
#else
|
||||||
Image(uiImage: cover.cover)
|
Image(uiImage: cover.cover)
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(minWidth: 256, maxWidth: 256, minHeight: 256, maxHeight: 256)
|
.frame(minWidth: 256, maxWidth: 256, minHeight: 256, maxHeight: 256)
|
||||||
.frame(width: 256.0, height: 256.0)
|
.frame(width: 256.0, height: 256.0)
|
||||||
.clipShape(.rect(cornerRadius: 24))
|
.clipShape(.rect(cornerRadius: 24))
|
||||||
.shadow(radius: 16)
|
.shadow(radius: 16)
|
||||||
.padding(16)
|
.padding(16)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
VStack(alignment: .center){
|
VStack(alignment: .center) {
|
||||||
Text(metadata.title).bold()
|
Text(metadata.title).bold()
|
||||||
|
|
||||||
Text(metadata.album)
|
Text(metadata.album)
|
||||||
|
@ -119,14 +118,15 @@ struct Player: View {
|
||||||
cover.update()
|
cover.update()
|
||||||
}
|
}
|
||||||
let image = cover.cover
|
let image = cover.cover
|
||||||
let mediaArtwork = MPMediaItemArtwork(boundsSize: image.size) { (size: CGSize) -> PlatformImage in
|
let mediaArtwork = MPMediaItemArtwork(boundsSize: image.size) {
|
||||||
|
(size: CGSize) -> PlatformImage in
|
||||||
return image
|
return image
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
#if os(macOS)
|
MPNowPlayingInfoCenter.default().playbackState =
|
||||||
MPNowPlayingInfoCenter.default().playbackState = state == PlayerState.Playing ? .playing : .paused
|
state == PlayerState.Playing ? .playing : .paused
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
let nowPlayingInfo: [String: Any] = [
|
let nowPlayingInfo: [String: Any] = [
|
||||||
MPMediaItemPropertyArtist: metadata.artist,
|
MPMediaItemPropertyArtist: metadata.artist,
|
||||||
|
@ -144,11 +144,11 @@ struct Player: View {
|
||||||
"Server",
|
"Server",
|
||||||
text: $server,
|
text: $server,
|
||||||
onCommit: {
|
onCommit: {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
NSApp.keyWindow?.makeFirstResponder(nil)
|
NSApp.keyWindow?.makeFirstResponder(nil)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
|
@ -158,7 +158,7 @@ struct Player: View {
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
Button(action: stop){
|
Button(action: stop) {
|
||||||
Image(systemName: "stop.fill").padding(4).frame(width: 32, height: 24)
|
Image(systemName: "stop.fill").padding(4).frame(width: 32, height: 24)
|
||||||
}
|
}
|
||||||
.disabled(state == PlayerState.NotStarted)
|
.disabled(state == PlayerState.NotStarted)
|
||||||
|
@ -166,16 +166,22 @@ struct Player: View {
|
||||||
.font(.system(size: 20))
|
.font(.system(size: 20))
|
||||||
.buttonBorderShape(.capsule)
|
.buttonBorderShape(.capsule)
|
||||||
|
|
||||||
Button(action: play){
|
Button(action: play) {
|
||||||
Image(systemName: state == PlayerState.NotStarted ? "infinity.circle" : (state == PlayerState.Playing) ? "pause.circle.fill" : "play.circle" )
|
Image(
|
||||||
.font(.system(size: 30))
|
systemName: state == PlayerState.NotStarted
|
||||||
.padding(4)
|
? "infinity.circle"
|
||||||
|
: (state == PlayerState.Playing)
|
||||||
|
? "pause.circle.fill" : "play.circle"
|
||||||
|
)
|
||||||
|
.font(.system(size: 30))
|
||||||
|
.padding(4)
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
.buttonBorderShape(.capsule)
|
.buttonBorderShape(.capsule)
|
||||||
|
|
||||||
Button(action: next){
|
Button(action: next) {
|
||||||
Image(systemName: "forward.end.fill").padding(4).frame(width: 32, height: 24)
|
Image(systemName: "forward.end.fill").padding(4).frame(
|
||||||
|
width: 32, height: 24)
|
||||||
}.disabled(state == PlayerState.NotStarted)
|
}.disabled(state == PlayerState.NotStarted)
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
.font(.system(size: 20))
|
.font(.system(size: 20))
|
||||||
|
@ -185,7 +191,7 @@ struct Player: View {
|
||||||
}
|
}
|
||||||
Menu {
|
Menu {
|
||||||
Picker("Playlist", selection: $playlist) {
|
Picker("Playlist", selection: $playlist) {
|
||||||
ForEach ($playlists) { pl in
|
ForEach($playlists) { pl in
|
||||||
Text(pl.wrappedValue.name).tag(pl.wrappedValue)
|
Text(pl.wrappedValue.name).tag(pl.wrappedValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,6 +208,8 @@ struct Player: View {
|
||||||
.tag(EncoderType.Alac)
|
.tag(EncoderType.Alac)
|
||||||
Text("Vorbis (lossy)")
|
Text("Vorbis (lossy)")
|
||||||
.tag(EncoderType.Vorbis)
|
.tag(EncoderType.Vorbis)
|
||||||
|
Text("Sea (lossy)")
|
||||||
|
.tag(EncoderType.Sea)
|
||||||
}.pickerStyle(.menu)
|
}.pickerStyle(.menu)
|
||||||
|
|
||||||
Picker("Cover size", selection: $settings.cover_size) {
|
Picker("Cover size", selection: $settings.cover_size) {
|
||||||
|
@ -227,18 +235,21 @@ struct Player: View {
|
||||||
.padding(32)
|
.padding(32)
|
||||||
.onReceive(timer_playlists) { _ in
|
.onReceive(timer_playlists) { _ in
|
||||||
var id = -1
|
var id = -1
|
||||||
playlists = (["All tracks"] + String(cString: c_list_playlists(server)).components(separatedBy: "\n")).map({elem in
|
playlists =
|
||||||
if elem.isEmpty {
|
(["All tracks"]
|
||||||
return PlayList(id: -1, name: elem)
|
+ String(cString: c_list_playlists(server)).components(separatedBy: "\n")).map({
|
||||||
}
|
elem in
|
||||||
id += 1;
|
if elem.isEmpty {
|
||||||
return PlayList(id: id, name: elem)
|
return PlayList(id: -1, name: elem)
|
||||||
}).filter({elem in elem.id != -1})
|
}
|
||||||
|
id += 1
|
||||||
|
return PlayList(id: id, name: elem)
|
||||||
|
}).filter({ elem in elem.id != -1 })
|
||||||
}
|
}
|
||||||
.onAppear() {
|
.onAppear {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
UIApplication.shared.beginReceivingRemoteControlEvents()
|
UIApplication.shared.beginReceivingRemoteControlEvents()
|
||||||
#endif
|
#endif
|
||||||
MPRemoteCommandCenter.shared().previousTrackCommand.isEnabled = false
|
MPRemoteCommandCenter.shared().previousTrackCommand.isEnabled = false
|
||||||
MPRemoteCommandCenter.shared().nextTrackCommand.isEnabled = true
|
MPRemoteCommandCenter.shared().nextTrackCommand.isEnabled = true
|
||||||
MPRemoteCommandCenter.shared().skipForwardCommand.isEnabled = false
|
MPRemoteCommandCenter.shared().skipForwardCommand.isEnabled = false
|
||||||
|
@ -256,49 +267,52 @@ struct Player: View {
|
||||||
return MPRemoteCommandHandlerStatus.success
|
return MPRemoteCommandHandlerStatus.success
|
||||||
})
|
})
|
||||||
|
|
||||||
MPRemoteCommandCenter.shared().togglePlayPauseCommand.addTarget(handler: {_ in
|
MPRemoteCommandCenter.shared().togglePlayPauseCommand.addTarget(handler: { _ in
|
||||||
play()
|
play()
|
||||||
return MPRemoteCommandHandlerStatus.success
|
return MPRemoteCommandHandlerStatus.success
|
||||||
})
|
})
|
||||||
|
|
||||||
MPRemoteCommandCenter.shared().nextTrackCommand.addTarget(handler: {_ in
|
MPRemoteCommandCenter.shared().nextTrackCommand.addTarget(handler: { _ in
|
||||||
next()
|
next()
|
||||||
return MPRemoteCommandHandlerStatus.success
|
return MPRemoteCommandHandlerStatus.success
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
.animation(.spring, value: UUID())
|
|
||||||
}
|
}
|
||||||
|
.animation(.spring, value: UUID())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func play() {
|
func play() {
|
||||||
switch state {
|
switch state {
|
||||||
case PlayerState.NotStarted: do {
|
case PlayerState.NotStarted:
|
||||||
/*#if os(macOS)
|
|
||||||
#else*/
|
|
||||||
let audioSession = AVAudioSession.sharedInstance()
|
|
||||||
do {
|
do {
|
||||||
try audioSession.setCategory(
|
#if os(macOS)
|
||||||
.playback, mode: .default)
|
#else
|
||||||
try audioSession.setActive(true)
|
let audioSession = AVAudioSession.sharedInstance()
|
||||||
|
do {
|
||||||
|
try audioSession.setCategory(
|
||||||
|
.playback, mode: .default)
|
||||||
|
try audioSession.setActive(true)
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to set the audio session configuration")
|
print("Failed to set the audio session configuration")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
Thread.detachNewThread {
|
||||||
|
c_start(
|
||||||
|
server,
|
||||||
|
CSettings(
|
||||||
|
encoder: settings.encoder.rawValue, cover: settings.cover_size.rawValue),
|
||||||
|
playlist.name == "All tracks" ? "" : playlist.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/*#endif*/
|
default:
|
||||||
Thread.detachNewThread {
|
do {
|
||||||
c_start(server, CSettings(encoder: settings.encoder.rawValue, cover: settings.cover_size.rawValue), playlist.name == "All tracks" ? "" : playlist.name)
|
c_toggle()
|
||||||
|
state.update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default: do {
|
|
||||||
c_toggle()
|
|
||||||
state.update()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
func stop() {
|
func stop() {
|
||||||
|
|
|
@ -57,7 +57,7 @@ fn main() {
|
||||||
monolib::run(
|
monolib::run(
|
||||||
&args.address,
|
&args.address,
|
||||||
Settings {
|
Settings {
|
||||||
encoder: Encoder::Flac,
|
encoder: Encoder::Sea,
|
||||||
cover: -1,
|
cover: -1,
|
||||||
},
|
},
|
||||||
&args.playlist,
|
&args.playlist,
|
||||||
|
|
|
@ -3,7 +3,7 @@ name = "monolib"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "A library implementing the lonely radio audio streaming protocol"
|
description = "A library implementing the lonelyradio audio streaming protocol"
|
||||||
repository = "https://github.com/ivabus/lonelyradio"
|
repository = "https://github.com/ivabus/lonelyradio"
|
||||||
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
|
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
|
||||||
|
|
||||||
|
@ -22,15 +22,17 @@ anyhow = "1.0.86"
|
||||||
claxon = { version = "0.4.3", optional = true }
|
claxon = { version = "0.4.3", optional = true }
|
||||||
symphonia-codec-alac = {version = "0.5.4", optional = true }
|
symphonia-codec-alac = {version = "0.5.4", optional = true }
|
||||||
symphonia-core = {version = "0.5.4", optional = true }
|
symphonia-core = {version = "0.5.4", optional = true }
|
||||||
vorbis_rs = {version = "0.5.4", optional = true }
|
lewton = {version = "0.10.2", optional = true }
|
||||||
|
sea-codec = { version = "0.5.2", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["all-lossless", "all-lossy"]
|
default = ["all-lossless", "all-lossy"]
|
||||||
all-lossless = ["alac", "flac"]
|
all-lossless = ["alac", "flac"]
|
||||||
all-lossy = ["vorbis"]
|
all-lossy = ["vorbis", "sea"]
|
||||||
alac = ["dep:symphonia-codec-alac", "dep:symphonia-core"]
|
alac = ["dep:symphonia-codec-alac", "dep:symphonia-core"]
|
||||||
flac = ["dep:claxon"]
|
flac = ["dep:claxon"]
|
||||||
vorbis = ["dep:vorbis_rs"]
|
vorbis = ["dep:lewton"]
|
||||||
|
sea = ["dep:sea-codec"]
|
||||||
|
|
||||||
[package.metadata.xcframework]
|
[package.metadata.xcframework]
|
||||||
include-dir = "src"
|
include-dir = "src"
|
||||||
|
@ -38,5 +40,4 @@ lib-type = "cdylib"
|
||||||
zip = false
|
zip = false
|
||||||
macOS = true
|
macOS = true
|
||||||
iOS = true
|
iOS = true
|
||||||
#tvOS = true
|
simulators = false
|
||||||
simulators = true
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ pub extern "C" fn c_start(server: *const c_char, settings: CSettings, playlist:
|
||||||
2 => Encoder::Flac,
|
2 => Encoder::Flac,
|
||||||
3 => Encoder::Alac,
|
3 => Encoder::Alac,
|
||||||
7 => Encoder::Vorbis,
|
7 => Encoder::Vorbis,
|
||||||
|
8 => Encoder::Sea,
|
||||||
_ => return,
|
_ => return,
|
||||||
},
|
},
|
||||||
cover: settings.cover,
|
cover: settings.cover,
|
||||||
|
|
|
@ -18,8 +18,7 @@ pub(crate) fn decode(
|
||||||
Encoder::Pcm16 => {
|
Encoder::Pcm16 => {
|
||||||
let mut samples_i16 = vec![0; fmd.length as usize / 2];
|
let mut samples_i16 = vec![0; fmd.length as usize / 2];
|
||||||
stream.read_i16_into::<LittleEndian>(&mut samples_i16)?;
|
stream.read_i16_into::<LittleEndian>(&mut samples_i16)?;
|
||||||
samples
|
samples.extend(samples_i16.iter().map(|sample| *sample as f32 / 32767.0));
|
||||||
.append(&mut samples_i16.iter().map(|sample| *sample as f32 / 32767.0).collect());
|
|
||||||
}
|
}
|
||||||
Encoder::PcmFloat => {
|
Encoder::PcmFloat => {
|
||||||
let mut samples_f32 = vec![0f32; fmd.length as usize / 4];
|
let mut samples_f32 = vec![0f32; fmd.length as usize / 4];
|
||||||
|
@ -31,12 +30,8 @@ pub(crate) fn decode(
|
||||||
{
|
{
|
||||||
let take = std::io::Read::by_ref(&mut stream).take(fmd.length);
|
let take = std::io::Read::by_ref(&mut stream).take(fmd.length);
|
||||||
let mut reader = claxon::FlacReader::new(take)?;
|
let mut reader = claxon::FlacReader::new(take)?;
|
||||||
samples.append(
|
samples
|
||||||
&mut reader
|
.extend(&mut reader.samples().map(|x| x.unwrap_or(0) as f32 / 32768.0 / 256.0));
|
||||||
.samples()
|
|
||||||
.map(|x| x.unwrap_or(0) as f32 / 32768.0 / 256.0)
|
|
||||||
.collect::<Vec<f32>>(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "flac"))]
|
#[cfg(not(feature = "flac"))]
|
||||||
|
@ -73,19 +68,9 @@ pub(crate) fn decode(
|
||||||
{
|
{
|
||||||
let mut buf = vec![];
|
let mut buf = vec![];
|
||||||
std::io::Read::by_ref(&mut stream).take(fmd.length).read_to_end(&mut buf)?;
|
std::io::Read::by_ref(&mut stream).take(fmd.length).read_to_end(&mut buf)?;
|
||||||
let mut decoder = vorbis_rs::VorbisDecoder::new(Cursor::new(buf))?;
|
let mut srr = lewton::inside_ogg::OggStreamReader::new(Cursor::new(buf))?;
|
||||||
let mut interleaved = vec![];
|
while let Some(pck_samples) = srr.read_dec_packet_itl()? {
|
||||||
|
samples.extend(pck_samples.iter().map(|x| *x as f32 / 32768.0));
|
||||||
while let Some(decoded_block) = decoder.decode_audio_block()? {
|
|
||||||
let s = decoded_block.samples();
|
|
||||||
interleaved.resize(s[0].len() * s.len(), 0f32);
|
|
||||||
for (ind, channel) in s.iter().enumerate() {
|
|
||||||
for (samind, sample) in channel.iter().enumerate() {
|
|
||||||
interleaved[ind + samind * md.channels as usize] = *sample;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
samples.extend(interleaved);
|
|
||||||
interleaved = vec![];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(not(feature = "vorbis"))]
|
#[cfg(not(feature = "vorbis"))]
|
||||||
|
@ -93,6 +78,19 @@ pub(crate) fn decode(
|
||||||
unimplemented!("vorbis decoding is disabled in library")
|
unimplemented!("vorbis decoding is disabled in library")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Encoder::Sea => {
|
||||||
|
#[cfg(feature = "sea")]
|
||||||
|
{
|
||||||
|
let mut buf = vec![];
|
||||||
|
std::io::Read::by_ref(&mut stream).take(fmd.length).read_to_end(&mut buf)?;
|
||||||
|
let dec = sea_codec::sea_decode(buf.as_slice());
|
||||||
|
samples.extend(dec.samples.iter().map(|x| *x as f32 / 32768.0));
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "sea"))]
|
||||||
|
{
|
||||||
|
unimplemented!("sea decoding is disabled in library")
|
||||||
|
}
|
||||||
|
}
|
||||||
Encoder::Aac | Encoder::Opus | Encoder::WavPack => unimplemented!(),
|
Encoder::Aac | Encoder::Opus | Encoder::WavPack => unimplemented!(),
|
||||||
};
|
};
|
||||||
Ok(samples)
|
Ok(samples)
|
||||||
|
|
|
@ -38,15 +38,17 @@ mod decode;
|
||||||
const CACHE_SIZE_PCM: usize = 32;
|
const CACHE_SIZE_PCM: usize = 32;
|
||||||
const CACHE_SIZE_COMPRESSED: usize = 4;
|
const CACHE_SIZE_COMPRESSED: usize = 4;
|
||||||
|
|
||||||
const SUPPORTED_DECODERS: &[Encoder] = &[
|
pub const SUPPORTED_DECODERS: &[Encoder] = &[
|
||||||
Encoder::Pcm16,
|
|
||||||
Encoder::PcmFloat,
|
|
||||||
#[cfg(feature = "flac")]
|
#[cfg(feature = "flac")]
|
||||||
Encoder::Flac,
|
Encoder::Flac,
|
||||||
#[cfg(feature = "alac")]
|
#[cfg(feature = "alac")]
|
||||||
Encoder::Alac,
|
Encoder::Alac,
|
||||||
#[cfg(feature = "vorbis")]
|
#[cfg(feature = "vorbis")]
|
||||||
Encoder::Vorbis,
|
Encoder::Vorbis,
|
||||||
|
#[cfg(feature = "sea")]
|
||||||
|
Encoder::Sea,
|
||||||
|
Encoder::PcmFloat,
|
||||||
|
Encoder::Pcm16,
|
||||||
];
|
];
|
||||||
|
|
||||||
static SINK: RwLock<Option<Sink>> = RwLock::new(None);
|
static SINK: RwLock<Option<Sink>> = RwLock::new(None);
|
||||||
|
|
|
@ -11,7 +11,7 @@ use symphonia::core::units::Time;
|
||||||
|
|
||||||
use crate::Args;
|
use crate::Args;
|
||||||
|
|
||||||
pub async fn get_meta(file_path: &Path, encoder_wants: u32) -> (u16, u32, Time) {
|
pub fn get_meta(file_path: &Path, encoder_wants: u32) -> (u16, u32, Time) {
|
||||||
let file = Box::new(std::fs::File::open(file_path).unwrap());
|
let file = Box::new(std::fs::File::open(file_path).unwrap());
|
||||||
let mut hint = Hint::new();
|
let mut hint = Hint::new();
|
||||||
hint.with_extension(file_path.extension().unwrap().to_str().unwrap());
|
hint.with_extension(file_path.extension().unwrap().to_str().unwrap());
|
||||||
|
@ -41,12 +41,7 @@ pub async fn get_meta(file_path: &Path, encoder_wants: u32) -> (u16, u32, Time)
|
||||||
let mut sample_rate = 0;
|
let mut sample_rate = 0;
|
||||||
let track_length =
|
let track_length =
|
||||||
track.codec_params.time_base.unwrap().calc_time(track.codec_params.n_frames.unwrap());
|
track.codec_params.time_base.unwrap().calc_time(track.codec_params.n_frames.unwrap());
|
||||||
loop {
|
while let Ok(packet) = format.next_packet() {
|
||||||
let packet = match format.next_packet() {
|
|
||||||
Ok(packet) => packet,
|
|
||||||
_ => break,
|
|
||||||
};
|
|
||||||
|
|
||||||
if packet.track_id() != track_id {
|
if packet.track_id() != track_id {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -112,12 +107,7 @@ pub fn decode_file_stream(file_path: PathBuf, encoder_wants: u32) -> impl Stream
|
||||||
.expect("unsupported codec");
|
.expect("unsupported codec");
|
||||||
let track_id = track.id;
|
let track_id = track.id;
|
||||||
stream! {
|
stream! {
|
||||||
loop {
|
while let Ok(packet) = format.next_packet() {
|
||||||
let packet = match format.next_packet() {
|
|
||||||
Ok(packet) => packet,
|
|
||||||
_ => break,
|
|
||||||
};
|
|
||||||
|
|
||||||
if packet.track_id() != track_id {
|
if packet.track_id() != track_id {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,6 +127,32 @@ pub fn encode(
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Encoder::Sea => {
|
||||||
|
#[cfg(feature = "sea")]
|
||||||
|
{
|
||||||
|
Some((
|
||||||
|
sea_codec::sea_encode(
|
||||||
|
samples
|
||||||
|
.iter_mut()
|
||||||
|
.map(|x| (*x * 32768.0) as i16)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.as_slice(),
|
||||||
|
sample_rate,
|
||||||
|
channels as u32,
|
||||||
|
sea_codec::encoder::EncoderSettings {
|
||||||
|
residual_bits: 5.0,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "sea"))]
|
||||||
|
{
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
Encoder::Aac | Encoder::Opus | Encoder::WavPack => unimplemented!(),
|
Encoder::Aac | Encoder::Opus | Encoder::WavPack => unimplemented!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
109
src/main.rs
|
@ -14,6 +14,7 @@ use encode::encode;
|
||||||
use futures_util::pin_mut;
|
use futures_util::pin_mut;
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use image::ImageReader;
|
use image::ImageReader;
|
||||||
|
use image::RgbImage;
|
||||||
use lofty::Accessor;
|
use lofty::Accessor;
|
||||||
use lofty::TaggedFileExt;
|
use lofty::TaggedFileExt;
|
||||||
use lonelyradio_types::Encoder;
|
use lonelyradio_types::Encoder;
|
||||||
|
@ -33,7 +34,7 @@ use xspf::Playlist;
|
||||||
use crate::decode::decode_file_stream;
|
use crate::decode::decode_file_stream;
|
||||||
use crate::decode::get_meta;
|
use crate::decode::get_meta;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser, Clone)]
|
||||||
struct Args {
|
struct Args {
|
||||||
/// Directory with audio files
|
/// Directory with audio files
|
||||||
dir: PathBuf,
|
dir: PathBuf,
|
||||||
|
@ -67,6 +68,8 @@ const SUPPORTED_ENCODERS: &[Encoder] = &[
|
||||||
Encoder::Alac,
|
Encoder::Alac,
|
||||||
#[cfg(feature = "vorbis")]
|
#[cfg(feature = "vorbis")]
|
||||||
Encoder::Vorbis,
|
Encoder::Vorbis,
|
||||||
|
#[cfg(feature = "sea")]
|
||||||
|
Encoder::Sea,
|
||||||
];
|
];
|
||||||
|
|
||||||
async fn stream_track(
|
async fn stream_track(
|
||||||
|
@ -94,6 +97,7 @@ async fn stream_track(
|
||||||
Encoder::Flac => 16,
|
Encoder::Flac => 16,
|
||||||
Encoder::Alac => 32,
|
Encoder::Alac => 32,
|
||||||
Encoder::Vorbis => 64,
|
Encoder::Vorbis => 64,
|
||||||
|
Encoder::Sea => 64,
|
||||||
Encoder::Aac | Encoder::Opus | Encoder::WavPack => unimplemented!(),
|
Encoder::Aac | Encoder::Opus | Encoder::WavPack => unimplemented!(),
|
||||||
})
|
})
|
||||||
.next()
|
.next()
|
||||||
|
@ -106,7 +110,8 @@ async fn stream_track(
|
||||||
| Encoder::PcmFloat
|
| Encoder::PcmFloat
|
||||||
| Encoder::Flac
|
| Encoder::Flac
|
||||||
| Encoder::Alac
|
| Encoder::Alac
|
||||||
| Encoder::Vorbis => {
|
| Encoder::Vorbis
|
||||||
|
| Encoder::Sea => {
|
||||||
let (encoded, magic_cookie) =
|
let (encoded, magic_cookie) =
|
||||||
encode(md.encoder, _samples, md.sample_rate, md.channels).unwrap();
|
encode(md.encoder, _samples, md.sample_rate, md.channels).unwrap();
|
||||||
let _md = PlayMessage::F(FragmentMetadata {
|
let _md = PlayMessage::F(FragmentMetadata {
|
||||||
|
@ -294,6 +299,56 @@ fn track_valid(track: &Path) -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Metadata {
|
||||||
|
title: String,
|
||||||
|
album: String,
|
||||||
|
artist: String,
|
||||||
|
cover: Option<RgbImage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_metadata(track: impl AsRef<Path>, args: &Args, settings: &Settings) -> Option<Metadata> {
|
||||||
|
let mut title = String::new();
|
||||||
|
let mut artist = String::new();
|
||||||
|
let mut album = String::new();
|
||||||
|
let mut cover = std::thread::spawn(|| None);
|
||||||
|
let mut file = std::fs::File::open(&track).unwrap();
|
||||||
|
let tagged = lofty::read_from(&mut file).ok()?;
|
||||||
|
if let Some(id3v2) = tagged.primary_tag() {
|
||||||
|
title = id3v2
|
||||||
|
.title()
|
||||||
|
.unwrap_or(track.as_ref().file_stem().unwrap().to_string_lossy())
|
||||||
|
.to_string();
|
||||||
|
album = id3v2.album().unwrap_or("".into()).to_string();
|
||||||
|
artist = id3v2.artist().unwrap_or("".into()).to_string();
|
||||||
|
if !(id3v2.pictures().is_empty() || args.artwork == -1 || settings.cover == -1) {
|
||||||
|
let pic = id3v2.pictures()[0].clone();
|
||||||
|
let args = args.clone();
|
||||||
|
let settings = settings.clone();
|
||||||
|
cover = std::thread::spawn(move || {
|
||||||
|
let dec = ImageReader::new(Cursor::new(pic.into_data()))
|
||||||
|
.with_guessed_format()
|
||||||
|
.ok()?
|
||||||
|
.decode()
|
||||||
|
.ok()?;
|
||||||
|
let img = if args.artwork != 0 && settings.cover != 0 {
|
||||||
|
let size = std::cmp::min(args.artwork as u32, settings.cover as u32);
|
||||||
|
dec.resize(size, size, image::imageops::FilterType::Lanczos3)
|
||||||
|
} else {
|
||||||
|
dec
|
||||||
|
}
|
||||||
|
.to_rgb8();
|
||||||
|
Some(img)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
Some(Metadata {
|
||||||
|
title,
|
||||||
|
album,
|
||||||
|
artist,
|
||||||
|
cover: cover.join().unwrap(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async fn stream(mut s: impl Write, tracklist: Arc<Vec<PathBuf>>, settings: Settings) {
|
async fn stream(mut s: impl Write, tracklist: Arc<Vec<PathBuf>>, settings: Settings) {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
let encoder_wants = match settings.encoder {
|
let encoder_wants = match settings.encoder {
|
||||||
|
@ -304,46 +359,20 @@ async fn stream(mut s: impl Write, tracklist: Arc<Vec<PathBuf>>, settings: Setti
|
||||||
loop {
|
loop {
|
||||||
let track = tracklist.choose(&mut thread_rng()).unwrap().clone();
|
let track = tracklist.choose(&mut thread_rng()).unwrap().clone();
|
||||||
|
|
||||||
let mut title = String::new();
|
let Metadata {
|
||||||
let mut artist = String::new();
|
title,
|
||||||
let mut album = String::new();
|
album,
|
||||||
let mut cover = std::thread::spawn(|| None);
|
artist,
|
||||||
let mut file = std::fs::File::open(&track).unwrap();
|
cover,
|
||||||
let tagged = match lofty::read_from(&mut file) {
|
} = match get_metadata(&track, &args, &settings) {
|
||||||
Ok(f) => f,
|
Some(m) => m,
|
||||||
_ => continue,
|
_ => continue,
|
||||||
};
|
};
|
||||||
if let Some(id3v2) = tagged.primary_tag() {
|
|
||||||
title =
|
|
||||||
id3v2.title().unwrap_or(track.file_stem().unwrap().to_string_lossy()).to_string();
|
|
||||||
album = id3v2.album().unwrap_or("".into()).to_string();
|
|
||||||
artist = id3v2.artist().unwrap_or("".into()).to_string();
|
|
||||||
if !(id3v2.pictures().is_empty() || args.artwork == -1 || settings.cover == -1) {
|
|
||||||
let pic = id3v2.pictures()[0].clone();
|
|
||||||
cover = std::thread::spawn(move || {
|
|
||||||
let dec = ImageReader::new(Cursor::new(pic.into_data()))
|
|
||||||
.with_guessed_format()
|
|
||||||
.ok()?
|
|
||||||
.decode()
|
|
||||||
.ok()?;
|
|
||||||
let mut img = Vec::new();
|
|
||||||
if args.artwork != 0 && settings.cover != 0 {
|
|
||||||
let size = std::cmp::min(args.artwork as u32, settings.cover as u32);
|
|
||||||
dec.resize(size, size, image::imageops::FilterType::Lanczos3)
|
|
||||||
} else {
|
|
||||||
dec
|
|
||||||
}
|
|
||||||
.to_rgb8()
|
|
||||||
.write_to(&mut Cursor::new(&mut img), image::ImageFormat::Jpeg)
|
|
||||||
.unwrap();
|
|
||||||
Some(img)
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
let track_message = format!("{} - {} - {}", &artist, &album, &title);
|
let track_message = format!("{} - {} - {}", &artist, &album, &title);
|
||||||
println!("[{}] {} ({:?})", Local::now().to_rfc3339(), track_message, settings.encoder);
|
println!("[{}] {} ({:?})", Local::now().to_rfc3339(), track_message, settings.encoder);
|
||||||
|
|
||||||
let (channels, sample_rate, time) = get_meta(track.as_path(), encoder_wants).await;
|
let (channels, sample_rate, time) = get_meta(track.as_path(), encoder_wants);
|
||||||
let stream = decode_file_stream(track, encoder_wants);
|
let stream = decode_file_stream(track, encoder_wants);
|
||||||
let id = thread_rng().gen();
|
let id = thread_rng().gen();
|
||||||
if stream_track(
|
if stream_track(
|
||||||
|
@ -352,7 +381,11 @@ async fn stream(mut s: impl Write, tracklist: Arc<Vec<PathBuf>>, settings: Setti
|
||||||
track_length_frac: time.frac as f32,
|
track_length_frac: time.frac as f32,
|
||||||
track_length_secs: time.seconds,
|
track_length_secs: time.seconds,
|
||||||
encoder: settings.encoder,
|
encoder: settings.encoder,
|
||||||
cover: cover.join().unwrap(),
|
cover: cover.map(|x| {
|
||||||
|
let mut buf = Cursor::new(Vec::new());
|
||||||
|
x.write_to(&mut buf, image::ImageFormat::Jpeg).unwrap();
|
||||||
|
buf.into_inner()
|
||||||
|
}),
|
||||||
id,
|
id,
|
||||||
album,
|
album,
|
||||||
artist,
|
artist,
|
||||||
|
|