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]
|
||||
name = "lonelyradio"
|
||||
description = "TCP radio for lonely ones"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
|
||||
|
@ -49,14 +49,16 @@ samplerate = "0.2.4"
|
|||
flacenc = { version = "0.4.0", default-features = false, optional = true }
|
||||
alac-encoder = { version = "0.3.0", optional = true }
|
||||
vorbis_rs = {version = "0.5.4", optional = true }
|
||||
sea-codec = { version = "0.5.2", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["all-lossless", "all-lossy"]
|
||||
all-lossless = ["alac", "flac"]
|
||||
all-lossy = ["vorbis"]
|
||||
all-lossy = ["vorbis", "sea"]
|
||||
alac = ["dep:alac-encoder"]
|
||||
flac = ["dep:flacenc"]
|
||||
vorbis = ["dep:vorbis_rs"]
|
||||
sea = ["dep:sea-codec"]
|
||||
|
||||
[profile.distribute]
|
||||
inherits = "release"
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
# Autogenerated by docker init
|
||||
# https://docs.docker.com/go/dockerfile-reference/
|
||||
|
||||
ARG RUST_VERSION=1.80.1
|
||||
ARG RUST_VERSION=1.85
|
||||
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))
|
||||
|
||||
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
|
||||
|
||||
```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
|
||||
|
@ -57,7 +57,7 @@ Only the `<location>` and (playlist's) element would be used and only `file://`
|
|||
##### Install
|
||||
|
||||
```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.
|
||||
|
@ -71,7 +71,7 @@ Desktop integration will be added later.
|
|||
##### Install monoclient
|
||||
|
||||
```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
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use std::fmt::Display;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const HELLO_MAGIC: &[u8; 8] = b"lonelyra";
|
||||
|
@ -99,6 +101,7 @@ pub enum Encoder {
|
|||
Opus = 5,
|
||||
Aac = 6,
|
||||
Vorbis = 7,
|
||||
Sea = 8,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||
|
@ -119,3 +122,19 @@ pub struct FragmentMetadata {
|
|||
fn none() -> Option<Vec<u8>> {
|
||||
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"
|
||||
version = "0.7.0"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
slint = { version = "1.6" }
|
||||
slint = { version = "1.8" }
|
||||
monolib = { path = "../monolib", version = "0.7.1" }
|
||||
zune-jpeg = "0.4.13"
|
||||
|
||||
[build-dependencies]
|
||||
slint-build = "1.8"
|
||||
|
||||
[package.metadata.bundle]
|
||||
name = "monoclient-s"
|
||||
identifier = "dev.ivabus.monoclient-s"
|
||||
icon = ["lonelyradio.png", "lonelyradio.icns"]
|
||||
version = "0.5.0"
|
||||
version = "0.7.1"
|
||||
copyright = "Copyright (c) 2024 Ivan Bushchik."
|
||||
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,
|
||||
};
|
||||
|
||||
slint::slint! {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
slint::include_modules!();
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn start_playback(window_weak: Weak<MainWindow>) {
|
||||
let window = window_weak.upgrade().unwrap();
|
||||
let addr = window.get_addr().to_string();
|
||||
let playlist = window.get_selected_playlist();
|
||||
let encoder = monolib::SUPPORTED_DECODERS[window.get_selected_encoder() as usize];
|
||||
let handle = std::thread::spawn(move || {
|
||||
monolib::run(
|
||||
&addr,
|
||||
lonelyradio_types::Settings {
|
||||
encoder: lonelyradio_types::Encoder::Flac,
|
||||
cover: 512,
|
||||
encoder,
|
||||
cover: 2048,
|
||||
},
|
||||
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();
|
||||
window.on_next(move || {
|
||||
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_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
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[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = "";
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 0.6.0;
|
||||
MARKETING_VERSION = 0.7.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "dev.ivabus.monoclient-x";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -405,12 +405,12 @@
|
|||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
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[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = "${PROJECT_DIR/../target/aarch64-apple-darwin/release}";
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 0.6.0;
|
||||
MARKETING_VERSION = 0.7.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "dev.ivabus.monoclient-x";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
|
@ -5,11 +5,10 @@
|
|||
// Created by ivabus on 13.06.2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AVFAudio
|
||||
import MediaPlayer
|
||||
import MonoLib
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum PlayerState {
|
||||
case NotStarted
|
||||
|
@ -17,11 +16,12 @@ enum PlayerState {
|
|||
case Paused
|
||||
|
||||
mutating func update() {
|
||||
self = switch c_get_state() {
|
||||
case 2: PlayerState.Playing
|
||||
case 3: PlayerState.Paused
|
||||
default: PlayerState.NotStarted
|
||||
}
|
||||
self =
|
||||
switch c_get_state() {
|
||||
case 2: PlayerState.Playing
|
||||
case 3: PlayerState.Paused
|
||||
default: PlayerState.NotStarted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,10 +30,11 @@ enum EncoderType: UInt8 {
|
|||
case PCMFloat = 1
|
||||
case FLAC = 2
|
||||
case Alac = 3
|
||||
//WavPack = 4,
|
||||
//Opus = 5,
|
||||
//Aac = 6,
|
||||
//WavPack = 4,
|
||||
//Opus = 5,
|
||||
//Aac = 6,
|
||||
case Vorbis = 7
|
||||
case Sea = 8
|
||||
}
|
||||
|
||||
enum CoverSize: Int32 {
|
||||
|
@ -51,10 +52,10 @@ struct PlayList: Identifiable, Hashable {
|
|||
var name: String
|
||||
}
|
||||
|
||||
|
||||
struct Settings {
|
||||
var encoder: EncoderType = EncoderType.FLAC
|
||||
var cover_size: CoverSize = CoverSize.High/*
|
||||
var cover_size: CoverSize = CoverSize
|
||||
.High /*
|
||||
init(enc: EncoderType, cov: CoverSize) {
|
||||
encoder = enc
|
||||
cover_size = cov
|
||||
|
@ -62,9 +63,9 @@ struct Settings {
|
|||
}
|
||||
|
||||
#if os(tvOS)
|
||||
typealias MyStack = HStack
|
||||
typealias MyStack = HStack
|
||||
#else
|
||||
typealias MyStack = VStack
|
||||
typealias MyStack = VStack
|
||||
#endif
|
||||
|
||||
struct Player: View {
|
||||
|
@ -81,32 +82,30 @@ struct Player: View {
|
|||
|
||||
var body: some View {
|
||||
|
||||
|
||||
MyStack(alignment: .center) {
|
||||
|
||||
VStack(alignment: .center) {
|
||||
|
||||
#if os(macOS)
|
||||
Image(nsImage: cover.cover)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(minWidth: 256, maxWidth: 256, minHeight: 256, maxHeight: 256)
|
||||
.frame(width: 256.0, height: 256.0)
|
||||
.clipShape(.rect(cornerRadius: 24))
|
||||
.shadow(radius: 16)
|
||||
.padding(16)
|
||||
#else
|
||||
Image(uiImage: cover.cover)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(minWidth: 256, maxWidth: 256, minHeight: 256, maxHeight: 256)
|
||||
.frame(width: 256.0, height: 256.0)
|
||||
.clipShape(.rect(cornerRadius: 24))
|
||||
.shadow(radius: 16)
|
||||
.padding(16)
|
||||
#endif
|
||||
#if os(macOS)
|
||||
Image(nsImage: cover.cover)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(minWidth: 256, maxWidth: 256, minHeight: 256, maxHeight: 256)
|
||||
.frame(width: 256.0, height: 256.0)
|
||||
.clipShape(.rect(cornerRadius: 24))
|
||||
.shadow(radius: 16)
|
||||
.padding(16)
|
||||
#else
|
||||
Image(uiImage: cover.cover)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(minWidth: 256, maxWidth: 256, minHeight: 256, maxHeight: 256)
|
||||
.frame(width: 256.0, height: 256.0)
|
||||
.clipShape(.rect(cornerRadius: 24))
|
||||
.shadow(radius: 16)
|
||||
.padding(16)
|
||||
#endif
|
||||
|
||||
VStack(alignment: .center){
|
||||
VStack(alignment: .center) {
|
||||
Text(metadata.title).bold()
|
||||
|
||||
Text(metadata.album)
|
||||
|
@ -119,14 +118,15 @@ struct Player: View {
|
|||
cover.update()
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
#if os(macOS)
|
||||
MPNowPlayingInfoCenter.default().playbackState = state == PlayerState.Playing ? .playing : .paused
|
||||
#endif
|
||||
#if os(macOS)
|
||||
MPNowPlayingInfoCenter.default().playbackState =
|
||||
state == PlayerState.Playing ? .playing : .paused
|
||||
#endif
|
||||
|
||||
let nowPlayingInfo: [String: Any] = [
|
||||
MPMediaItemPropertyArtist: metadata.artist,
|
||||
|
@ -144,11 +144,11 @@ struct Player: View {
|
|||
"Server",
|
||||
text: $server,
|
||||
onCommit: {
|
||||
#if os(macOS)
|
||||
DispatchQueue.main.async {
|
||||
NSApp.keyWindow?.makeFirstResponder(nil)
|
||||
}
|
||||
#endif
|
||||
#if os(macOS)
|
||||
DispatchQueue.main.async {
|
||||
NSApp.keyWindow?.makeFirstResponder(nil)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
)
|
||||
.disableAutocorrection(true)
|
||||
|
@ -158,7 +158,7 @@ struct Player: View {
|
|||
.multilineTextAlignment(.center)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Button(action: stop){
|
||||
Button(action: stop) {
|
||||
Image(systemName: "stop.fill").padding(4).frame(width: 32, height: 24)
|
||||
}
|
||||
.disabled(state == PlayerState.NotStarted)
|
||||
|
@ -166,16 +166,22 @@ struct Player: View {
|
|||
.font(.system(size: 20))
|
||||
.buttonBorderShape(.capsule)
|
||||
|
||||
Button(action: play){
|
||||
Image(systemName: state == PlayerState.NotStarted ? "infinity.circle" : (state == PlayerState.Playing) ? "pause.circle.fill" : "play.circle" )
|
||||
.font(.system(size: 30))
|
||||
.padding(4)
|
||||
Button(action: play) {
|
||||
Image(
|
||||
systemName: state == PlayerState.NotStarted
|
||||
? "infinity.circle"
|
||||
: (state == PlayerState.Playing)
|
||||
? "pause.circle.fill" : "play.circle"
|
||||
)
|
||||
.font(.system(size: 30))
|
||||
.padding(4)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.buttonBorderShape(.capsule)
|
||||
|
||||
Button(action: next){
|
||||
Image(systemName: "forward.end.fill").padding(4).frame(width: 32, height: 24)
|
||||
Button(action: next) {
|
||||
Image(systemName: "forward.end.fill").padding(4).frame(
|
||||
width: 32, height: 24)
|
||||
}.disabled(state == PlayerState.NotStarted)
|
||||
.buttonStyle(.bordered)
|
||||
.font(.system(size: 20))
|
||||
|
@ -185,7 +191,7 @@ struct Player: View {
|
|||
}
|
||||
Menu {
|
||||
Picker("Playlist", selection: $playlist) {
|
||||
ForEach ($playlists) { pl in
|
||||
ForEach($playlists) { pl in
|
||||
Text(pl.wrappedValue.name).tag(pl.wrappedValue)
|
||||
}
|
||||
|
||||
|
@ -202,6 +208,8 @@ struct Player: View {
|
|||
.tag(EncoderType.Alac)
|
||||
Text("Vorbis (lossy)")
|
||||
.tag(EncoderType.Vorbis)
|
||||
Text("Sea (lossy)")
|
||||
.tag(EncoderType.Sea)
|
||||
}.pickerStyle(.menu)
|
||||
|
||||
Picker("Cover size", selection: $settings.cover_size) {
|
||||
|
@ -227,18 +235,21 @@ struct Player: View {
|
|||
.padding(32)
|
||||
.onReceive(timer_playlists) { _ in
|
||||
var id = -1
|
||||
playlists = (["All tracks"] + String(cString: c_list_playlists(server)).components(separatedBy: "\n")).map({elem in
|
||||
if elem.isEmpty {
|
||||
return PlayList(id: -1, name: elem)
|
||||
}
|
||||
id += 1;
|
||||
return PlayList(id: id, name: elem)
|
||||
}).filter({elem in elem.id != -1})
|
||||
playlists =
|
||||
(["All tracks"]
|
||||
+ String(cString: c_list_playlists(server)).components(separatedBy: "\n")).map({
|
||||
elem in
|
||||
if elem.isEmpty {
|
||||
return PlayList(id: -1, name: elem)
|
||||
}
|
||||
id += 1
|
||||
return PlayList(id: id, name: elem)
|
||||
}).filter({ elem in elem.id != -1 })
|
||||
}
|
||||
.onAppear() {
|
||||
#if os(iOS)
|
||||
UIApplication.shared.beginReceivingRemoteControlEvents()
|
||||
#endif
|
||||
.onAppear {
|
||||
#if os(iOS)
|
||||
UIApplication.shared.beginReceivingRemoteControlEvents()
|
||||
#endif
|
||||
MPRemoteCommandCenter.shared().previousTrackCommand.isEnabled = false
|
||||
MPRemoteCommandCenter.shared().nextTrackCommand.isEnabled = true
|
||||
MPRemoteCommandCenter.shared().skipForwardCommand.isEnabled = false
|
||||
|
@ -256,49 +267,52 @@ struct Player: View {
|
|||
return MPRemoteCommandHandlerStatus.success
|
||||
})
|
||||
|
||||
MPRemoteCommandCenter.shared().togglePlayPauseCommand.addTarget(handler: {_ in
|
||||
MPRemoteCommandCenter.shared().togglePlayPauseCommand.addTarget(handler: { _ in
|
||||
play()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
})
|
||||
|
||||
MPRemoteCommandCenter.shared().nextTrackCommand.addTarget(handler: {_ in
|
||||
MPRemoteCommandCenter.shared().nextTrackCommand.addTarget(handler: { _ in
|
||||
next()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
.animation(.spring, value: UUID())
|
||||
|
||||
}
|
||||
.animation(.spring, value: UUID())
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
func play() {
|
||||
switch state {
|
||||
case PlayerState.NotStarted: do {
|
||||
/*#if os(macOS)
|
||||
#else*/
|
||||
let audioSession = AVAudioSession.sharedInstance()
|
||||
case PlayerState.NotStarted:
|
||||
do {
|
||||
try audioSession.setCategory(
|
||||
.playback, mode: .default)
|
||||
try audioSession.setActive(true)
|
||||
#if os(macOS)
|
||||
#else
|
||||
let audioSession = AVAudioSession.sharedInstance()
|
||||
do {
|
||||
try audioSession.setCategory(
|
||||
.playback, mode: .default)
|
||||
try audioSession.setActive(true)
|
||||
|
||||
} catch {
|
||||
print("Failed to set the audio session configuration")
|
||||
} catch {
|
||||
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*/
|
||||
Thread.detachNewThread {
|
||||
c_start(server, CSettings(encoder: settings.encoder.rawValue, cover: settings.cover_size.rawValue), playlist.name == "All tracks" ? "" : playlist.name)
|
||||
default:
|
||||
do {
|
||||
c_toggle()
|
||||
state.update()
|
||||
}
|
||||
}
|
||||
default: do {
|
||||
c_toggle()
|
||||
state.update()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
func stop() {
|
||||
|
|
|
@ -57,7 +57,7 @@ fn main() {
|
|||
monolib::run(
|
||||
&args.address,
|
||||
Settings {
|
||||
encoder: Encoder::Flac,
|
||||
encoder: Encoder::Sea,
|
||||
cover: -1,
|
||||
},
|
||||
&args.playlist,
|
||||
|
|
|
@ -3,7 +3,7 @@ name = "monolib"
|
|||
version = "0.7.1"
|
||||
edition = "2021"
|
||||
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"
|
||||
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
|
||||
|
||||
|
@ -22,15 +22,17 @@ anyhow = "1.0.86"
|
|||
claxon = { version = "0.4.3", optional = true }
|
||||
symphonia-codec-alac = {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]
|
||||
default = ["all-lossless", "all-lossy"]
|
||||
all-lossless = ["alac", "flac"]
|
||||
all-lossy = ["vorbis"]
|
||||
all-lossy = ["vorbis", "sea"]
|
||||
alac = ["dep:symphonia-codec-alac", "dep:symphonia-core"]
|
||||
flac = ["dep:claxon"]
|
||||
vorbis = ["dep:vorbis_rs"]
|
||||
vorbis = ["dep:lewton"]
|
||||
sea = ["dep:sea-codec"]
|
||||
|
||||
[package.metadata.xcframework]
|
||||
include-dir = "src"
|
||||
|
@ -38,5 +40,4 @@ lib-type = "cdylib"
|
|||
zip = false
|
||||
macOS = true
|
||||
iOS = true
|
||||
#tvOS = true
|
||||
simulators = true
|
||||
simulators = false
|
||||
|
|
|
@ -35,6 +35,7 @@ pub extern "C" fn c_start(server: *const c_char, settings: CSettings, playlist:
|
|||
2 => Encoder::Flac,
|
||||
3 => Encoder::Alac,
|
||||
7 => Encoder::Vorbis,
|
||||
8 => Encoder::Sea,
|
||||
_ => return,
|
||||
},
|
||||
cover: settings.cover,
|
||||
|
|
|
@ -18,8 +18,7 @@ pub(crate) fn decode(
|
|||
Encoder::Pcm16 => {
|
||||
let mut samples_i16 = vec![0; fmd.length as usize / 2];
|
||||
stream.read_i16_into::<LittleEndian>(&mut samples_i16)?;
|
||||
samples
|
||||
.append(&mut samples_i16.iter().map(|sample| *sample as f32 / 32767.0).collect());
|
||||
samples.extend(samples_i16.iter().map(|sample| *sample as f32 / 32767.0));
|
||||
}
|
||||
Encoder::PcmFloat => {
|
||||
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 mut reader = claxon::FlacReader::new(take)?;
|
||||
samples.append(
|
||||
&mut reader
|
||||
.samples()
|
||||
.map(|x| x.unwrap_or(0) as f32 / 32768.0 / 256.0)
|
||||
.collect::<Vec<f32>>(),
|
||||
);
|
||||
samples
|
||||
.extend(&mut reader.samples().map(|x| x.unwrap_or(0) as f32 / 32768.0 / 256.0));
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "flac"))]
|
||||
|
@ -73,19 +68,9 @@ pub(crate) fn decode(
|
|||
{
|
||||
let mut buf = vec![];
|
||||
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 interleaved = vec![];
|
||||
|
||||
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![];
|
||||
let mut srr = lewton::inside_ogg::OggStreamReader::new(Cursor::new(buf))?;
|
||||
while let Some(pck_samples) = srr.read_dec_packet_itl()? {
|
||||
samples.extend(pck_samples.iter().map(|x| *x as f32 / 32768.0));
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "vorbis"))]
|
||||
|
@ -93,6 +78,19 @@ pub(crate) fn decode(
|
|||
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!(),
|
||||
};
|
||||
Ok(samples)
|
||||
|
|
|
@ -38,15 +38,17 @@ mod decode;
|
|||
const CACHE_SIZE_PCM: usize = 32;
|
||||
const CACHE_SIZE_COMPRESSED: usize = 4;
|
||||
|
||||
const SUPPORTED_DECODERS: &[Encoder] = &[
|
||||
Encoder::Pcm16,
|
||||
Encoder::PcmFloat,
|
||||
pub const SUPPORTED_DECODERS: &[Encoder] = &[
|
||||
#[cfg(feature = "flac")]
|
||||
Encoder::Flac,
|
||||
#[cfg(feature = "alac")]
|
||||
Encoder::Alac,
|
||||
#[cfg(feature = "vorbis")]
|
||||
Encoder::Vorbis,
|
||||
#[cfg(feature = "sea")]
|
||||
Encoder::Sea,
|
||||
Encoder::PcmFloat,
|
||||
Encoder::Pcm16,
|
||||
];
|
||||
|
||||
static SINK: RwLock<Option<Sink>> = RwLock::new(None);
|
||||
|
|
|
@ -11,7 +11,7 @@ use symphonia::core::units::Time;
|
|||
|
||||
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 mut hint = Hint::new();
|
||||
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 track_length =
|
||||
track.codec_params.time_base.unwrap().calc_time(track.codec_params.n_frames.unwrap());
|
||||
loop {
|
||||
let packet = match format.next_packet() {
|
||||
Ok(packet) => packet,
|
||||
_ => break,
|
||||
};
|
||||
|
||||
while let Ok(packet) = format.next_packet() {
|
||||
if packet.track_id() != track_id {
|
||||
continue;
|
||||
}
|
||||
|
@ -112,12 +107,7 @@ pub fn decode_file_stream(file_path: PathBuf, encoder_wants: u32) -> impl Stream
|
|||
.expect("unsupported codec");
|
||||
let track_id = track.id;
|
||||
stream! {
|
||||
loop {
|
||||
let packet = match format.next_packet() {
|
||||
Ok(packet) => packet,
|
||||
_ => break,
|
||||
};
|
||||
|
||||
while let Ok(packet) = format.next_packet() {
|
||||
if packet.track_id() != track_id {
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -127,6 +127,32 @@ pub fn encode(
|
|||
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!(),
|
||||
}
|
||||
}
|
||||
|
|
109
src/main.rs
|
@ -14,6 +14,7 @@ use encode::encode;
|
|||
use futures_util::pin_mut;
|
||||
use futures_util::StreamExt;
|
||||
use image::ImageReader;
|
||||
use image::RgbImage;
|
||||
use lofty::Accessor;
|
||||
use lofty::TaggedFileExt;
|
||||
use lonelyradio_types::Encoder;
|
||||
|
@ -33,7 +34,7 @@ use xspf::Playlist;
|
|||
use crate::decode::decode_file_stream;
|
||||
use crate::decode::get_meta;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[derive(Parser, Clone)]
|
||||
struct Args {
|
||||
/// Directory with audio files
|
||||
dir: PathBuf,
|
||||
|
@ -67,6 +68,8 @@ const SUPPORTED_ENCODERS: &[Encoder] = &[
|
|||
Encoder::Alac,
|
||||
#[cfg(feature = "vorbis")]
|
||||
Encoder::Vorbis,
|
||||
#[cfg(feature = "sea")]
|
||||
Encoder::Sea,
|
||||
];
|
||||
|
||||
async fn stream_track(
|
||||
|
@ -94,6 +97,7 @@ async fn stream_track(
|
|||
Encoder::Flac => 16,
|
||||
Encoder::Alac => 32,
|
||||
Encoder::Vorbis => 64,
|
||||
Encoder::Sea => 64,
|
||||
Encoder::Aac | Encoder::Opus | Encoder::WavPack => unimplemented!(),
|
||||
})
|
||||
.next()
|
||||
|
@ -106,7 +110,8 @@ async fn stream_track(
|
|||
| Encoder::PcmFloat
|
||||
| Encoder::Flac
|
||||
| Encoder::Alac
|
||||
| Encoder::Vorbis => {
|
||||
| Encoder::Vorbis
|
||||
| Encoder::Sea => {
|
||||
let (encoded, magic_cookie) =
|
||||
encode(md.encoder, _samples, md.sample_rate, md.channels).unwrap();
|
||||
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) {
|
||||
let args = Args::parse();
|
||||
let encoder_wants = match settings.encoder {
|
||||
|
@ -304,46 +359,20 @@ async fn stream(mut s: impl Write, tracklist: Arc<Vec<PathBuf>>, settings: Setti
|
|||
loop {
|
||||
let track = tracklist.choose(&mut thread_rng()).unwrap().clone();
|
||||
|
||||
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 = match lofty::read_from(&mut file) {
|
||||
Ok(f) => f,
|
||||
let Metadata {
|
||||
title,
|
||||
album,
|
||||
artist,
|
||||
cover,
|
||||
} = match get_metadata(&track, &args, &settings) {
|
||||
Some(m) => m,
|
||||
_ => 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);
|
||||
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 id = thread_rng().gen();
|
||||
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_secs: time.seconds,
|
||||
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,
|
||||
album,
|
||||
artist,
|
||||
|
|