├── .gitignore
├── data
├── screenshots
│ ├── amberol-full.png
│ ├── amberol-compact.png
│ ├── amberol-playlist.png
│ └── amberol-recolor.png
├── io.bassi.Amberol.service.in
├── icons
│ ├── meson.build
│ └── hicolor
│ │ ├── symbolic
│ │ └── apps
│ │ │ ├── io.bassi.Amberol-symbolic.svg
│ │ │ └── io.bassi.Amberol.Devel-symbolic.svg
│ │ └── scalable
│ │ └── apps
│ │ ├── io.bassi.Amberol.svg
│ │ └── io.bassi.Amberol.Devel.svg
├── io.bassi.Amberol.gschema.xml
├── io.bassi.Amberol.desktop.in.in
└── meson.build
├── src
├── gtk
│ ├── style-hc.css
│ ├── volume-control.ui
│ ├── song-details.ui
│ ├── song-cover.ui
│ ├── style.css
│ ├── help-overlay.ui
│ ├── queue-row.ui
│ ├── playlist-view.ui
│ └── playback-control.ui
├── assets
│ └── icons
│ │ ├── app-remove-symbolic.svg
│ │ ├── media-playback-pause-symbolic.svg
│ │ ├── view-queue-symbolic.svg
│ │ ├── media-playback-start-symbolic.svg
│ │ ├── view-queue-rtl-symbolic.svg
│ │ ├── go-previous-symbolic.svg
│ │ ├── media-playlist-consecutive-symbolic.svg
│ │ ├── media-skip-backward-symbolic.svg
│ │ ├── media-skip-forward-symbolic.svg
│ │ ├── folder-music-symbolic.svg
│ │ ├── edit-select-all-symbolic.svg
│ │ ├── edit-clear-all-symbolic.svg
│ │ ├── media-playlist-repeat-symbolic.svg
│ │ ├── selection-mode-symbolic.svg
│ │ ├── media-playlist-repeat-song-symbolic.svg
│ │ ├── media-playlist-shuffle-symbolic.svg
│ │ └── audio-only-symbolic.svg
├── config.rs.in
├── audio
│ ├── controller.rs
│ ├── inhibit_controller.rs
│ ├── mod.rs
│ ├── shuffle.rs
│ ├── state.rs
│ ├── gst_backend.rs
│ ├── cover_cache.rs
│ ├── mpris_controller.rs
│ └── waveform_generator.rs
├── meson.build
├── song_details.rs
├── song_cover.rs
├── amberol.gresource.xml
├── search.rs
├── main.rs
├── sort.rs
├── playback_control.rs
├── playlist_view.rs
├── drag_overlay.rs
├── cover_picture.rs
├── volume_control.rs
├── i18n.rs
└── queue_row.rs
├── meson_options.txt
├── .typos.toml
├── po
├── LINGUAS
├── meson.build
└── POTFILES.in
├── .rustfmt.toml
├── .editorconfig
├── .gitlab-ci.yml
├── amberol.doap
├── Cargo.toml
├── RELEASING.md
├── io.bassi.Amberol.json
├── .reuse
└── dep5
├── meson.build
├── README.md
├── LICENSES
└── CC0-1.0.txt
└── CONTRIBUTING.md
/.gitignore:
--------------------------------------------------------------------------------
1 | *.json~
2 |
3 | src/config.rs
4 |
5 | target
6 |
--------------------------------------------------------------------------------
/data/screenshots/amberol-full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamjatim/amberol/HEAD/data/screenshots/amberol-full.png
--------------------------------------------------------------------------------
/data/io.bassi.Amberol.service.in:
--------------------------------------------------------------------------------
1 | [D-BUS Service]
2 | Name=@application_id@
3 | Exec=@bindir@/amberol --gapplication-service
4 |
--------------------------------------------------------------------------------
/data/screenshots/amberol-compact.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamjatim/amberol/HEAD/data/screenshots/amberol-compact.png
--------------------------------------------------------------------------------
/data/screenshots/amberol-playlist.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamjatim/amberol/HEAD/data/screenshots/amberol-playlist.png
--------------------------------------------------------------------------------
/data/screenshots/amberol-recolor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adamjatim/amberol/HEAD/data/screenshots/amberol-recolor.png
--------------------------------------------------------------------------------
/src/gtk/style-hc.css:
--------------------------------------------------------------------------------
1 | .main-window button:focus:focus-visible,
2 | .main-window row:focus:focus-visible,
3 | .main-window scale:focus:focus-visible > trough {
4 | outline-color: alpha(currentColor, .5);
5 | }
6 |
--------------------------------------------------------------------------------
/src/assets/icons/app-remove-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/meson_options.txt:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2022 Emmanuele Bassi
2 | # SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | option (
5 | 'profile',
6 | type: 'combo',
7 | choices: [
8 | 'default',
9 | 'development'
10 | ],
11 | value: 'default',
12 | )
13 |
--------------------------------------------------------------------------------
/.typos.toml:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2022 Emmanuele Bassi
2 | # SPDX-License-Identifier: CC0-1.0
3 | [files]
4 | extend-exclude = ["LICENSES/*", "po/*"]
5 |
6 | [default.extend-identifiers]
7 | # MPRIS has a bad API, and mpris-server goes along with it
8 | Seeked = "Seeked"
9 | seeked = "seeked"
10 |
--------------------------------------------------------------------------------
/po/LINGUAS:
--------------------------------------------------------------------------------
1 | be
2 | bg
3 | ca
4 | cs
5 | da
6 | de
7 | el
8 | en_GB
9 | es
10 | eu
11 | fa
12 | fi
13 | fr
14 | fur
15 | gl
16 | he
17 | hi
18 | hr
19 | hu
20 | ia
21 | id
22 | is
23 | it
24 | ka
25 | ko
26 | lt
27 | my
28 | nb
29 | ne
30 | nl
31 | oc
32 | pl
33 | pt
34 | pt_BR
35 | ro
36 | ru
37 | sl
38 | sr
39 | sv
40 | tr
41 | uk
42 | zh_CN
43 |
--------------------------------------------------------------------------------
/.rustfmt.toml:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2022 Emmanuele Bassi
2 | # SPDX-License-Identifier: CC0-1.0
3 | condense_wildcard_suffixes = true
4 | format_code_in_doc_comments = true
5 | group_imports = "StdExternalCrate"
6 | imports_granularity = "Crate"
7 | newline_style = "Unix"
8 | normalize_comments = true
9 | normalize_doc_attributes = true
10 | wrap_comments = true
11 |
--------------------------------------------------------------------------------
/po/meson.build:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2022 Emmanuele Bassi
2 | # SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | i18n.gettext(
5 | 'amberol',
6 | args: [
7 | '--keyword=i18n',
8 | '--keyword=i18n_f',
9 | '--keyword=i18n_k',
10 | '--keyword=ni18n:1,2',
11 | '--keyword=ni18n_f:1,2',
12 | '--keyword=ni18n_k:1,2',
13 | ],
14 | preset: 'glib',
15 | )
16 |
--------------------------------------------------------------------------------
/src/config.rs.in:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022 Emmanuele Bassi
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | pub static VERSION: &str = @VERSION@;
5 | pub static GETTEXT_PACKAGE: &str = @GETTEXT_PACKAGE@;
6 | pub static LOCALEDIR: &str = @LOCALEDIR@;
7 | pub static PKGDATADIR: &str = @PKGDATADIR@;
8 | pub static APPLICATION_ID: &str = @APPLICATION_ID@;
9 | pub static PROFILE: &str = @PROFILE@;
10 |
--------------------------------------------------------------------------------
/po/POTFILES.in:
--------------------------------------------------------------------------------
1 | data/io.bassi.Amberol.desktop.in.in
2 | data/io.bassi.Amberol.gschema.xml
3 | data/io.bassi.Amberol.metainfo.xml.in.in
4 | src/audio/inhibit_controller.rs
5 | src/audio/song.rs
6 | src/gtk/help-overlay.ui
7 | src/gtk/playback-control.ui
8 | src/gtk/playlist-view.ui
9 | src/gtk/queue-row.ui
10 | src/gtk/window.ui
11 | src/application.rs
12 | src/cover_picture.rs
13 | src/playback_control.rs
14 | src/window.rs
15 |
--------------------------------------------------------------------------------
/src/audio/controller.rs:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022 Emmanuele Bassi
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | use crate::audio::{PlaybackState, RepeatMode, Song};
5 |
6 | pub trait Controller {
7 | fn set_playback_state(&self, state: &PlaybackState);
8 |
9 | fn set_song(&self, song: &Song);
10 | fn set_position(&self, position: u64);
11 | fn set_repeat_mode(&self, repeat: RepeatMode);
12 | }
13 |
--------------------------------------------------------------------------------
/data/icons/meson.build:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2022 Emmanuele Bassi
2 | # SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | scalable_dir = 'hicolor/scalable/apps'
5 | install_data(
6 | scalable_dir / ('@0@.svg').format(application_id),
7 | install_dir: get_option('datadir') / 'icons' / scalable_dir,
8 | )
9 |
10 | symbolic_dir = 'hicolor/symbolic/apps'
11 | install_data(
12 | symbolic_dir / ('@0@-symbolic.svg').format(application_id),
13 | install_dir: get_option('datadir') / 'icons' / symbolic_dir,
14 | )
15 |
--------------------------------------------------------------------------------
/src/assets/icons/media-playback-pause-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2022 Emmanuele Bassi
2 | # SPDX-License-Identifier: CC0-1.0
3 |
4 | root = true
5 |
6 | [*]
7 | charset = utf-8
8 | end_of_line = lf
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 | indent_style = space
12 | indent_size = 4
13 |
14 | [*.css]
15 | indent_size = 2
16 |
17 | [*.ui]
18 | indent_size = 2
19 |
20 | [*.{xml,xml.in,xml.in.in}]
21 | indent_size = 2
22 |
23 | [meson.build]
24 | indent_size = 2
25 |
26 | [*.md]
27 | max_line_length = 80
28 | trim_trailing_whitespace = false
29 |
30 | [*.json]
31 | indent_size = 4
32 |
--------------------------------------------------------------------------------
/src/assets/icons/view-queue-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/assets/icons/media-playback-start-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/view-queue-rtl-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/assets/icons/go-previous-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/media-playlist-consecutive-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/media-skip-backward-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/src/assets/icons/media-skip-forward-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/data/icons/hicolor/symbolic/apps/io.bassi.Amberol-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/data/icons/hicolor/symbolic/apps/io.bassi.Amberol.Devel-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/folder-music-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/data/io.bassi.Amberol.gschema.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | 600
11 |
12 |
13 | 300
14 |
15 |
16 | true
17 |
18 |
19 | 'off'
20 |
21 |
22 | true
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/data/io.bassi.Amberol.desktop.in.in:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Name=Amberol
3 | GenericName=Music Player
4 | TryExec=amberol
5 | Exec=amberol %U
6 | Icon=@APPLICATION_ID@
7 | Terminal=false
8 | Type=Application
9 | Categories=GNOME;GTK;Music;Audio;AudioVideo;
10 | # Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!
11 | Keywords=music;player;media;audio;playlist;
12 | StartupNotify=true
13 | X-SingleMainWindow=true
14 | X-Purism-FormFactor=Workstation;Mobile;
15 | DBusActivatable=true
16 | MimeType=audio/mpeg;audio/wav;audio/x-aac;audio/x-aiff;audio/x-ape;audio/x-flac;audio/x-m4a;audio/x-m4b;audio/x-mp1;audio/x-mp2;audio/x-mp3;audio/x-mpg;audio/x-mpeg;audio/x-mpegurl;audio/x-opus+ogg;audio/x-pn-aiff;audio/x-pn-au;audio/x-pn-wav;audio/x-speex;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-wavpack;inode/directory;
17 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | include:
2 | - project: 'gnome/citemplates'
3 | file: 'flatpak/flatpak-ci-initiative-sdk-extensions.yml'
4 | # ref: ''
5 |
6 | stages:
7 | - check
8 | - test
9 | - deploy
10 |
11 | flatpak:
12 | extends: ".flatpak"
13 | variables:
14 | APP_ID: "io.bassi.Amberol.Devel"
15 | BUNDLE: "io.bassi.Amberol.Devel.flatpak"
16 | FLATPAK_MODULE: "amberol"
17 | MANIFEST_PATH: "io.bassi.Amberol.json"
18 | RUNTIME_REPO: "https://nightly.gnome.org/gnome-nightly.flatpakrepo"
19 |
20 | rust-fmt:
21 | image: "registry.gitlab.com/alatiera/rustfmt-oci-image/rustfmt:stable"
22 | stage: "check"
23 | needs: []
24 | script:
25 | - echo -e "" > src/config.rs
26 | - rustc -Vv && cargo -Vv
27 | - cargo --version
28 | - cargo fmt --all -- --check
29 |
30 | reuse:
31 | image: fedora:latest
32 | stage: "check"
33 | needs: []
34 | before_script:
35 | - export PATH="$HOME/.local/bin:$PATH"
36 | - dnf install -y python3-pip
37 | script:
38 | - pip install --user reuse
39 | - reuse lint
40 |
--------------------------------------------------------------------------------
/src/gtk/volume-control.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
15 |
16 |
17 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/assets/icons/edit-select-all-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/amberol.doap:
--------------------------------------------------------------------------------
1 |
6 |
7 | Amberol
8 | Plays music, and nothing else
9 |
10 |
11 |
12 | Rust
13 | GTK 4
14 | Libadwaita
15 |
16 |
17 |
18 | Emmanuele Bassi
19 |
20 |
21 |
22 |
23 | ebassi
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2022 Emmanuele Bassi
2 | # SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | [package]
5 | name = "amberol"
6 | authors = ["Emmanuele Bassi "]
7 | version = "0.1.0"
8 | edition = "2018"
9 |
10 | [dependencies]
11 | color-thief = "0.2.1"
12 | gdk-pixbuf = { version = "0.20", features = ["v2_42"] }
13 | gettext-rs = { version = "0.7", features = ["gettext-system"] }
14 | gtk = { version = "0.9", package = "gtk4", features = ["v4_14"] }
15 | lofty = "0.21.0"
16 | log = "0.4"
17 | mpris-server = "0.8"
18 | once_cell = "1.10"
19 | pretty_env_logger = "0.5"
20 | rand = "0.8.5"
21 | regex = "1.3.4"
22 | serde_json = "1.0"
23 | sha2 = "0.10.2"
24 | fuzzy-matcher = "0.3.7"
25 | async-channel = "2.2.0"
26 | futures = "0.3"
27 |
28 | [target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
29 | ashpd = {version = "0.9.1", features = ["gtk4"]}
30 |
31 | [dependencies.adw]
32 | package = "libadwaita"
33 | version = "0.7"
34 | features = ["v1_5"]
35 |
36 | [dependencies.gst]
37 | package = "gstreamer"
38 | version = "0.23"
39 |
40 | [dependencies.gst-audio]
41 | package = "gstreamer-audio"
42 | version = "0.23"
43 |
44 | [dependencies.gst-player]
45 | package = "gstreamer-player"
46 | version = "0.23"
47 |
--------------------------------------------------------------------------------
/src/assets/icons/edit-clear-all-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
9 |
--------------------------------------------------------------------------------
/src/assets/icons/media-playlist-repeat-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/RELEASING.md:
--------------------------------------------------------------------------------
1 | Releasing Amberol
2 | =================
3 |
4 | QA Plan
5 | -------
6 |
7 | 1. Initial state
8 | - Open folder
9 | - Open file
10 | - Drag and drop
11 | 2. Main view
12 | - Play/pause
13 | - Previous/next
14 | - Scrubbing
15 | 3. Playlist
16 | - Shuffle
17 | - Repeat: all, one, continuous
18 | - Select
19 | - Remove song
20 | - Remove all
21 | - Toggle playlist
22 |
23 | Release
24 | -------
25 |
26 | Checklist for a release.
27 |
28 | - [ ] Update the [change log](./CHANGES.md)
29 | - [ ] **Added**: New features, settings, UI, translations
30 | - [ ] **Changed**: UI updates, improvements, translation updates
31 | - [ ] **Fixed**: bug fixes, with reference
32 | - [ ] **Removed**: Removed features, settings, UI; **IMPORTANT**: anything
33 | inside this list requires a version bump
34 | - [ ] Update the [appdata](./data/io.bassi.Amberol.appdata.xml.in.in)
35 | - [ ] New `` element
36 | - [ ] *Optional*: new screenshots
37 | - [ ] `git commit -m 'Release Amberol $VERSION'`
38 | - [ ] `meson dist`
39 | - [ ] `git tag -s $VERSION` (use the change log entry)
40 | - [ ] Bump up the project version in [`meson.build`](./meson.build)
41 | - [ ] `git push origin HEAD && git push origin $VERSION`
42 | - [ ] Create a new release on [GitLab](https://gitlab.gnome.org/World/amberol/-/releases)
43 | - [ ] Copy the `CHANGES.md` entry
44 | - [ ] Attach the release tarball and the SHA256 checksum files
45 |
46 | Flathub
47 | -------
48 |
49 | - [ ] Update the `io.bassi.Amberol.json` manifest
50 | - [ ] Change the archive URL
51 | - [ ] Change the SHA256 checksum
52 | - [ ] `git push origin HEAD`
53 |
--------------------------------------------------------------------------------
/io.bassi.Amberol.json:
--------------------------------------------------------------------------------
1 | {
2 | "app-id" : "io.bassi.Amberol.Devel",
3 | "runtime" : "org.gnome.Platform",
4 | "runtime-version" : "master",
5 | "sdk" : "org.gnome.Sdk",
6 | "sdk-extensions" : [
7 | "org.freedesktop.Sdk.Extension.rust-stable"
8 | ],
9 | "command" : "amberol",
10 | "finish-args" : [
11 | "--share=ipc",
12 | "--socket=fallback-x11",
13 | "--device=dri",
14 | "--socket=wayland",
15 | "--socket=pulseaudio",
16 | "--filesystem=xdg-music:ro"
17 | ],
18 | "build-options" : {
19 | "append-path" : "/usr/lib/sdk/rust-stable/bin",
20 | "build-args" : [
21 | "--share=network"
22 | ],
23 | "env" : {
24 | "G_MESSAGES_DEBUG" : "none",
25 | "RUST_BACKTRACE" : "1",
26 | "RUST_LOG" : "amberol=debug,glib=debug"
27 | }
28 | },
29 | "cleanup" : [
30 | "/include",
31 | "/lib/pkgconfig",
32 | "/man",
33 | "/share/doc",
34 | "/share/gtk-doc",
35 | "/share/man",
36 | "/share/pkgconfig",
37 | "*.la",
38 | "*.a"
39 | ],
40 | "modules" : [
41 | {
42 | "name" : "amberol",
43 | "builddir" : true,
44 | "buildsystem" : "meson",
45 | "config-opts": [
46 | "-Dprofile=development"
47 | ],
48 | "sources" : [
49 | {
50 | "type" : "git",
51 | "url" : "https://gitlab.gnome.org/ebassi/amberol.git"
52 | }
53 | ]
54 | }
55 | ]
56 | }
57 |
--------------------------------------------------------------------------------
/src/assets/icons/selection-mode-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/src/assets/icons/media-playlist-repeat-song-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/media-playlist-shuffle-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/audio/inhibit_controller.rs:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022 Emmanuele Bassi
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | use std::cell::Cell;
5 |
6 | use gtk::{gio, prelude::*};
7 | use log::debug;
8 |
9 | use crate::{
10 | audio::{Controller, PlaybackState, RepeatMode, Song},
11 | i18n::i18n,
12 | };
13 |
14 | #[derive(Debug, Default)]
15 | pub struct InhibitController {
16 | cookie: Cell,
17 | }
18 |
19 | impl InhibitController {
20 | pub fn new() -> Self {
21 | Self::default()
22 | }
23 | }
24 |
25 | impl Controller for InhibitController {
26 | fn set_playback_state(&self, playback_state: &PlaybackState) {
27 | let app = gio::Application::default()
28 | .expect("Failed to retrieve application singleton")
29 | .downcast::()
30 | .unwrap();
31 | let win = app
32 | .active_window()
33 | .map(|win| win.downcast::().unwrap());
34 |
35 | if playback_state == &PlaybackState::Playing {
36 | if self.cookie.get() == 0 {
37 | let cookie = app.inhibit(
38 | win.as_ref(),
39 | gtk::ApplicationInhibitFlags::SUSPEND,
40 | Some(&i18n("Playback in progress")),
41 | );
42 | self.cookie.set(cookie);
43 |
44 | debug!("Suspend inhibited");
45 | }
46 | } else {
47 | let cookie = self.cookie.take();
48 | if cookie != 0 {
49 | app.uninhibit(cookie);
50 |
51 | debug!("Suspend uninhibited");
52 | }
53 | }
54 | }
55 |
56 | fn set_song(&self, _song: &Song) {}
57 | fn set_position(&self, _position: u64) {}
58 | fn set_repeat_mode(&self, _mode: RepeatMode) {}
59 | }
60 |
--------------------------------------------------------------------------------
/src/gtk/song-details.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/src/gtk/song-cover.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | center
8 | center
9 |
10 |
11 | crossfade
12 |
13 |
14 | no-image
15 |
16 |
17 | hidden
18 | folder-music-symbolic
19 | 64
20 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | cover-image
32 |
33 |
34 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/meson.build:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2022 Emmanuele Bassi
2 | # SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | pkgdatadir = get_option('prefix') / get_option('datadir') / meson.project_name()
5 |
6 | gnome.compile_resources('amberol',
7 | 'amberol.gresource.xml',
8 | gresource_bundle: true,
9 | install: true,
10 | install_dir: pkgdatadir,
11 | )
12 |
13 | conf = configuration_data()
14 | conf.set_quoted('VERSION', '@0@@1@'.format(meson.project_version(), version_suffix))
15 | conf.set_quoted('GETTEXT_PACKAGE', 'amberol')
16 | conf.set_quoted('LOCALEDIR', get_option('prefix') / get_option('localedir'))
17 | conf.set_quoted('PKGDATADIR', pkgdatadir)
18 | conf.set_quoted('APPLICATION_ID', application_id)
19 | conf.set_quoted('PROFILE', get_option('profile'))
20 |
21 | config_rs = configure_file(
22 | input: 'config.rs.in',
23 | output: 'config.rs',
24 | configuration: conf
25 | )
26 |
27 | # Copy the config.rs output to the source directory.
28 | run_command(
29 | 'cp',
30 | meson.project_build_root() / 'src' / 'config.rs',
31 | meson.project_source_root() / 'src' / 'config.rs',
32 | check: true,
33 | )
34 |
35 | cargo_options = [ '--manifest-path', meson.project_source_root() / 'Cargo.toml' ]
36 | cargo_options += [ '--target-dir', meson.project_build_root() / 'src' ]
37 |
38 | if get_option('profile') == 'default'
39 | cargo_options += [ '--release' ]
40 | rust_target = 'release'
41 | message('Building in release mode')
42 | else
43 | rust_target = 'debug'
44 | message('Building in debug mode')
45 | endif
46 |
47 | cargo_env = [ 'CARGO_HOME=' + meson.project_build_root() / 'cargo-home' ]
48 |
49 | cargo_release = custom_target(
50 | 'cargo-build',
51 | build_by_default: true,
52 | build_always_stale: true,
53 | output: meson.project_name(),
54 | console: true,
55 | install: true,
56 | install_dir: get_option('bindir'),
57 | command: [
58 | 'env',
59 | cargo_env,
60 | cargo, 'build',
61 | cargo_options,
62 | '&&',
63 | 'cp', 'src' / rust_target / meson.project_name(), '@OUTPUT@',
64 | ],
65 | )
66 |
--------------------------------------------------------------------------------
/src/song_details.rs:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022 Emmanuele Bassi
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | use adw::subclass::prelude::*;
5 | use gtk::{glib, prelude::*, CompositeTemplate};
6 |
7 | mod imp {
8 | use super::*;
9 |
10 | #[derive(Debug, Default, CompositeTemplate)]
11 | #[template(resource = "/io/bassi/Amberol/song-details.ui")]
12 | pub struct SongDetails {
13 | // Template widgets
14 | #[template_child]
15 | pub song_title_label: TemplateChild,
16 | #[template_child]
17 | pub song_artist_label: TemplateChild,
18 | #[template_child]
19 | pub song_album_label: TemplateChild,
20 | }
21 |
22 | #[glib::object_subclass]
23 | impl ObjectSubclass for SongDetails {
24 | const NAME: &'static str = "AmberolSongDetails";
25 | type Type = super::SongDetails;
26 | type ParentType = gtk::Widget;
27 |
28 | fn class_init(klass: &mut Self::Class) {
29 | Self::bind_template(klass);
30 |
31 | klass.set_layout_manager_type::();
32 | klass.set_css_name("songdetails");
33 | klass.set_accessible_role(gtk::AccessibleRole::Group);
34 | }
35 |
36 | fn instance_init(obj: &glib::subclass::InitializingObject) {
37 | obj.init_template();
38 | }
39 | }
40 |
41 | impl ObjectImpl for SongDetails {
42 | fn dispose(&self) {
43 | while let Some(child) = self.obj().first_child() {
44 | child.unparent();
45 | }
46 | }
47 | }
48 |
49 | impl WidgetImpl for SongDetails {}
50 | }
51 |
52 | glib::wrapper! {
53 | pub struct SongDetails(ObjectSubclass)
54 | @extends gtk::Widget;
55 | }
56 |
57 | impl Default for SongDetails {
58 | fn default() -> Self {
59 | glib::Object::new()
60 | }
61 | }
62 |
63 | impl SongDetails {
64 | pub fn new() -> Self {
65 | Self::default()
66 | }
67 |
68 | pub fn artist_label(&self) -> gtk::Label {
69 | self.imp().song_artist_label.get()
70 | }
71 |
72 | pub fn title_label(&self) -> gtk::Label {
73 | self.imp().song_title_label.get()
74 | }
75 |
76 | pub fn album_label(&self) -> gtk::Label {
77 | self.imp().song_album_label.get()
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/.reuse/dep5:
--------------------------------------------------------------------------------
1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
2 | Upstream-Name: amberol
3 | Upstream-Contact: Emmanuele Bassi
4 | Source: https://gitlab.gnome.org/ebassi/amberol
5 |
6 | Files: .gitignore
7 | Copyright: 2022 Emmanuele Bassi
8 | License: CC0-1.0
9 |
10 | Files: .gitlab-ci.yml
11 | Copyright: 2022 Emmanuele Bassi
12 | License: CC0-1.0
13 |
14 | Files: amberol.doap
15 | Copyright: 2022 Sophie Herold
16 | License: CC0-1.0
17 |
18 | Files: Cargo.lock
19 | Copyright: 2022 Emmanuele Bassi
20 | License: GPL-3.0-or-later
21 |
22 | Files: README.md CONTRIBUTING.md CHANGES.md RELEASING.md
23 | Copyright: 2022 Emmanuele Bassi
24 | License: CC0-1.0
25 |
26 | Files: io.bassi.Amberol.json
27 | Copyright: 2022 Emmanuele Bassi
28 | License: CC0-1.0
29 |
30 | Files: io.bassi.Amberol.Devel.json
31 | Copyright: 2022 Emmanuele Bassi
32 | License: CC0-1.0
33 |
34 | Files: data/io.bassi.Amberol.metainfo.xml.in.in
35 | Copyright: 2022 Emmanuele Bassi
36 | License: CC0-1.0
37 |
38 | Files: data/io.bassi.Amberol.desktop.in.in
39 | Copyright: 2022 Emmanuele Bassi
40 | License: CC0-1.0
41 |
42 | Files: data/io.bassi.Amberol.gschema.xml
43 | Copyright: 2022 Emmanuele Bassi
44 | License: CC0-1.0
45 |
46 | Files: data/io.bassi.Amberol.service.in
47 | Copyright: 2022 Emmanuele Bassi
48 | License: CC0-1.0
49 |
50 | Files: data/screenshots/*.png
51 | Copyright: 2022 Emmanuele Bassi
52 | License: CC0-1.0
53 |
54 | Files: data/icons/hicolor/scalable/apps/io.bassi.Amberol.*
55 | Copyright: 2022 Jakub Steiner
56 | License: CC-BY-SA-3.0
57 |
58 | Files: data/icons/hicolor/symbolic/apps/io.bassi.Amberol.*
59 | Copyright: 2022 Jakub Steiner
60 | License: CC-BY-SA-3.0
61 |
62 | Files: *.svg
63 | Copyright: 2022 Emmanuele Bassi
64 | License: CC-BY-SA-3.0
65 |
66 | Files: po/POTFILES.in po/LINGUAS
67 | Copyright: 2022 Emmanuele Bassi
68 | License: CC0-1.0
69 |
70 | Files: po/*.po
71 | Copyright: 2022 Translation authors
72 | License: GPL-3.0-or-later
73 |
74 | Files: src/amberol.gresource.xml
75 | Copyright: 2022 Emmanuele Bassi
76 | License: GPL-3.0-or-later
77 |
78 | Files: src/gtk/*.ui
79 | Copyright: 2022 Emmanuele Bassi
80 | License: GPL-3.0-or-later
81 |
82 | Files: src/gtk/style.css src/gtk/style-hc.css
83 | Copyright: 2022 Emmanuele Bassi
84 | License: GPL-3.0-or-later
85 |
86 | Files: src/gtk/style-dark.css
87 | Copyright: 2024 Alice Mikhaylenko
88 | License: GPL-3.0-or-later
89 |
--------------------------------------------------------------------------------
/src/song_cover.rs:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2023 Emmanuele Bassi
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | use adw::subclass::prelude::*;
5 | use gtk::{glib, prelude::*, CompositeTemplate};
6 |
7 | use crate::cover_picture::CoverPicture;
8 |
9 | mod imp {
10 | use super::*;
11 |
12 | #[derive(Debug, Default, CompositeTemplate)]
13 | #[template(resource = "/io/bassi/Amberol/song-cover.ui")]
14 | pub struct SongCover {
15 | // Template widgets
16 | #[template_child]
17 | pub cover_stack: TemplateChild,
18 | #[template_child]
19 | pub album_image: TemplateChild,
20 | }
21 |
22 | #[glib::object_subclass]
23 | impl ObjectSubclass for SongCover {
24 | const NAME: &'static str = "AmberolSongCover";
25 | type Type = super::SongCover;
26 | type ParentType = gtk::Widget;
27 |
28 | fn class_init(klass: &mut Self::Class) {
29 | Self::bind_template(klass);
30 |
31 | klass.set_layout_manager_type::();
32 | klass.set_css_name("songcover");
33 | klass.set_accessible_role(gtk::AccessibleRole::Group);
34 | }
35 |
36 | fn instance_init(obj: &glib::subclass::InitializingObject) {
37 | CoverPicture::static_type();
38 | obj.init_template();
39 | }
40 | }
41 |
42 | impl ObjectImpl for SongCover {
43 | fn dispose(&self) {
44 | while let Some(child) = self.obj().first_child() {
45 | child.unparent();
46 | }
47 | }
48 | }
49 |
50 | impl WidgetImpl for SongCover {}
51 | }
52 |
53 | glib::wrapper! {
54 | pub struct SongCover(ObjectSubclass)
55 | @extends gtk::Widget;
56 | }
57 |
58 | impl Default for SongCover {
59 | fn default() -> Self {
60 | glib::Object::new::()
61 | }
62 | }
63 |
64 | impl SongCover {
65 | pub fn new() -> Self {
66 | Self::default()
67 | }
68 |
69 | pub fn album_image(&self) -> CoverPicture {
70 | self.imp().album_image.get()
71 | }
72 |
73 | pub fn show_cover_image(&self, has_image: bool) {
74 | let cover_stack = self.imp().cover_stack.get();
75 | if has_image {
76 | cover_stack.set_visible_child_name("cover-image");
77 | } else {
78 | cover_stack.set_visible_child_name("no-image");
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/meson.build:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2022 Emmanuele Bassi
2 | # SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | project('amberol', 'rust',
5 | version: '2024.2',
6 | license: ['GPL-3.0'],
7 | meson_version: '>= 0.59.0',
8 | default_options: [ 'warning_level=2', ],
9 | )
10 |
11 | dependency('gtk4', version: '>= 4.13.4')
12 | dependency('libadwaita-1', version: '>= 1.5')
13 | dependency('gstreamer-1.0', version: '>= 1.20')
14 | dependency('gstreamer-audio-1.0', version: '>= 1.20')
15 | dependency('gstreamer-player-1.0', version: '>= 1.20')
16 | dependency('gstreamer-plugins-base-1.0', version: '>= 1.20')
17 | dependency('gstreamer-plugins-bad-1.0', version: '>= 1.20')
18 | dependency('gstreamer-bad-audio-1.0', version: '>= 1.20')
19 |
20 | i18n = import('i18n')
21 | gnome = import('gnome')
22 | fs = import('fs')
23 |
24 | cargo = find_program('cargo', required: true)
25 |
26 | cargo_sources = files(
27 | 'Cargo.toml',
28 | 'Cargo.lock',
29 | )
30 |
31 | if get_option('profile') == 'development'
32 | profile = '.Devel'
33 | if fs.is_dir('.git')
34 | vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD', check: true).stdout().strip()
35 | if vcs_tag == ''
36 | version_suffix = '-devel'
37 | else
38 | version_suffix = '-@0@'.format(vcs_tag)
39 | endif
40 | else
41 | version_suffix = '-devel'
42 | endif
43 | else
44 | profile = ''
45 | version_suffix = ''
46 | endif
47 |
48 | application_id = 'io.bassi.Amberol@0@'.format(profile)
49 |
50 | subdir('data')
51 | subdir('src')
52 | subdir('po')
53 |
54 | reuse = find_program('reuse', required: false)
55 | if reuse.found()
56 | test('license',
57 | reuse,
58 | args:['lint'],
59 | workdir: meson.project_source_root(),
60 | suite: ['lint'],
61 | )
62 | endif
63 |
64 | meson.add_dist_script(
65 | 'build-aux/dist-vendor.sh',
66 | meson.project_source_root(),
67 | meson.project_build_root() / 'meson-dist' / '@0@-@1@'.format(meson.project_name(), meson.project_version()),
68 | )
69 |
70 | gnome.post_install(
71 | glib_compile_schemas: true,
72 | gtk_update_icon_cache: true,
73 | update_desktop_database: true,
74 | )
75 |
76 | summary({
77 | 'prefix': get_option('prefix'),
78 | 'libdir': get_option('libdir'),
79 | 'datadir': get_option('datadir'),
80 | 'bindir': get_option('bindir'),
81 | },
82 | section: 'Directories',
83 | )
84 |
85 | summary({
86 | 'Profile': get_option('profile'),
87 | },
88 | section: 'Build options',
89 | )
90 |
--------------------------------------------------------------------------------
/data/meson.build:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2022 Emmanuele Bassi
2 | # SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | desktop_data = configuration_data()
5 | desktop_data.set('APPLICATION_ID', application_id)
6 | desktop_file = i18n.merge_file(
7 | input: configure_file(
8 | input: 'io.bassi.Amberol.desktop.in.in',
9 | output: 'io.bassi.Amberol.desktop.in',
10 | configuration: desktop_data,
11 | ),
12 | output: '@0@.desktop'.format(application_id),
13 | type: 'desktop',
14 | po_dir: '../po',
15 | install: true,
16 | install_dir: get_option('datadir') / 'applications',
17 | )
18 |
19 | desktop_utils = find_program('desktop-file-validate', required: false)
20 | if desktop_utils.found()
21 | test('Validate desktop file',
22 | desktop_utils,
23 | args: [desktop_file],
24 | suite: ['lint'],
25 | )
26 | endif
27 |
28 | appstream_data = configuration_data()
29 | appstream_data.set('APPLICATION_ID', application_id)
30 | appstream_file = i18n.merge_file(
31 | input: configure_file(
32 | input: 'io.bassi.Amberol.metainfo.xml.in.in',
33 | output: 'io.bassi.Amberol.metainfo.xml.in',
34 | configuration: appstream_data,
35 | ),
36 | output: '@0@.metainfo.xml'.format(application_id),
37 | po_dir: '../po',
38 | install: true,
39 | install_dir: get_option('datadir') / 'metainfo',
40 | )
41 |
42 | appstreamcli = find_program('appstreamcli', required: false)
43 | if appstreamcli.found()
44 | test('Validate appstream file',
45 | appstreamcli,
46 | args: ['validate', '--no-net', '--explain', appstream_file],
47 | suite: ['lint'],
48 | )
49 | endif
50 |
51 | install_data('io.bassi.Amberol.gschema.xml',
52 | install_dir: get_option('datadir') / 'glib-2.0/schemas',
53 | )
54 |
55 | # Compile schemas locally, so we can run uninstalled under a devenv
56 | gnome.compile_schemas()
57 |
58 | compile_schemas = find_program('glib-compile-schemas', required: false)
59 | if compile_schemas.found()
60 | test('Validate schema file',
61 | compile_schemas,
62 | args: ['--strict', '--dry-run', meson.current_source_dir()],
63 | suite: ['lint'],
64 | )
65 | endif
66 |
67 | service_conf = configuration_data()
68 | service_conf.set('application_id', application_id)
69 | service_conf.set('bindir', get_option('prefix') / get_option('bindir'))
70 | configure_file(
71 | input: 'io.bassi.Amberol.service.in',
72 | output: '@0@.service'.format(application_id),
73 | configuration: service_conf,
74 | install: true,
75 | install_dir: get_option('datadir') / 'dbus-1/services',
76 | )
77 |
78 | subdir('icons')
79 |
--------------------------------------------------------------------------------
/src/amberol.gresource.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | assets/icons/app-remove-symbolic.svg
5 | assets/icons/audio-only-symbolic.svg
6 | assets/icons/edit-clear-all-symbolic.svg
7 | assets/icons/edit-select-all-symbolic.svg
8 | assets/icons/folder-music-symbolic.svg
9 | assets/icons/go-previous-symbolic.svg
10 | assets/icons/media-playback-pause-symbolic.svg
11 | assets/icons/media-playback-start-symbolic.svg
12 | assets/icons/media-playlist-consecutive-symbolic.svg
13 | assets/icons/media-playlist-repeat-song-symbolic.svg
14 | assets/icons/media-playlist-repeat-symbolic.svg
15 | assets/icons/media-playlist-shuffle-symbolic.svg
16 | assets/icons/media-skip-backward-symbolic.svg
17 | assets/icons/media-skip-forward-symbolic.svg
18 | assets/icons/selection-mode-symbolic.svg
19 | assets/icons/view-queue-rtl-symbolic.svg
20 | assets/icons/view-queue-symbolic.svg
21 |
22 |
23 | gtk/help-overlay.ui
24 | gtk/playback-control.ui
25 | gtk/playlist-view.ui
26 | gtk/queue-row.ui
27 | gtk/song-cover.ui
28 | gtk/song-details.ui
29 | gtk/style-hc.css
30 | gtk/style.css
31 | gtk/volume-control.ui
32 | gtk/window.ui
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/audio/mod.rs:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022 Emmanuele Bassi
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | // Audio playback is slightly more complicated than simple UI handling code.
5 | //
6 | // We want to bind the state of the UI to the state of the player through
7 | // well established patterns of property bindings and signal emissions, but
8 | // the player can be updated from different threads and by different contexts
9 | // which has an effect on the types that are in play. Of course, we can work
10 | // around it by telling GStreamer that we want to receive signals in the
11 | // default main context, but it's always largely iffy to do so as it requires
12 | // working against the type system through the use of wrappers like Rc and
13 | // Fragile.
14 |
15 | // To avoid this mess, the best practice is to use message passing.
16 | //
17 | // In this particular case, the design of the audio playback interface is
18 | // split into the following components:
19 | //
20 | // AudioPlayer: the main object managing the audio playback
21 | // ├── PlayerState: the state tracker GObject used by the UI
22 | // ├── Queue: the playlist tracker GListModel
23 | // ├── GstBackend: a GstPlayer wrapper
24 | // ╰── controllers: external bits of code that interact with the state
25 | // ╰── MprisController: an MPRIS wrapper
26 | //
27 | // The AudioPlayer object creates a glib::Sender/Receiver channel pair, and
28 | // passes the sender to the controllers; whenever the controllers update their
29 | // state, they will use the glib::Sender to notify the AudioPlayer, which will
30 | // update the PlayerState.
31 | //
32 | // The GstBackend uses a similar sender/receiver pair to communicate with the
33 | // AudioPlayer whenever the GStreamer state changes.
34 | //
35 | // The UI side connects to the PlayerState object for state tracking; all
36 | // changes to the state object happen in the main context by design.
37 | //
38 | // Playback actions are proxied to the AudioPlayer object from the controllers.
39 |
40 | mod controller;
41 | pub use controller::Controller;
42 |
43 | mod cover_cache;
44 | pub use cover_cache::CoverCache;
45 |
46 | mod inhibit_controller;
47 | mod mpris_controller;
48 | pub use inhibit_controller::InhibitController;
49 | pub use mpris_controller::MprisController;
50 |
51 | mod gst_backend;
52 | pub use gst_backend::GstBackend;
53 |
54 | mod player;
55 | mod queue;
56 | mod shuffle;
57 | mod song;
58 | mod state;
59 | mod waveform_generator;
60 |
61 | pub use player::{
62 | AudioPlayer, PlaybackAction, PlaybackState, RepeatMode, ReplayGainMode, SeekDirection,
63 | };
64 | pub use queue::Queue;
65 | pub use shuffle::ShuffleListModel;
66 | pub use song::Song;
67 | pub use state::PlayerState;
68 | pub use waveform_generator::WaveformGenerator;
69 |
--------------------------------------------------------------------------------
/src/assets/icons/audio-only-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/search.rs:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2022 John Toohey
2 | // SPDX-License-Identifier: GPL-3.0-or-later
3 |
4 | use gtk::{glib, prelude::*, subclass::prelude::*};
5 |
6 | mod imp {
7 |
8 | use std::cell::RefCell;
9 |
10 | use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
11 | use gtk::{
12 | glib::{self, ParamSpec, ParamSpecString, Value},
13 | prelude::*,
14 | subclass::prelude::*,
15 | };
16 | use once_cell::sync::Lazy;
17 |
18 | use crate::audio::Song;
19 |
20 | #[derive(Default)]
21 | pub struct FuzzyFilter {
22 | pub search: RefCell