From 15cf412840ffeb16b8ab15fbe994aa726a912ec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Barnouin?= Date: Wed, 13 Nov 2024 16:41:51 +0100 Subject: [PATCH] first commit --- .editorconfig | 12 + .gitattributes | 1 + .github/ISSUE_TEMPLATE/bug_report.md | 39 + .github/dependabot.yml | 11 + .github/workflows/spot-development.yml | 22 + .github/workflows/spot-quality.yml | 36 + .github/workflows/spot-snapshots.yml | 24 + .gitignore | 7 + .vscode/tasks.json | 51 + ARTISTS | 2 + AUTHORS | 22 + Cargo.lock | 5651 ++++++++++++++ Cargo.toml | 78 + LICENSE | 21 + README.md | 150 + TRANSLATORS | 25 + build-aux/flatpak-cargo-generator.py | 450 ++ cargo-sources.json | 6898 +++++++++++++++++ data/appstream/1.png | Bin 0 -> 36723 bytes data/appstream/2.png | Bin 0 -> 129394 bytes data/appstream/3.png | Bin 0 -> 420888 bytes data/dev.alextren.Spot.Source.svg | 1033 +++ data/dev.alextren.Spot.appdata.xml | 356 + data/dev.alextren.Spot.desktop | 13 + data/dev.alextren.Spot.gschema.xml | 56 + .../scalable/apps/dev.alextren.Spot.svg | 89 + .../apps/dev.alextren.Spot-symbolic.svg | 21 + data/meson.build | 31 + dev.alextren.Spot.development.json | 68 + dev.alextren.Spot.snapshots.json | 71 + doc/.latexmkrc | 4 + doc/Dockerfile | 4 + doc/Makefile | 30 + doc/doc.pdf | Bin 0 -> 44585 bytes doc/doc.tex | 191 + doc/enter.sh | 3 + flake.lock | 112 + flake.nix | 89 + meson.build | 25 + meson_options.txt | 2 + po/LINGUAS | 1 + po/POTFILES | 47 + po/ar.po | 393 + po/bg.po | 389 + po/bn.po | 438 ++ po/ca.po | 391 + po/cs.po | 393 + po/de.po | 389 + po/en.po | 389 + po/es.po | 389 + po/et.po | 389 + po/eu.po | 389 + po/fi.po | 389 + po/fr.po | 389 + po/ia.po | 389 + po/id.po | 388 + po/it.po | 390 + po/ja.po | 389 + po/meson.build | 2 + po/nb.po | 390 + po/nl.po | 389 + po/pl.po | 391 + po/poeditor.yml | 5 + po/pt-br.po | 389 + po/pt.po | 389 + po/ru.po | 391 + po/sl.po | 391 + po/spot.pot | 418 + po/tr.po | 389 + po/uk.po | 391 + rustfmt.toml | 3 + spot.nix | 73 + src/api/api_models.rs | 689 ++ src/api/cache.rs | 289 + src/api/cached_client.rs | 876 +++ src/api/client.rs | 716 ++ src/api/mod.rs | 16 + src/api/oauth2.rs | 293 + src/app.css | 15 + src/app/batch_loader.rs | 105 + src/app/components/album/album.blp | 75 + src/app/components/album/album.css | 34 + src/app/components/album/album.rs | 129 + src/app/components/album/mod.rs | 3 + src/app/components/artist/artist.blp | 31 + src/app/components/artist/mod.rs | 95 + .../artist_details/artist_details.blp | 63 + .../artist_details/artist_details.css | 18 + .../artist_details/artist_details.rs | 168 + .../artist_details/artist_details_model.rs | 187 + src/app/components/artist_details/mod.rs | 6 + src/app/components/details/album_header.blp | 136 + src/app/components/details/album_header.css | 33 + src/app/components/details/album_header.rs | 166 + src/app/components/details/details.blp | 55 + src/app/components/details/details.rs | 335 + src/app/components/details/details_model.rs | 272 + src/app/components/details/mod.rs | 8 + .../components/details/release_details.blp | 82 + src/app/components/details/release_details.rs | 92 + .../components/device_selector/component.rs | 100 + .../device_selector/device_selector.blp | 44 + src/app/components/device_selector/mod.rs | 11 + src/app/components/device_selector/widget.rs | 169 + src/app/components/headerbar/component.rs | 296 + src/app/components/headerbar/headerbar.blp | 67 + src/app/components/headerbar/mod.rs | 11 + src/app/components/headerbar/widget.rs | 189 + src/app/components/labels.rs | 55 + src/app/components/library/library.blp | 35 + src/app/components/library/library.rs | 158 + src/app/components/library/library_model.rs | 70 + src/app/components/library/mod.rs | 6 + src/app/components/login/login.blp | 108 + src/app/components/login/login.rs | 247 + src/app/components/login/login_model.rs | 82 + src/app/components/login/mod.rs | 6 + src/app/components/mod.rs | 183 + src/app/components/navigation/factory.rs | 149 + src/app/components/navigation/home.rs | 88 + src/app/components/navigation/mod.rs | 11 + src/app/components/navigation/navigation.rs | 147 + .../components/navigation/navigation_model.rs | 31 + src/app/components/notification/mod.rs | 47 + src/app/components/now_playing/mod.rs | 6 + .../components/now_playing/now_playing.blp | 23 + src/app/components/now_playing/now_playing.rs | 151 + .../now_playing/now_playing_model.rs | 174 + src/app/components/playback/component.rs | 162 + src/app/components/playback/mod.rs | 11 + src/app/components/playback/playback.css | 31 + .../components/playback/playback_controls.blp | 57 + .../components/playback/playback_controls.rs | 123 + src/app/components/playback/playback_info.blp | 43 + src/app/components/playback/playback_info.rs | 74 + .../components/playback/playback_widget.blp | 101 + .../components/playback/playback_widget.rs | 242 + src/app/components/player_notifier.rs | 239 + src/app/components/playlist/mod.rs | 8 + .../playback-0-symbolic.svg | 5 + .../playback-1-symbolic.svg | 5 + .../playback-10-symbolic.svg | 5 + .../playback-11-symbolic.svg | 5 + .../playback-12-symbolic.svg | 5 + .../playback-13-symbolic.svg | 5 + .../playback-14-symbolic.svg | 5 + .../playback-15-symbolic.svg | 5 + .../playback-16-symbolic.svg | 5 + .../playback-2-symbolic.svg | 5 + .../playback-3-symbolic.svg | 5 + .../playback-4-symbolic.svg | 5 + .../playback-5-symbolic.svg | 5 + .../playback-6-symbolic.svg | 5 + .../playback-7-symbolic.svg | 5 + .../playback-8-symbolic.svg | 5 + .../playback-9-symbolic.svg | 5 + .../playback-paused-symbolic.svg | 5 + src/app/components/playlist/playlist.rs | 248 + src/app/components/playlist/song.blp | 143 + src/app/components/playlist/song.css | 186 + src/app/components/playlist/song.rs | 189 + src/app/components/playlist/song_actions.rs | 82 + src/app/components/playlist_details/mod.rs | 14 + .../playlist_details/playlist_details.blp | 52 + .../playlist_details/playlist_details.rs | 326 + .../playlist_details_model.rs | 242 + .../playlist_details/playlist_header.blp | 84 + .../playlist_details/playlist_header.css | 32 + .../playlist_details/playlist_header.rs | 190 + .../playlist_details/playlist_headerbar.blp | 77 + .../playlist_details/playlist_headerbar.rs | 164 + src/app/components/saved_playlists/mod.rs | 6 + .../saved_playlists/saved_playlists.blp | 36 + .../saved_playlists/saved_playlists.rs | 160 + .../saved_playlists/saved_playlists_model.rs | 70 + src/app/components/saved_tracks/mod.rs | 6 + .../components/saved_tracks/saved_tracks.blp | 15 + .../components/saved_tracks/saved_tracks.rs | 117 + .../saved_tracks/saved_tracks_model.rs | 147 + src/app/components/scrolling_header/mod.rs | 7 + .../scrolling_header/scrolling_header.blp | 20 + .../scrolling_header_widget.rs | 117 + src/app/components/search/mod.rs | 9 + src/app/components/search/search.blp | 119 + src/app/components/search/search.rs | 257 + src/app/components/search/search_button.rs | 26 + src/app/components/search/search_model.rs | 66 + src/app/components/selection/component.rs | 233 + .../selection/icons/music-queue-symbolic.svg | 8 + .../selection/icons/playlist2-symbolic.svg | 7 + src/app/components/selection/mod.rs | 10 + .../selection/selection_toolbar.blp | 91 + .../selection/selection_toolbar.css | 3 + src/app/components/selection/widget.rs | 183 + src/app/components/settings/mod.rs | 6 + src/app/components/settings/settings.blp | 109 + src/app/components/settings/settings.rs | 290 + src/app/components/settings/settings_model.rs | 32 + .../components/sidebar/create_playlist.blp | 40 + src/app/components/sidebar/create_playlist.rs | 67 + .../sidebar/icons/library-music-symbolic.svg | 7 + src/app/components/sidebar/mod.rs | 9 + src/app/components/sidebar/sidebar.rs | 200 + src/app/components/sidebar/sidebar_item.rs | 167 + src/app/components/sidebar/sidebar_row.blp | 17 + src/app/components/sidebar/sidebar_row.rs | 84 + src/app/components/user_details/mod.rs | 6 + .../components/user_details/user_details.blp | 44 + .../components/user_details/user_details.css | 8 + .../components/user_details/user_details.rs | 150 + .../user_details/user_details_model.rs | 63 + src/app/components/user_menu/mod.rs | 6 + src/app/components/user_menu/user_menu.rs | 93 + .../components/user_menu/user_menu_model.rs | 52 + src/app/components/utils.rs | 165 + src/app/components/window/mod.rs | 90 + src/app/credentials.rs | 77 + src/app/dispatch.rs | 128 + src/app/list_store.rs | 113 + src/app/loader.rs | 100 + src/app/mod.rs | 271 + src/app/models/album_model.rs | 73 + src/app/models/artist_model.rs | 58 + src/app/models/main.rs | 313 + src/app/models/mod.rs | 68 + src/app/models/songs/mod.rs | 9 + src/app/models/songs/song_list_model.rs | 255 + src/app/models/songs/song_model.rs | 255 + src/app/models/songs/support.rs | 686 ++ src/app/rng.rs | 198 + src/app/state/app_model.rs | 79 + src/app/state/app_state.rs | 270 + src/app/state/browser_state.rs | 440 ++ src/app/state/login_state.rs | 153 + src/app/state/mod.rs | 25 + src/app/state/pagination.rs | 60 + src/app/state/playback_state.rs | 785 ++ src/app/state/screen_states.rs | 471 ++ src/app/state/selection_state.rs | 153 + src/app/state/settings_state.rs | 54 + src/config.rs.in | 5 + src/connect/mod.rs | 47 + src/connect/player.rs | 257 + src/dbus/listener.rs | 131 + src/dbus/mod.rs | 103 + src/dbus/mpris.rs | 348 + src/dbus/types.rs | 247 + src/main.rs | 207 + src/meson.build | 167 + src/player/mod.rs | 133 + src/player/player.rs | 444 ++ src/settings.rs | 115 + src/spot.gresource.xml | 86 + src/window.blp | 134 + subprojects/blueprint-compiler.wrap | 8 + 255 files changed, 47845 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/spot-development.yml create mode 100644 .github/workflows/spot-quality.yml create mode 100644 .github/workflows/spot-snapshots.yml create mode 100644 .gitignore create mode 100644 .vscode/tasks.json create mode 100644 ARTISTS create mode 100644 AUTHORS create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 TRANSLATORS create mode 100644 build-aux/flatpak-cargo-generator.py create mode 100644 cargo-sources.json create mode 100644 data/appstream/1.png create mode 100644 data/appstream/2.png create mode 100644 data/appstream/3.png create mode 100644 data/dev.alextren.Spot.Source.svg create mode 100644 data/dev.alextren.Spot.appdata.xml create mode 100644 data/dev.alextren.Spot.desktop create mode 100644 data/dev.alextren.Spot.gschema.xml create mode 100644 data/hicolor/scalable/apps/dev.alextren.Spot.svg create mode 100644 data/hicolor/symbolic/apps/dev.alextren.Spot-symbolic.svg create mode 100644 data/meson.build create mode 100644 dev.alextren.Spot.development.json create mode 100644 dev.alextren.Spot.snapshots.json create mode 100644 doc/.latexmkrc create mode 100644 doc/Dockerfile create mode 100644 doc/Makefile create mode 100644 doc/doc.pdf create mode 100644 doc/doc.tex create mode 100755 doc/enter.sh create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 meson.build create mode 100644 meson_options.txt create mode 100644 po/LINGUAS create mode 100644 po/POTFILES create mode 100644 po/ar.po create mode 100644 po/bg.po create mode 100644 po/bn.po create mode 100644 po/ca.po create mode 100644 po/cs.po create mode 100644 po/de.po create mode 100644 po/en.po create mode 100644 po/es.po create mode 100644 po/et.po create mode 100644 po/eu.po create mode 100644 po/fi.po create mode 100644 po/fr.po create mode 100644 po/ia.po create mode 100644 po/id.po create mode 100644 po/it.po create mode 100644 po/ja.po create mode 100644 po/meson.build create mode 100644 po/nb.po create mode 100644 po/nl.po create mode 100644 po/pl.po create mode 100644 po/poeditor.yml create mode 100644 po/pt-br.po create mode 100644 po/pt.po create mode 100644 po/ru.po create mode 100644 po/sl.po create mode 100644 po/spot.pot create mode 100644 po/tr.po create mode 100644 po/uk.po create mode 100644 rustfmt.toml create mode 100644 spot.nix create mode 100644 src/api/api_models.rs create mode 100644 src/api/cache.rs create mode 100644 src/api/cached_client.rs create mode 100644 src/api/client.rs create mode 100644 src/api/mod.rs create mode 100644 src/api/oauth2.rs create mode 100644 src/app.css create mode 100644 src/app/batch_loader.rs create mode 100644 src/app/components/album/album.blp create mode 100644 src/app/components/album/album.css create mode 100644 src/app/components/album/album.rs create mode 100644 src/app/components/album/mod.rs create mode 100644 src/app/components/artist/artist.blp create mode 100644 src/app/components/artist/mod.rs create mode 100644 src/app/components/artist_details/artist_details.blp create mode 100644 src/app/components/artist_details/artist_details.css create mode 100644 src/app/components/artist_details/artist_details.rs create mode 100644 src/app/components/artist_details/artist_details_model.rs create mode 100644 src/app/components/artist_details/mod.rs create mode 100644 src/app/components/details/album_header.blp create mode 100644 src/app/components/details/album_header.css create mode 100644 src/app/components/details/album_header.rs create mode 100644 src/app/components/details/details.blp create mode 100644 src/app/components/details/details.rs create mode 100644 src/app/components/details/details_model.rs create mode 100644 src/app/components/details/mod.rs create mode 100644 src/app/components/details/release_details.blp create mode 100644 src/app/components/details/release_details.rs create mode 100644 src/app/components/device_selector/component.rs create mode 100644 src/app/components/device_selector/device_selector.blp create mode 100644 src/app/components/device_selector/mod.rs create mode 100644 src/app/components/device_selector/widget.rs create mode 100644 src/app/components/headerbar/component.rs create mode 100644 src/app/components/headerbar/headerbar.blp create mode 100644 src/app/components/headerbar/mod.rs create mode 100644 src/app/components/headerbar/widget.rs create mode 100644 src/app/components/labels.rs create mode 100644 src/app/components/library/library.blp create mode 100644 src/app/components/library/library.rs create mode 100644 src/app/components/library/library_model.rs create mode 100644 src/app/components/library/mod.rs create mode 100644 src/app/components/login/login.blp create mode 100644 src/app/components/login/login.rs create mode 100644 src/app/components/login/login_model.rs create mode 100644 src/app/components/login/mod.rs create mode 100644 src/app/components/mod.rs create mode 100644 src/app/components/navigation/factory.rs create mode 100644 src/app/components/navigation/home.rs create mode 100644 src/app/components/navigation/mod.rs create mode 100644 src/app/components/navigation/navigation.rs create mode 100644 src/app/components/navigation/navigation_model.rs create mode 100644 src/app/components/notification/mod.rs create mode 100644 src/app/components/now_playing/mod.rs create mode 100644 src/app/components/now_playing/now_playing.blp create mode 100644 src/app/components/now_playing/now_playing.rs create mode 100644 src/app/components/now_playing/now_playing_model.rs create mode 100644 src/app/components/playback/component.rs create mode 100644 src/app/components/playback/mod.rs create mode 100644 src/app/components/playback/playback.css create mode 100644 src/app/components/playback/playback_controls.blp create mode 100644 src/app/components/playback/playback_controls.rs create mode 100644 src/app/components/playback/playback_info.blp create mode 100644 src/app/components/playback/playback_info.rs create mode 100644 src/app/components/playback/playback_widget.blp create mode 100644 src/app/components/playback/playback_widget.rs create mode 100644 src/app/components/player_notifier.rs create mode 100644 src/app/components/playlist/mod.rs create mode 100644 src/app/components/playlist/playback-indicator/playback-0-symbolic.svg create mode 100644 src/app/components/playlist/playback-indicator/playback-1-symbolic.svg create mode 100644 src/app/components/playlist/playback-indicator/playback-10-symbolic.svg create mode 100644 src/app/components/playlist/playback-indicator/playback-11-symbolic.svg create mode 100644 src/app/components/playlist/playback-indicator/playback-12-symbolic.svg create mode 100644 src/app/components/playlist/playback-indicator/playback-13-symbolic.svg create mode 100644 src/app/components/playlist/playback-indicator/playback-14-symbolic.svg create mode 100644 src/app/components/playlist/playback-indicator/playback-15-symbolic.svg create mode 100644 src/app/components/playlist/playback-indicator/playback-16-symbolic.svg create mode 100644 src/app/components/playlist/playback-indicator/playback-2-symbolic.svg create mode 100644 src/app/components/playlist/playback-indicator/playback-3-symbolic.svg create mode 100644 src/app/components/playlist/playback-indicator/playback-4-symbolic.svg create mode 100644 src/app/components/playlist/playback-indicator/playback-5-symbolic.svg create mode 100644 src/app/components/playlist/playback-indicator/playback-6-symbolic.svg create mode 100644 src/app/components/playlist/playback-indicator/playback-7-symbolic.svg create mode 100644 src/app/components/playlist/playback-indicator/playback-8-symbolic.svg create mode 100644 src/app/components/playlist/playback-indicator/playback-9-symbolic.svg create mode 100644 src/app/components/playlist/playback-indicator/playback-paused-symbolic.svg create mode 100644 src/app/components/playlist/playlist.rs create mode 100644 src/app/components/playlist/song.blp create mode 100644 src/app/components/playlist/song.css create mode 100644 src/app/components/playlist/song.rs create mode 100644 src/app/components/playlist/song_actions.rs create mode 100644 src/app/components/playlist_details/mod.rs create mode 100644 src/app/components/playlist_details/playlist_details.blp create mode 100644 src/app/components/playlist_details/playlist_details.rs create mode 100644 src/app/components/playlist_details/playlist_details_model.rs create mode 100644 src/app/components/playlist_details/playlist_header.blp create mode 100644 src/app/components/playlist_details/playlist_header.css create mode 100644 src/app/components/playlist_details/playlist_header.rs create mode 100644 src/app/components/playlist_details/playlist_headerbar.blp create mode 100644 src/app/components/playlist_details/playlist_headerbar.rs create mode 100644 src/app/components/saved_playlists/mod.rs create mode 100644 src/app/components/saved_playlists/saved_playlists.blp create mode 100644 src/app/components/saved_playlists/saved_playlists.rs create mode 100644 src/app/components/saved_playlists/saved_playlists_model.rs create mode 100644 src/app/components/saved_tracks/mod.rs create mode 100644 src/app/components/saved_tracks/saved_tracks.blp create mode 100644 src/app/components/saved_tracks/saved_tracks.rs create mode 100644 src/app/components/saved_tracks/saved_tracks_model.rs create mode 100644 src/app/components/scrolling_header/mod.rs create mode 100644 src/app/components/scrolling_header/scrolling_header.blp create mode 100644 src/app/components/scrolling_header/scrolling_header_widget.rs create mode 100644 src/app/components/search/mod.rs create mode 100644 src/app/components/search/search.blp create mode 100644 src/app/components/search/search.rs create mode 100644 src/app/components/search/search_button.rs create mode 100644 src/app/components/search/search_model.rs create mode 100644 src/app/components/selection/component.rs create mode 100644 src/app/components/selection/icons/music-queue-symbolic.svg create mode 100644 src/app/components/selection/icons/playlist2-symbolic.svg create mode 100644 src/app/components/selection/mod.rs create mode 100644 src/app/components/selection/selection_toolbar.blp create mode 100644 src/app/components/selection/selection_toolbar.css create mode 100644 src/app/components/selection/widget.rs create mode 100644 src/app/components/settings/mod.rs create mode 100644 src/app/components/settings/settings.blp create mode 100644 src/app/components/settings/settings.rs create mode 100644 src/app/components/settings/settings_model.rs create mode 100644 src/app/components/sidebar/create_playlist.blp create mode 100644 src/app/components/sidebar/create_playlist.rs create mode 100644 src/app/components/sidebar/icons/library-music-symbolic.svg create mode 100644 src/app/components/sidebar/mod.rs create mode 100644 src/app/components/sidebar/sidebar.rs create mode 100644 src/app/components/sidebar/sidebar_item.rs create mode 100644 src/app/components/sidebar/sidebar_row.blp create mode 100644 src/app/components/sidebar/sidebar_row.rs create mode 100644 src/app/components/user_details/mod.rs create mode 100644 src/app/components/user_details/user_details.blp create mode 100644 src/app/components/user_details/user_details.css create mode 100644 src/app/components/user_details/user_details.rs create mode 100644 src/app/components/user_details/user_details_model.rs create mode 100644 src/app/components/user_menu/mod.rs create mode 100644 src/app/components/user_menu/user_menu.rs create mode 100644 src/app/components/user_menu/user_menu_model.rs create mode 100644 src/app/components/utils.rs create mode 100644 src/app/components/window/mod.rs create mode 100644 src/app/credentials.rs create mode 100644 src/app/dispatch.rs create mode 100644 src/app/list_store.rs create mode 100644 src/app/loader.rs create mode 100644 src/app/mod.rs create mode 100644 src/app/models/album_model.rs create mode 100644 src/app/models/artist_model.rs create mode 100644 src/app/models/main.rs create mode 100644 src/app/models/mod.rs create mode 100644 src/app/models/songs/mod.rs create mode 100644 src/app/models/songs/song_list_model.rs create mode 100644 src/app/models/songs/song_model.rs create mode 100644 src/app/models/songs/support.rs create mode 100644 src/app/rng.rs create mode 100644 src/app/state/app_model.rs create mode 100644 src/app/state/app_state.rs create mode 100644 src/app/state/browser_state.rs create mode 100644 src/app/state/login_state.rs create mode 100644 src/app/state/mod.rs create mode 100644 src/app/state/pagination.rs create mode 100644 src/app/state/playback_state.rs create mode 100644 src/app/state/screen_states.rs create mode 100644 src/app/state/selection_state.rs create mode 100644 src/app/state/settings_state.rs create mode 100644 src/config.rs.in create mode 100644 src/connect/mod.rs create mode 100644 src/connect/player.rs create mode 100644 src/dbus/listener.rs create mode 100644 src/dbus/mod.rs create mode 100644 src/dbus/mpris.rs create mode 100644 src/dbus/types.rs create mode 100644 src/main.rs create mode 100644 src/meson.build create mode 100644 src/player/mod.rs create mode 100644 src/player/player.rs create mode 100644 src/settings.rs create mode 100644 src/spot.gresource.xml create mode 100644 src/window.blp create mode 100644 subprojects/blueprint-compiler.wrap diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6cb4b9c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 + +[*.{build,yml,ui,yaml,css,blp}] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..283f09d --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.po diff=po diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..cdb8d7b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,39 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**General information:** + - Distribution: + - Installation method [e. g. built from source, installed from Flathub...]: + - Version [e.g. 0.1.0]: + - Device used [e. g. desktop, phone...]: + +**Stack trace:** +If applicable, run the application from a terminal and paste relevant log output. +``` +flatpak run --env=RUST_BACKTRACE=full dev.alextren.Spot +``` + +**Additional context** +Add any other context about the problem here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c1ce3c3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "cargo" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/spot-development.yml b/.github/workflows/spot-development.yml new file mode 100644 index 0000000..9a25fc1 --- /dev/null +++ b/.github/workflows/spot-development.yml @@ -0,0 +1,22 @@ +name: spot-development + +on: + pull_request: + branches: [development] + workflow_dispatch: + +jobs: + flatpak-builder: + name: "Flatpak Builder" + runs-on: ubuntu-latest + container: + image: bilelmoussaoui/flatpak-github-actions:gnome-nightly + options: --privileged + steps: + - uses: actions/checkout@v2 + - uses: bilelmoussaoui/flatpak-github-actions/flatpak-builder@v4 + with: + bundle: "spot.flatpak" + manifest-path: "dev.alextren.Spot.development.json" + cache-key: flatpak-builder-${{ github.sha }} + run-tests: true diff --git a/.github/workflows/spot-quality.yml b/.github/workflows/spot-quality.yml new file mode 100644 index 0000000..984b43f --- /dev/null +++ b/.github/workflows/spot-quality.yml @@ -0,0 +1,36 @@ +name: spot-quality + +on: + push: + pull_request: + +jobs: + ci-check: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + default: true + components: rustfmt + + - name: Add empty config.rs + run: | + echo >> src/config.rs + + - name: Format + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + shellcheck: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + - run: | + sudo apt-get -y update && sudo apt-get -y install shellcheck + find $GITHUB_WORKSPACE -type f -and \( -name "*.sh" \) | xargs shellcheck diff --git a/.github/workflows/spot-snapshots.yml b/.github/workflows/spot-snapshots.yml new file mode 100644 index 0000000..d3cc56d --- /dev/null +++ b/.github/workflows/spot-snapshots.yml @@ -0,0 +1,24 @@ +name: spot-snapshots + +on: + push: + branches: [master] + pull_request: + branches: [master] + workflow_dispatch: + +jobs: + flatpak-builder: + name: "Flatpak Builder" + runs-on: ubuntu-latest + container: + image: bilelmoussaoui/flatpak-github-actions:gnome-42 + options: --privileged + steps: + - uses: actions/checkout@v2 + - uses: bilelmoussaoui/flatpak-github-actions/flatpak-builder@v4 + with: + bundle: "spot.flatpak" + manifest-path: "dev.alextren.Spot.snapshots.json" + cache-key: flatpak-builder-${{ github.sha }} + run-tests: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a15d90 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/target/ +/doc/target +/cargo/ +/build/ +/.flatpak-builder/ +/src/config.rs +/subprojects/blueprint-compiler diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..49d86c6 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,51 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "options": { + "env": { + "RUST_BACKTRACE": "1", + "RUST_LOG": "spot=debug", + "LANG": "C", + //"https_proxy": "localhost:8080" + } + }, + "tasks": [ + { + "label": "meson", + "type": "shell", + "command": "meson setup target -Dbuildtype=debug -Doffline=false --prefix=\"$HOME/.local\"" + }, + { + "label": "build", + "type": "shell", + "command": "ninja install -C target", + "problemMatcher": [], + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "clear": true + } + }, + { + "label": "run", + "type": "shell", + "command": "$HOME/.local/bin/spot", + "presentation": { + "reveal": "always", + "clear": true + } + }, + { + "label": "test", + "type": "shell", + "command": "meson test -C target --verbose", + "group": { + "kind": "test", + "isDefault": true + } + } + ] +} \ No newline at end of file diff --git a/ARTISTS b/ARTISTS new file mode 100644 index 0000000..d2b8884 --- /dev/null +++ b/ARTISTS @@ -0,0 +1,2 @@ +Tobias Bernard +Noëlle diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..f7dad66 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,22 @@ +Alexandre Trendel +Noëlle +Armin Begert +Douile +Diego Augusto +TotalDarkness-NRF +Seioo Inoue +xRMG412 +realJavabot +Gabriele Musco +Alistair Francis +Daniel Peukert +Nils Tonnätt +Niklas Sauter +Nicolas Fella +Fridolin Weisser +Jan Przebor +Warren Hu +bbb651 +Julius Rüberg +janbrummer +Alisson Lauffer diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ccf3967 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,5651 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if", + "cipher 0.3.0", + "cpufeatures", + "opaque-debug", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "alsa" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +dependencies = [ + "alsa-sys", + "bitflags 2.6.0", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + +[[package]] +name = "anyhow" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-broadcast" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" +dependencies = [ + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0c4a4f319e45986f347ee47fef8bf5e81c9abc3f6f58dc2391439f30df65f0" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand 2.0.1", + "futures-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" +dependencies = [ + "async-lock", + "autocfg", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-global-executor" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" +dependencies = [ + "async-channel", + "async-executor", + "async-io 1.13.0", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite", + "log", + "parking", + "polling 2.8.0", + "rustix 0.37.27", + "slab", + "socket2 0.4.10", + "waker-fn", +] + +[[package]] +name = "async-io" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10da8f3146014722c89e7859e1d7bb97873125d7346d10ca642ffab794355828" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling 3.3.0", + "rustix 0.38.21", + "slab", + "tracing", + "waker-fn", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-process" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" +dependencies = [ + "async-io 1.13.0", + "async-lock", + "async-signal", + "blocking", + "cfg-if", + "event-listener 3.0.1", + "futures-lite", + "rustix 0.38.21", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-recursion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "async-signal" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5" +dependencies = [ + "async-io 2.1.0", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 0.38.21", + "signal-hook-registry", + "slab", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-channel", + "async-global-executor", + "async-io 1.13.0", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4eb2cdb97421e01129ccb49169d8279ed21e829929144f4a22a6e54ac549ca1" + +[[package]] +name = "async-trait" +version = "0.1.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atomic_refcell" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "aws-lc-rs" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd82dba44d209fddb11c190e0a94b78651f95299598e472215667417a03ff1d" +dependencies = [ + "aws-lc-sys", + "mirai-annotations", + "paste", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7a4168111d7eb622a31b214057b8509c0a7e1794f44c546d742330dc793972" +dependencies = [ + "bindgen 0.69.5", + "cc", + "cmake", + "dunce", + "fs_extra", + "libc", + "paste", +] + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bindgen" +version = "0.68.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.85", +] + +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.85", + "which", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-modes" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cb03d1bed155d89dce0f845b7899b18a9a163e148fd004e1c28421a783e2d8e" +dependencies = [ + "block-padding", + "cipher 0.3.0", +] + +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + +[[package]] +name = "blocking" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c36a4d0d48574b3dd360b4b7d95cc651d2b6557b6402848a27d4b228a473e2a" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "fastrand 2.0.1", + "futures-io", + "futures-lite", + "piper", + "tracing", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "bytemuck" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cairo-rs" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3603c4028a5e368d09b51c8b624b9a46edcd7c3778284077a6125af73c9f0a" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "glib 0.17.10", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "691d0c66b1fb4881be80a760cb8fe76ea97218312f9dfe2c9cc0f496ca279cb1" +dependencies = [ + "glib-sys 0.17.10", + "libc", + "system-deps 6.2.0", +] + +[[package]] +name = "castaway" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" + +[[package]] +name = "cc" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e8aabfac534be767c909e0690571677d49f41bd8465ae876fe043d52ba5292" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-expr" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-expr" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0890061c4d3223e7267f3bad2ec40b997d64faac1c2815a4a9d95018e2b9e9c" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.48.5", +] + +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clang-sys" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "coreaudio-rs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8478e5bdad14dce236b9898ea002eabfa87cbe14f0aa538dbe3b6a4bec4332d" +dependencies = [ + "bindgen 0.68.1", +] + +[[package]] +name = "cpal" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk", + "ndk-context", + "oboe", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.54.0", +] + +[[package]] +name = "cpufeatures" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "curl" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22" +dependencies = [ + "curl-sys", + "libc", + "openssl-probe", + "openssl-sys", + "schannel", + "socket2 0.4.10", + "winapi", +] + +[[package]] +name = "curl-sys" +version = "0.4.68+curl-8.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a0d18d88360e374b16b2273c832b5e57258ffc1d4aa4f96b108e0738d5752f" +dependencies = [ + "cc", + "libc", + "libnghttp2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", + "windows-sys 0.48.0", +] + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.85", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.85", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enumflags2" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5998b4f30320c9d93aed72f63af821bfdac50465b75428fce77b48ec482c3939" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f95e2801cd355d4a1a3e3953ce6ee5ae9603a5c833455343a8bfe3f44d418246" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", +] + +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cec0252c2afff729ee6f00e903d479fba81784c8e2bd77447673471fdfaea1" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset 0.9.0", + "rustc_version", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "695d6bc846438c5708b07007537b9274d883373dd30858ca881d7d71b5540717" +dependencies = [ + "bitflags 1.3.2", + "gdk-pixbuf-sys", + "gio", + "glib 0.17.10", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9285ec3c113c66d7d0ab5676599176f1f42f4944ca1b581852215bf5694870cb" +dependencies = [ + "gio-sys 0.17.10", + "glib-sys 0.17.10", + "gobject-sys 0.17.10", + "libc", + "system-deps 6.2.0", +] + +[[package]] +name = "gdk4" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3abf96408a26e3eddf881a7f893a1e111767137136e347745e8ea6ed12731ff" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk-pixbuf", + "gdk4-sys", + "gio", + "glib 0.17.10", + "libc", + "pango", +] + +[[package]] +name = "gdk4-sys" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc92aa1608c089c49393d014c38ac0390d01e4841e1fedaa75dbcef77aaed64" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys 0.17.10", + "glib-sys 0.17.10", + "gobject-sys 0.17.10", + "libc", + "pango-sys", + "pkg-config", + "system-deps 6.2.0", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gettext-rs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e49ea8a8fad198aaa1f9655a2524b64b70eb06b2f3ff37da407566c93054f364" +dependencies = [ + "gettext-sys", + "locale_config", +] + +[[package]] +name = "gettext-sys" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c63ce2e00f56a206778276704bbe38564c8695249fdc8f354b4ef71c57c3839d" +dependencies = [ + "cc", + "temp-dir", +] + +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + +[[package]] +name = "gio" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6973e92937cf98689b6a054a9e56c657ed4ff76de925e36fc331a15f0c5d30a" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys 0.17.10", + "glib 0.17.10", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ccf87c30a12c469b6d958950f6a9c09f2be20b7773f7e70d20b867fdf2628c3" +dependencies = [ + "glib-sys 0.17.10", + "gobject-sys 0.17.10", + "libc", + "system-deps 6.2.0", + "winapi", +] + +[[package]] +name = "gio-sys" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f7efc368de04755344f0084104835b6bb71df2c1d41e37d863947392a894779" +dependencies = [ + "glib-sys 0.20.4", + "gobject-sys 0.20.4", + "libc", + "system-deps 7.0.3", + "windows-sys 0.52.0", +] + +[[package]] +name = "glib" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fad45ba8d4d2cea612b432717e834f48031cd8853c8aaf43b2c79fec8d144b" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys 0.17.10", + "glib-macros 0.17.10", + "glib-sys 0.17.10", + "gobject-sys 0.17.10", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcf1ec6d3650bf9fdbc6cee242d4fcebc6f6bfd9bea5b929b6a8b7344eb85ff" +dependencies = [ + "bitflags 2.6.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys 0.20.4", + "glib-macros 0.20.4", + "glib-sys 0.20.4", + "gobject-sys 0.20.4", + "libc", + "memchr", + "smallvec", +] + +[[package]] +name = "glib-macros" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca5c79337338391f1ab8058d6698125034ce8ef31b72a442437fa6c8580de26" +dependencies = [ + "anyhow", + "heck 0.4.1", + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "glib-macros" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6bf88f70cd5720a6197639dcabcb378dd528d0cb68cb1f45e3b358bcb841cd7" +dependencies = [ + "heck 0.5.0", + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "glib-sys" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d80aa6ea7bba0baac79222204aa786a6293078c210abe69ef1336911d4bdc4f0" +dependencies = [ + "libc", + "system-deps 6.2.0", +] + +[[package]] +name = "glib-sys" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f9eca5d88cfa6a453b00d203287c34a2b7cac3a7831779aa2bb0b3c7233752b" +dependencies = [ + "libc", + "system-deps 7.0.3", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gobject-sys" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd34c3317740a6358ec04572c1bcfd3ac0b5b6529275fae255b237b314bb8062" +dependencies = [ + "glib-sys 0.17.10", + "libc", + "system-deps 6.2.0", +] + +[[package]] +name = "gobject-sys" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c674d2ff8478cf0ec29d2be730ed779fef54415a2fb4b565c52def62696462" +dependencies = [ + "glib-sys 0.20.4", + "libc", + "system-deps 7.0.3", +] + +[[package]] +name = "governor" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a7f542ee6b35af73b06abc0dad1c1bae89964e4e253bc4b587b91c9637867b" +dependencies = [ + "cfg-if", + "futures", + "futures-timer", + "no-std-compat", + "nonzero_ext", + "parking_lot", + "portable-atomic", + "rand", + "smallvec", + "spinning_top", +] + +[[package]] +name = "graphene-rs" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def4bb01265b59ed548b05455040d272d989b3012c42d4c1bbd39083cb9b40d9" +dependencies = [ + "glib 0.17.10", + "graphene-sys", + "libc", +] + +[[package]] +name = "graphene-sys" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1856fc817e6a6675e36cea0bd9a3afe296f5d9709d1e2d3182803ac77f0ab21d" +dependencies = [ + "glib-sys 0.17.10", + "libc", + "pkg-config", + "system-deps 6.2.0", +] + +[[package]] +name = "gsk4" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f01ef44fa7cac15e2da9978529383e6bee03e570ba5bf7036b4c10a15cc3a3c" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk4", + "glib 0.17.10", + "graphene-rs", + "gsk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gsk4-sys" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07a84fb4dcf1323d29435aa85e2f5f58bef564342bef06775ec7bd0da1f01b0" +dependencies = [ + "cairo-sys-rs", + "gdk4-sys", + "glib-sys 0.17.10", + "gobject-sys 0.17.10", + "graphene-sys", + "libc", + "pango-sys", + "system-deps 6.2.0", +] + +[[package]] +name = "gstreamer" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecf3bcfc2ceb82ce02437f53ff2fcaee5e7d45ae697ab64a018408749779b9" +dependencies = [ + "cfg-if", + "futures-channel", + "futures-core", + "futures-util", + "glib 0.20.4", + "gstreamer-sys", + "itertools 0.13.0", + "libc", + "muldiv", + "num-integer", + "num-rational", + "once_cell", + "option-operations", + "paste", + "pin-project-lite", + "smallvec", + "thiserror", +] + +[[package]] +name = "gstreamer-app" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a4ec9f0d2037349c82f589c1cbfe788a62f4941851924bb7c3929a6a790007" +dependencies = [ + "futures-core", + "futures-sink", + "glib 0.20.4", + "gstreamer", + "gstreamer-app-sys", + "gstreamer-base", + "libc", +] + +[[package]] +name = "gstreamer-app-sys" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d5cac633c1ab7030c777c8c58c682a0c763bbc4127bccc370dabe39c01a12d" +dependencies = [ + "glib-sys 0.20.4", + "gstreamer-base-sys", + "gstreamer-sys", + "libc", + "system-deps 7.0.3", +] + +[[package]] +name = "gstreamer-audio" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36d39b07213f83055fc705a384fa32ad581776b8e5b04c86f3a419ec5dfc0f81" +dependencies = [ + "cfg-if", + "glib 0.20.4", + "gstreamer", + "gstreamer-audio-sys", + "gstreamer-base", + "libc", + "once_cell", + "smallvec", +] + +[[package]] +name = "gstreamer-audio-sys" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d84744e7ac8f8bc0cf76b7be40f2d5be12e6cf197e4c6ca9d3438109c21e2f51" +dependencies = [ + "glib-sys 0.20.4", + "gobject-sys 0.20.4", + "gstreamer-base-sys", + "gstreamer-sys", + "libc", + "system-deps 7.0.3", +] + +[[package]] +name = "gstreamer-base" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46ce7330d2995138a77192ea20961422ddee1578e1a47480acb820c43ceb0e2d" +dependencies = [ + "atomic_refcell", + "cfg-if", + "glib 0.20.4", + "gstreamer", + "gstreamer-base-sys", + "libc", +] + +[[package]] +name = "gstreamer-base-sys" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7796e694c21c215447811c9cff694dce1fc6e02b0bbafb75cd8583b6aefe9e5f" +dependencies = [ + "glib-sys 0.20.4", + "gobject-sys 0.20.4", + "gstreamer-sys", + "libc", + "system-deps 7.0.3", +] + +[[package]] +name = "gstreamer-sys" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3859929db32f26a35818d0d9ed82f0887c9221ca402ddefaea2bb99833d535" +dependencies = [ + "glib-sys 0.20.4", + "gobject-sys 0.20.4", + "libc", + "system-deps 7.0.3", +] + +[[package]] +name = "gtk4" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b28a32a04cd75cef14a0983f8b0c669e0fe152a0a7725accdeb594e2c764c88b" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk-pixbuf", + "gdk4", + "gio", + "glib 0.17.10", + "graphene-rs", + "gsk4", + "gtk4-macros", + "gtk4-sys", + "libc", + "once_cell", + "pango", +] + +[[package]] +name = "gtk4-macros" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a4d6b61570f76d3ee542d984da443b1cd69b6105264c61afec3abed08c2500f" +dependencies = [ + "anyhow", + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "gtk4-sys" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f8283f707b07e019e76c7f2934bdd4180c277e08aa93f4c0d8dd07b7a34e22f" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk4-sys", + "gio-sys 0.17.10", + "glib-sys 0.17.10", + "gobject-sys 0.17.10", + "graphene-sys", + "gsk4-sys", + "libc", + "pango-sys", + "system-deps 6.2.0", +] + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.9", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "headers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +dependencies = [ + "base64 0.21.5", + "bytes", + "headers-core", + "http 1.1.0", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http 1.1.0", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "hostname" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" +dependencies = [ + "cfg-if", + "libc", + "windows 0.52.0", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http 0.2.9", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.9", + "http-body 0.4.5", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.6", + "http 1.1.0", + "http-body 1.0.1", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-proxy2" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9043b7b23fb0bc4a1c7014c27b50a4fc42cc76206f71d34fc0dfe5b28ddc3faf" +dependencies = [ + "bytes", + "futures-util", + "headers", + "http 1.1.0", + "hyper 1.5.0", + "hyper-rustls 0.26.0", + "hyper-util", + "pin-project-lite", + "rustls-native-certs 0.7.3", + "tokio", + "tokio-rustls 0.25.0", + "tower-service", + "webpki", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.9", + "hyper 0.14.27", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper 1.5.0", + "hyper-util", + "log", + "rustls 0.22.4", + "rustls-native-certs 0.7.3", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.25.0", + "tower-service", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper 1.5.0", + "hyper-util", + "log", + "rustls 0.23.15", + "rustls-native-certs 0.8.0", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.0", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "hyper 1.5.0", + "pin-project-lite", + "socket2 0.5.5", + "tokio", + "tower", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core 0.51.1", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "if-addrs" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2a33e9c38988ecbda730c85b0fd9ddcdf83c0305ac7fd21c8bb9f57f2f0cc8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix 0.38.21", + "windows-sys 0.48.0", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "isahc" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9" +dependencies = [ + "async-channel", + "castaway", + "crossbeam-utils", + "curl", + "curl-sys", + "encoding_rs", + "event-listener 2.5.3", + "futures-lite", + "http 0.2.9", + "log", + "mime", + "once_cell", + "polling 2.8.0", + "serde", + "serde_json", + "slab", + "sluice", + "tracing", + "tracing-futures", + "url", + "waker-fn", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libadwaita" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab9c0843f9f23ff25634df2743690c3a1faffe0a190e60c490878517eb81abf" +dependencies = [ + "bitflags 1.3.2", + "gdk-pixbuf", + "gdk4", + "gio", + "glib 0.17.10", + "gtk4", + "libadwaita-sys", + "libc", + "pango", +] + +[[package]] +name = "libadwaita-sys" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4231cb2499a9f0c4cdfa4885414b33e39901ddcac61150bc0bb4ff8a57ede404" +dependencies = [ + "gdk4-sys", + "gio-sys 0.17.10", + "glib-sys 0.17.10", + "gobject-sys 0.17.10", + "gtk4-sys", + "libc", + "pango-sys", + "system-deps 6.2.0", +] + +[[package]] +name = "libc" +version = "0.2.161" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libmdns" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48854699e11b111433431b69cee2365fcab0b29b06993f48c257dfbaf6395862" +dependencies = [ + "byteorder", + "futures-util", + "hostname", + "if-addrs", + "log", + "multimap", + "rand", + "socket2 0.5.5", + "thiserror", + "tokio", +] + +[[package]] +name = "libnghttp2-sys" +version = "0.1.8+1.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fae956c192dadcdb5dace96db71fa0b827333cce7c7b38dc71446f024d8a340" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "libpulse-binding" +version = "2.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3557a2dfc380c8f061189a01c6ae7348354e0c9886038dc6c171219c08eaff" +dependencies = [ + "bitflags 1.3.2", + "libc", + "libpulse-sys", + "num-derive 0.3.3", + "num-traits", + "winapi", +] + +[[package]] +name = "libpulse-simple-binding" +version = "2.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05fd6b68f33f6a251265e6ed1212dc3107caad7c5c6fdcd847b2e65ef58c308d" +dependencies = [ + "libpulse-binding", + "libpulse-simple-sys", + "libpulse-sys", +] + +[[package]] +name = "libpulse-simple-sys" +version = "1.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6613b4199d8b9f0edcfb623e020cb17bbd0bee8dd21f3c7cc938de561c4152" +dependencies = [ + "libpulse-sys", + "pkg-config", +] + +[[package]] +name = "libpulse-sys" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc19e110fbf42c17260d30f6d3dc545f58491c7830d38ecb9aaca96e26067a9b" +dependencies = [ + "libc", + "num-derive 0.3.3", + "num-traits", + "pkg-config", + "winapi", +] + +[[package]] +name = "librespot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea500bb5673fbf4cc9dcbb7afdc228bf0cdb434cc3a0be79afa6bb385a8b5f8" +dependencies = [ + "data-encoding", + "env_logger 0.11.5", + "futures-util", + "getopts", + "librespot-audio", + "librespot-connect", + "librespot-core", + "librespot-discovery", + "librespot-metadata", + "librespot-oauth", + "librespot-playback", + "librespot-protocol", + "log", + "sha1", + "sysinfo", + "thiserror", + "tokio", + "url", +] + +[[package]] +name = "librespot-audio" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fbda070a5598b32718e497f585f46891f7113e64aff20a13c0f2ba8fe7ccad9" +dependencies = [ + "aes 0.8.4", + "bytes", + "ctr", + "futures-util", + "http-body-util", + "hyper 1.5.0", + "hyper-util", + "librespot-core", + "log", + "parking_lot", + "tempfile", + "thiserror", + "tokio", +] + +[[package]] +name = "librespot-connect" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "699745eaeff6f53b67e93fe973eb72c818cba58d10cfdbdd4c1bf7893f9b705b" +dependencies = [ + "form_urlencoded", + "futures-util", + "librespot-core", + "librespot-playback", + "librespot-protocol", + "log", + "protobuf", + "rand", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", +] + +[[package]] +name = "librespot-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "505a5ddd966231755994b60435607a1e8ae1d41c7f1169b078e0511bfb82d931" +dependencies = [ + "aes 0.8.4", + "base64 0.22.1", + "byteorder", + "bytes", + "data-encoding", + "form_urlencoded", + "futures-core", + "futures-util", + "governor", + "hmac", + "http 1.1.0", + "http-body-util", + "httparse", + "hyper 1.5.0", + "hyper-proxy2", + "hyper-rustls 0.27.3", + "hyper-util", + "librespot-oauth", + "librespot-protocol", + "log", + "nonzero_ext", + "num-bigint", + "num-derive 0.4.2", + "num-integer", + "num-traits", + "once_cell", + "parking_lot", + "pbkdf2", + "pin-project-lite", + "priority-queue", + "protobuf", + "quick-xml", + "rand", + "rsa", + "serde", + "serde_json", + "sha1", + "shannon", + "sysinfo", + "thiserror", + "time", + "tokio", + "tokio-stream", + "tokio-tungstenite", + "tokio-util", + "url", + "uuid", + "vergen-gitcl", +] + +[[package]] +name = "librespot-discovery" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abca1d9420179c0875189cee50b1cd75ad91c6180f38d079eae3a62a40b4745f" +dependencies = [ + "aes 0.8.4", + "base64 0.22.1", + "bytes", + "ctr", + "form_urlencoded", + "futures-core", + "futures-util", + "hmac", + "http-body-util", + "hyper 1.5.0", + "hyper-util", + "libmdns", + "librespot-core", + "log", + "rand", + "serde_json", + "sha1", + "thiserror", + "tokio", +] + +[[package]] +name = "librespot-metadata" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a10ab5a390f65281e763cd09c617b173f0e665994eae3d242526924625fdc66" +dependencies = [ + "async-trait", + "bytes", + "librespot-core", + "librespot-protocol", + "log", + "protobuf", + "serde", + "serde_json", + "thiserror", + "uuid", +] + +[[package]] +name = "librespot-oauth" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bda94233b358fb41c04ed15507c61136c80efe876c6e05a10ddb9a182b144e" +dependencies = [ + "log", + "oauth2", + "thiserror", + "url", +] + +[[package]] +name = "librespot-playback" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b1bcfe1d72c5ac14c798c7e3e1c20e1fb6af2b9c254794545cfcb1f2a4627e2" +dependencies = [ + "alsa", + "cpal", + "futures-util", + "gstreamer", + "gstreamer-app", + "gstreamer-audio", + "libpulse-binding", + "libpulse-simple-binding", + "librespot-audio", + "librespot-core", + "librespot-metadata", + "log", + "parking_lot", + "rand", + "rand_distr", + "rodio", + "shell-words", + "symphonia", + "thiserror", + "tokio", + "zerocopy", +] + +[[package]] +name = "librespot-protocol" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6f343f573e0469d3ff8a02b99bbd9789faa01e2ff167332542ac840a8b31e7" +dependencies = [ + "protobuf", + "protobuf-codegen", +] + +[[package]] +name = "libz-sys" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" + +[[package]] +name = "locale_config" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934" +dependencies = [ + "lazy_static", + "objc", + "objc-foundation", + "regex", + "winapi", +] + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +dependencies = [ + "value-bag", +] + +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "mirai-annotations" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" + +[[package]] +name = "muldiv" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0" + +[[package]] +name = "multimap" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" +dependencies = [ + "serde", +] + +[[package]] +name = "ndk" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +dependencies = [ + "bitflags 2.6.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.7.1", +] + +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[package]] +name = "num" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", + "rand", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "oauth2" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f" +dependencies = [ + "base64 0.13.1", + "chrono", + "getrandom", + "http 0.2.9", + "rand", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror", + "url", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "oboe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" +dependencies = [ + "jni", + "ndk", + "ndk-context", + "num-derive 0.4.2", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" +dependencies = [ + "cc", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "open" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a877bf6abd716642a53ef1b89fb498923a4afca5c754f9050b4d081c05c4b3" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f55da20b29f956fb01f0add8683eb26ee13ebe3ebd935e49898717c6b4b2830" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-operations" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c26d27bb1aeab65138e4bf7666045169d1717febcc9ff870166be8348b223d0" +dependencies = [ + "paste", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "pango" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35be456fc620e61f62dff7ff70fbd54dcbaf0a4b920c0f16de1107c47d921d48" +dependencies = [ + "bitflags 1.3.2", + "gio", + "glib 0.17.10", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da69f9f3850b0d8990d462f8c709561975e95f689c1cdf0fecdebde78b35195" +dependencies = [ + "glib-sys 0.17.10", + "gobject-sys 0.17.10", + "libc", + "system-deps 6.2.0", +] + +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "backtrace", + "cfg-if", + "libc", + "petgraph", + "redox_syscall", + "smallvec", + "thread-id", + "windows-targets 0.48.5", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "pathdiff" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361" + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand 2.0.1", + "futures-io", +] + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e53b6af1f60f36f8c2ac2aad5459d75a5a9b4be1e8cdd40264f315d78193e531" +dependencies = [ + "cfg-if", + "concurrent-queue", + "pin-project-lite", + "rustix 0.38.21", + "tracing", + "windows-sys 0.48.0", +] + +[[package]] +name = "portable-atomic" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "prettyplease" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" +dependencies = [ + "proc-macro2", + "syn 2.0.85", +] + +[[package]] +name = "priority-queue" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714c75db297bc88a63783ffc6ab9f830698a6705aa0201416931759ef4c8183d" +dependencies = [ + "autocfg", + "equivalent", + "indexmap", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit 0.22.22", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "protobuf" +version = "3.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3a7c64d9bf75b1b8d981124c14c179074e8caa7dfe7b6a12e6222ddcd0c8f72" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror", +] + +[[package]] +name = "protobuf-codegen" +version = "3.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e26b833f144769a30e04b1db0146b2aaa53fd2fd83acf10a6b5f996606c18144" +dependencies = [ + "anyhow", + "once_cell", + "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror", +] + +[[package]] +name = "protobuf-parse" +version = "3.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322330e133eab455718444b4e033ebfac7c6528972c784fcde28d2cc783c6257" +dependencies = [ + "anyhow", + "indexmap", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b088fd20b938a875ea00843b6faf48579462630015c3788d397ad6a786663252" +dependencies = [ + "thiserror", +] + +[[package]] +name = "quick-xml" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "ref_filter_map" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b5ceb840e4009da4841ed22a15eb49f64fdd00a2138945c5beacf506b2fb5ed" + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.5", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.9", + "http-body 0.4.5", + "hyper 0.14.27", + "hyper-rustls 0.24.2", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls 0.21.12", + "rustls-pemfile 1.0.4", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-rustls 0.24.1", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rodio" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6006a627c1a38d37f3d3a85c6575418cfe34a5392d60a686d0071e1c8d427acb" +dependencies = [ + "cpal", + "thiserror", +] + +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.37.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys 0.4.10", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls" +version = "0.23.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fbb44d7acc4e873d613422379f69f237a1b141928c02f6bc6ccfddddc2d7993" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe", + "rustls-pemfile 2.2.0", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a" +dependencies = [ + "openssl-probe", + "rustls-pemfile 2.2.0", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.5", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "secret-service" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da1a5ad4d28c03536f82f77d9f36603f5e37d8869ac98f0a750d5b5686d8d95" +dependencies = [ + "aes 0.7.5", + "block-modes", + "futures-util", + "generic-array", + "hkdf", + "num", + "once_cell", + "rand", + "serde", + "sha2", + "zbus", +] + +[[package]] +name = "security-framework" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" + +[[package]] +name = "serde" +version = "1.0.213" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.213" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "serde_spanned" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shannon" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ea5b41c9427b56caa7b808cb548a04fb50bb5b9e98590b53f28064ff4174561" +dependencies = [ + "byteorder", +] + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] +name = "shlex" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "sluice" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5" +dependencies = [ + "async-channel", + "futures-core", + "futures-io", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spinning_top" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "spot" +version = "0.5.0" +dependencies = [ + "async-std", + "env_logger 0.10.0", + "form_urlencoded", + "futures", + "gdk-pixbuf", + "gdk4", + "gettext-rs", + "gio", + "glib 0.17.10", + "gtk4", + "isahc", + "lazy_static", + "libadwaita", + "librespot", + "log", + "oauth2", + "open", + "percent-encoding", + "rand", + "ref_filter_map", + "regex", + "secret-service", + "serde", + "serde_json", + "thiserror", + "tokio", + "url", + "zbus", + "zvariant", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "symphonia" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" +dependencies = [ + "lazy_static", + "symphonia-bundle-mp3", + "symphonia-codec-vorbis", + "symphonia-core", + "symphonia-format-ogg", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30" +dependencies = [ + "log", + "symphonia-core", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-core" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-utils-xiph" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sysinfo" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "windows 0.57.0", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331" +dependencies = [ + "cfg-expr 0.15.5", + "heck 0.4.1", + "pkg-config", + "toml", + "version-compare 0.1.1", +] + +[[package]] +name = "system-deps" +version = "7.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d23aaf9f331227789a99e8de4c91bf46703add012bdfd45fdecdfb2975a005" +dependencies = [ + "cfg-expr 0.17.0", + "heck 0.5.0", + "pkg-config", + "toml", + "version-compare 0.2.0", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "temp-dir" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af547b166dd1ea4b472165569fc456cfb6818116f854690b0ff205e636523dab" + +[[package]] +name = "tempfile" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +dependencies = [ + "cfg-if", + "fastrand 2.0.1", + "redox_syscall", + "rustix 0.38.21", + "windows-sys 0.48.0", +] + +[[package]] +name = "termcolor" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "thread-id" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0ec81c46e9eb50deaa257be2f148adf052d1fb7701cfd55ccfab2525280b70b" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.5.5", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls 0.23.15", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "rustls 0.23.15", + "rustls-native-certs 0.8.0", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.0", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ff9e3abce27ee2c9a37f9ad37238c1bdd4e789c84ba37df76aa4d528f5072cc" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.20.7", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.5.18", +] + +[[package]] +name = "toml_edit" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.5.18", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.6.20", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand", + "rustls 0.23.15", + "rustls-pki-types", + "sha1", + "thiserror", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "uds_windows" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" +dependencies = [ + "tempfile", + "winapi", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" +dependencies = [ + "getrandom", + "rand", +] + +[[package]] +name = "value-bag" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vergen" +version = "9.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349ed9e45296a581f455bc18039878f409992999bc1d5da12a6800eb18c8752f" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", + "time", + "vergen-lib", +] + +[[package]] +name = "vergen-gitcl" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a3a7f91caabecefc3c249fd864b11d4abe315c166fbdb568964421bccfd2b7a" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", + "time", + "vergen", + "vergen-lib", +] + +[[package]] +name = "vergen-lib" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229eaddb0050920816cf051e619affaf18caa3dd512de8de5839ccbc8e53abb0" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", +] + +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "waker-fn" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.85", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "web-sys" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.21", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176b6138793677221d420fd2f0aeeced263f197688b36484660da767bca2fa32" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "xdg-home" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2769203cd13a0c6015d515be729c526d041e9cf2c0cc478d57faee85f40c6dcd" +dependencies = [ + "nix", + "winapi", +] + +[[package]] +name = "zbus" +version = "3.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31de390a2d872e4cd04edd71b425e29853f786dc99317ed72d73d6fcf5ebb948" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io 1.13.0", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "byteorder", + "derivative", + "enumflags2", + "event-listener 2.5.3", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "once_cell", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "winapi", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "3.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d1794a946878c0e807f55a397187c11fc7a038ba5d868e7db4f3bd7760bc9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb80bb776dbda6e23d705cf0123c3b95df99c4ebeaec6c2599d4a5419902b4a9" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zvariant" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44b291bee0d960c53170780af148dca5fa260a63cdd24f1962fa82e03e53338c" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "934d7a7dfc310d6ee06c87ffe88ef4eca7d3e37bb251dece2ef93da8f17d8ecd" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7640cd3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,78 @@ +[package] +name = "spot" +version = "0.5.0" +edition = "2018" +license = "MIT" + +[dependencies.gtk] +version = "^0.6.6" +package = "gtk4" +features = ["gnome_44", "blueprint"] + +[dependencies.libadwaita] +version = "^0.4.1" +features = ["v1_2"] + +[dependencies.gdk] +version = "^0.6.3" +package = "gdk4" + +[dependencies.gio] +version = "^0.17.9" +features = ["v2_60"] + +[dependencies.glib] +version = "^0.17.9" +features = ["v2_60"] + +[dependencies.librespot] +version = "0.5.0" +features = ["alsa-backend", "pulseaudio-backend", "gstreamer-backend"] + +[dependencies.tokio] +version = "1" +features = ["rt", "macros", "sync"] + +[dependencies.futures] +package = "futures" +version = "0.3.18" + +[dependencies.serde] +version = "^1.0.136" +features = ["derive"] + +[dependencies.serde_json] +version = "^1.0.96" + +[dependencies.isahc] +version = "^1.7.2" +features = ["json"] + +[dependencies.rand] +version = "^0.8.5" +features = ["small_rng"] + +[dependencies.gettext-rs] +version = "0.7.0" +features = ["gettext-system"] + +[dependencies.secret-service] +version = "3.0.1" +features = ["rt-async-io-crypto-rust"] + +[dependencies] +gdk-pixbuf = "0.17.0" +ref_filter_map = "1.0.1" +regex = "1.8.3" +async-std = "1.12.0" +form_urlencoded = "1.0.1" +zbus = "3.13" +zvariant = "3.14" +thiserror = "1.0.40" +lazy_static = "1.4.0" +log = "0.4.17" +env_logger = "0.10.0" +percent-encoding = "2.2.0" +oauth2 = "4.4" +url = "2.4.1" +open = "5.3.0" \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a7c3b62 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Alexandre Trendel + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c9ee930 --- /dev/null +++ b/README.md @@ -0,0 +1,150 @@ +# Spot [![spot-snapshots](https://github.com/xou816/spot/actions/workflows/spot-snapshots.yml/badge.svg?branch=master)](https://github.com/xou816/spot/actions/workflows/spot-snapshots.yml) + +Gtk/Rust native Spotify client for the GNOME desktop. **Only works with premium accounts!** + +Based on [librespot](https://github.com/librespot-org/librespot/). + +Join the discussion on [Matrix](https://matrix.to/#/#spot-devel:matrix.org). + +![Spot screenshot](./data/appstream/2.png) + +## Installing + +Download on Flathub + +## Usage notes + +### Credentials + +It is recommended to install a libsecret compliant keyring application, such as [GNOME Keyring](https://wiki.gnome.org/action/show/Projects/GnomeKeyring) (aka seahorse). This will allow saving your password securely between launches. + +In GNOME, things should work out of the box. It might be a bit trickier to get it working in other DEs: see this [ArchWiki entry](https://wiki.archlinux.org/index.php/GNOME/Keyring) for detailed explanations on how to automatically start the daemon with your session. + +Bear special attention to the fact that to enable automatic login, you might have to use the same password for your user account and for the keyring, and that the keyring might need to be [set as default](https://wiki.archlinux.org/index.php/GNOME/Keyring#Passwords_are_not_remembered). + +See [this comment](https://github.com/xou816/spot/issues/92#issuecomment-801852593) for more details! + +### Login in with Facebook + +...is not supported. However, you can update your account in order to be able to log in with a username and password [as explained in this issue](https://github.com/xou816/spot/issues/373). + + +### Settings + +Spot can also be configured via `gsettings` if you want to change the audio backend, the song bitrate, etc. + +### Seek bar warping +It is possible to click on the seek bar to navigate to that position in a song. If you are having issues with this not working you may have [gtk-primary-button-warps-slider](https://docs.gtk.org/gtk3/property.Settings.gtk-primary-button-warps-slider.html) set to false. +In order to fix this issue set the value to true in your gtk configuration. + +### Scrobbling + +Scrobbling is not supported directly by Spot. However, you can use a tool such a [rescrobbled](https://github.com/InputUsername/rescrobbled) ([see #85](https://github.com/xou816/spot/issues/85)). + +### Lyrics + +Similarly, Spot does not display lyrics for songs, but you can use [osdlyrics](https://github.com/osdlyrics/osdlyrics) ([see #226](https://github.com/xou816/spot/issues/226)). + +### Gtk theme + +Spot uses the dark theme variant by default; this can be changed in the settings. + +If you are using the flatpak version, don't forget to install your theme with flatpak as well. See [this comment](https://github.com/xou816/spot/issues/209#issuecomment-860180537) for details. + +Similarly, snap also requires that you install the corresponding snap for your theme. See [this comment](https://github.com/xou816/spot/issues/338#issuecomment-975543476) for details. + +## Features + +**Only works with premium accounts!** + +- playback control (play/pause, prev/next, seeking, shuffle, repeat (none, all, song)) +- selection mode: easily browse and select mutliple tracks to queue them +- browse your saved albums and playlists +- search albums and artists +- view an artist's releases +- view users' playlists +- view album info +- credentials management with Secret Service +- MPRIS integration +- playlist management (creation and edition) +- liked tracks + +### Planned + +- GNOME search provider? +- improved search? (track results) +- recommendations? + +## Contributing + +Contributions are welcome! If you wish, add yourself to the `AUTHORS` files when submitting your contribution. + +For any large feature/change, **please** open an issue first to discuss implementation and design decisions. + +### Translating + +Translations are managed using `gettext` and are available in the `po/` subdirectory. + +**Please use [POEditor](https://poeditor.com/join/project?hash=xfVrpQfRBM) to submit translations.** + +If you feel like it, you are welcome to open a PR to be added to the `TRANSLATORS` file! + +## Building + +### With GNOME Builder and flatpak + +Pre-requisite: install the `org.freedesktop.Sdk.Extension.rust-stable` SDK extension with flatpak. Builder might do this for you automatically, but it will install an older version; make sure the version installed matches the version of the Freedesktop SDK GNOME uses. + +Open the project in GNOME Builder and make the `dev.alextren.Spot.development.json` configuration active. Then build :) + +### Manually + +Requires Rust (stable), **GTK4**, and a couple other things. Also requires **libadwaita** and **blueprint-compiler**: they are not packaged on all distros at the moment, you might have to build them yourself! + +With meson: + +``` +meson target -Dbuildtype=debug -Doffline=false --prefix="$HOME/.local" +ninja install -C target +# to run test/linter/etc +meson test -C target --verbose +``` + +This will install a `.desktop` file among other things, and the spot executable will be put in `.local/bin` (you might want to add it to your path). + +To build an optimized release build, use `-Dbuildtype=release` instead. + +### Regenerating potfiles + +When adding new `msgids`, don't forget to regenerate/update the potfiles. + +``` +ninja spot-pot -C target +ninja spot-update-po -C target +``` + +### Pulling updated strings from POEditor + +We are now using POEditor and the wonderful [`poeditor-sync`](https://github.com/mick88/poeditor-sync) tool. + +``` +poeditor pull +``` + +### Regenerating sources for flatpak + +Using [flatpak-cargo-generator.py](https://github.com/flatpak/flatpak-builder-tools/tree/master/cargo): + +``` +ninja cargo-sources.json -C target +``` + +### Debugging + +Set the `RUST_LOG` env variable to the appropriate level. + +Debug builds (flatpak) are available from the master branch on Github (see the `spot-snaphots` action). + +Spot caches images and HTTP responses in `~/.cache/spot`. + +Spot uses [isahc](https://github.com/sagebind/isahc), which uses libcurl, therefore you can set the `https_proxy` env variable to help with debugging. In debug mode, Spot skips SSL certificate verification. diff --git a/TRANSLATORS b/TRANSLATORS new file mode 100644 index 0000000..d816210 --- /dev/null +++ b/TRANSLATORS @@ -0,0 +1,25 @@ +Heimen Stoffels +Philipp Kiemle +kleinHeiti +Ondřej Sluka +Ícar N. S. +Jonasz Potoniec +José Miguel Sarasola +Hugo Meireles Gonçalves +Paul Bragin +Yusuf Çınar Özmen +Kukuh Syafaat +Lucas Araujo +Igor Dyatlov +Jiri Grönroos +Guilherme +Lars Martinsen +Sergio +SoftInterlingua +Tine Jozelj +Ana Pika Šubic +Amor Ali +Seioo Inoue +Dmitry +Julius Rüberg +Francesco Babetto diff --git a/build-aux/flatpak-cargo-generator.py b/build-aux/flatpak-cargo-generator.py new file mode 100644 index 0000000..4181e51 --- /dev/null +++ b/build-aux/flatpak-cargo-generator.py @@ -0,0 +1,450 @@ +#!/usr/bin/env python3 + +__license__ = 'MIT' +import json +from urllib.parse import urlparse, ParseResult, parse_qs +import os +import contextlib +import copy +import glob +import subprocess +import argparse +import logging +import hashlib +import asyncio +from typing import Any, Dict, List, NamedTuple, Optional, Tuple, TypedDict + +import aiohttp +import toml + +CRATES_IO = 'https://static.crates.io/crates' +CARGO_HOME = 'cargo' +CARGO_CRATES = f'{CARGO_HOME}/vendor' +VENDORED_SOURCES = 'vendored-sources' +GIT_CACHE = 'flatpak-cargo/git' +COMMIT_LEN = 7 + + +@contextlib.contextmanager +def workdir(path: str): + oldpath = os.getcwd() + os.chdir(path) + try: + yield + finally: + os.chdir(oldpath) + + +def canonical_url(url: str) -> ParseResult: + 'Converts a string to a Cargo Canonical URL, as per https://github.com/rust-lang/cargo/blob/35c55a93200c84a4de4627f1770f76a8ad268a39/src/cargo/util/canonical_url.rs#L19' + # Hrm. The upstream cargo does not replace those URLs, but if we don't then it doesn't work too well :( + url = url.replace('git+https://', 'https://') + u = urlparse(url) + # It seems cargo drops query and fragment + u = ParseResult(u.scheme, u.netloc, u.path, '', '', '') + u = u._replace(path = u.path.rstrip('/')) + + if u.netloc == 'github.com': + u = u._replace(scheme = 'https') + u = u._replace(path = u.path.lower()) + + if u.path.endswith('.git'): + u = u._replace(path = u.path[:-len('.git')]) + + return u + + +def get_git_tarball(repo_url: str, commit: str) -> str: + url = canonical_url(repo_url) + path = url.path.split('/')[1:] + + assert len(path) == 2 + owner = path[0] + if path[1].endswith('.git'): + repo = path[1].replace('.git', '') + else: + repo = path[1] + if url.hostname == 'github.com': + return f'https://codeload.{url.hostname}/{owner}/{repo}/tar.gz/{commit}' + elif url.hostname.split('.')[0] == 'gitlab': # type: ignore + return f'https://{url.hostname}/{owner}/{repo}/-/archive/{commit}/{repo}-{commit}.tar.gz' + elif url.hostname == 'bitbucket.org': + return f'https://{url.hostname}/{owner}/{repo}/get/{commit}.tar.gz' + else: + raise ValueError(f'Don\'t know how to get tarball for {repo_url}') + + +async def get_remote_sha256(url: str) -> str: + logging.info(f"started sha256({url})") + sha256 = hashlib.sha256() + async with aiohttp.ClientSession(raise_for_status=True) as http_session: + async with http_session.get(url) as response: + while True: + data = await response.content.read(4096) + if not data: + break + sha256.update(data) + logging.info(f"done sha256({url})") + return sha256.hexdigest() + + +_TomlType = Dict[str, Any] + + +def load_toml(tomlfile: str = 'Cargo.lock') -> _TomlType: + with open(tomlfile, 'r') as f: + toml_data = toml.load(f) + return toml_data + + +def git_repo_name(git_url: str, commit: str) -> str: + name = canonical_url(git_url).path.split('/')[-1] + return f'{name}-{commit[:COMMIT_LEN]}' + + +def fetch_git_repo(git_url: str, commit: str) -> str: + repo_dir = git_url.replace('://', '_').replace('/', '_') + cache_dir = os.environ.get('XDG_CACHE_HOME', os.path.expanduser('~/.cache')) + clone_dir = os.path.join(cache_dir, 'flatpak-cargo', repo_dir) + if not os.path.isdir(os.path.join(clone_dir, '.git')): + subprocess.run(['git', 'clone', '--depth=1', git_url, clone_dir], check=True) + rev_parse_proc = subprocess.run(['git', 'rev-parse', 'HEAD'], cwd=clone_dir, check=True, + stdout=subprocess.PIPE) + head = rev_parse_proc.stdout.decode().strip() + if head[:COMMIT_LEN] != commit[:COMMIT_LEN]: + subprocess.run(['git', 'fetch', 'origin', commit], cwd=clone_dir, check=True) + subprocess.run(['git', 'checkout', commit], cwd=clone_dir, check=True) + return clone_dir + + +class _GitPackage(NamedTuple): + path: str + package: _TomlType + workspace: Optional[_TomlType] + + @property + def normalized(self) -> _TomlType: + package = copy.deepcopy(self.package) + if self.workspace is None: + return package + for section_key, section in package.items(): + # XXX We ignore top-level lists here; maybe we should iterate over list items, too + if not isinstance(section, dict): + continue + for key, value in section.items(): + if not isinstance(value, dict): + continue + if not value.get('workspace'): + continue + package[section_key][key] = self.workspace[section_key][key] + return package + + +_GitPackagesType = Dict[str, _GitPackage] + + +async def get_git_repo_packages(git_url: str, commit: str) -> _GitPackagesType: + logging.info('Loading packages from %s', git_url) + git_repo_dir = fetch_git_repo(git_url, commit) + packages: _GitPackagesType = {} + + with workdir(git_repo_dir): + if os.path.isfile('Cargo.toml'): + packages.update(await get_cargo_toml_packages(load_toml('Cargo.toml'), '.')) + else: + for toml_path in glob.glob('*/Cargo.toml'): + packages.update(await get_cargo_toml_packages(load_toml(toml_path), + os.path.dirname(toml_path))) + + assert packages, f"No packages found in {git_repo_dir}" + logging.debug( + 'Packages in %s:\n%s', + git_url, + json.dumps( + {k: v.path for k, v in packages.items()}, + indent=4, + ), + ) + return packages + + +async def get_cargo_toml_packages(root_toml: _TomlType, root_dir: str) -> _GitPackagesType: + assert not os.path.isabs(root_dir) and os.path.isdir(root_dir) + assert 'package' in root_toml or 'workspace' in root_toml + packages: _GitPackagesType = {} + + async def get_dep_packages( + entry: _TomlType, + toml_dir: str, + workspace: Optional[_TomlType] = None, + ): + assert not os.path.isabs(toml_dir) + # https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html + if 'dependencies' in entry: + for dep_name, dep in entry['dependencies'].items(): + if 'package' in dep: + dep_name = dep['package'] + if 'path' not in dep: + continue + if dep_name in packages: + continue + dep_dir = os.path.normpath(os.path.join(toml_dir, dep['path'])) + logging.debug("Loading dependency %s from %s", dep_name, dep_dir) + dep_toml = load_toml(os.path.join(dep_dir, 'Cargo.toml')) + assert dep_toml['package']['name'] == dep_name, toml_dir + await get_dep_packages(dep_toml, dep_dir, workspace) + packages[dep_name] = _GitPackage( + path=dep_dir, + package=dep, + workspace=workspace, + ) + if 'target' in entry: + for _, target in entry['target'].items(): + await get_dep_packages(target, toml_dir) + + if 'package' in root_toml: + await get_dep_packages(root_toml, root_dir) + packages[root_toml['package']['name']] = _GitPackage( + path=root_dir, + package=root_toml, + workspace=None, + ) + + if 'workspace' in root_toml: + for member in root_toml['workspace'].get('members', []): + for subpkg_toml in glob.glob(os.path.join(root_dir, member, 'Cargo.toml')): + subpkg = os.path.normpath(os.path.dirname(subpkg_toml)) + logging.debug( + "Loading workspace member %s in %s", + subpkg_toml, + os.path.abspath(root_dir), + ) + pkg_toml = load_toml(subpkg_toml) + await get_dep_packages(pkg_toml, subpkg, root_toml['workspace']) + packages[pkg_toml['package']['name']] = _GitPackage( + path=subpkg, + package=pkg_toml, + workspace=root_toml['workspace'], + ) + + return packages + + +_FlatpakSourceType = Dict[str, Any] + + +async def get_git_repo_sources( + url: str, + commit: str, + tarball: bool = False, +) -> List[_FlatpakSourceType]: + name = git_repo_name(url, commit) + if tarball: + tarball_url = get_git_tarball(url, commit) + git_repo_sources = [{ + 'type': 'archive', + 'archive-type': 'tar-gzip', + 'url': tarball_url, + 'sha256': await get_remote_sha256(tarball_url), + 'dest': f'{GIT_CACHE}/{name}', + }] + else: + git_repo_sources = [{ + 'type': 'git', + 'url': url, + 'commit': commit, + 'dest': f'{GIT_CACHE}/{name}', + }] + return git_repo_sources + + +_GitRepo = TypedDict('_GitRepo', {'lock': asyncio.Lock, 'commits': Dict[str, _GitPackagesType]}) +_GitReposType = Dict[str, _GitRepo] +_VendorEntryType = Dict[str, Dict[str, str]] + + +async def get_git_package_sources( + package: _TomlType, + git_repos: _GitReposType, +) -> Tuple[List[_FlatpakSourceType], _VendorEntryType]: + name = package['name'] + source = package['source'] + commit = urlparse(source).fragment + assert commit, 'The commit needs to be indicated in the fragement part' + canonical = canonical_url(source) + repo_url = canonical.geturl() + + git_repo = git_repos.setdefault(repo_url, { + 'commits': {}, + 'lock': asyncio.Lock(), + }) + async with git_repo['lock']: + if commit not in git_repo['commits']: + git_repo['commits'][commit] = await get_git_repo_packages(repo_url, commit) + + cargo_vendored_entry: _VendorEntryType = { + repo_url: { + 'git': repo_url, + 'replace-with': VENDORED_SOURCES, + } + } + rev = parse_qs(urlparse(source).query).get('rev') + tag = parse_qs(urlparse(source).query).get('tag') + branch = parse_qs(urlparse(source).query).get('branch') + if rev: + assert len(rev) == 1 + cargo_vendored_entry[repo_url]['rev'] = rev[0] + elif tag: + assert len(tag) == 1 + cargo_vendored_entry[repo_url]['tag'] = tag[0] + elif branch: + assert len(branch) == 1 + cargo_vendored_entry[repo_url]['branch'] = branch[0] + + logging.info("Adding package %s from %s", name, repo_url) + git_pkg = git_repo['commits'][commit][name] + pkg_repo_dir = os.path.join(GIT_CACHE, git_repo_name(repo_url, commit), git_pkg.path) + git_sources: List[_FlatpakSourceType] = [ + { + 'type': 'shell', + 'commands': [ + f'cp -r --reflink=auto "{pkg_repo_dir}" "{CARGO_CRATES}/{name}"' + ], + }, + { + 'type': 'inline', + 'contents': toml.dumps(git_pkg.normalized), + 'dest': f'{CARGO_CRATES}/{name}', #-{version}', + 'dest-filename': 'Cargo.toml', + }, + { + 'type': 'inline', + 'contents': json.dumps({'package': None, 'files': {}}), + 'dest': f'{CARGO_CRATES}/{name}', #-{version}', + 'dest-filename': '.cargo-checksum.json', + } + ] + + return (git_sources, cargo_vendored_entry) + + +async def get_package_sources( + package: _TomlType, + cargo_lock: _TomlType, + git_repos: _GitReposType, +) -> Optional[Tuple[List[_FlatpakSourceType], _VendorEntryType]]: + metadata = cargo_lock.get('metadata') + name = package['name'] + version = package['version'] + + if 'source' not in package: + logging.debug('%s has no source', name) + return None + source = package['source'] + + if source.startswith('git+'): + return await get_git_package_sources(package, git_repos) + + key = f'checksum {name} {version} ({source})' + if metadata is not None and key in metadata: + checksum = metadata[key] + elif 'checksum' in package: + checksum = package['checksum'] + else: + logging.warning(f'{name} doesn\'t have checksum') + return None + crate_sources = [ + { + 'type': 'archive', + 'archive-type': 'tar-gzip', + 'url': f'{CRATES_IO}/{name}/{name}-{version}.crate', + 'sha256': checksum, + 'dest': f'{CARGO_CRATES}/{name}-{version}', + }, + { + 'type': 'inline', + 'contents': json.dumps({'package': checksum, 'files': {}}), + 'dest': f'{CARGO_CRATES}/{name}-{version}', + 'dest-filename': '.cargo-checksum.json', + }, + ] + return (crate_sources, {'crates-io': {'replace-with': VENDORED_SOURCES}}) + + +async def generate_sources( + cargo_lock: _TomlType, + git_tarballs: bool = False, +) -> List[_FlatpakSourceType]: + # { + # "git-repo-url": { + # "lock": asyncio.Lock(), + # "commits": { + # "commit-hash": { + # "package-name": "./relative/package/path" + # } + # } + # } + # } + git_repos: _GitReposType = {} + sources: List[_FlatpakSourceType] = [] + package_sources = [] + cargo_vendored_sources = { + VENDORED_SOURCES: {'directory': f'{CARGO_CRATES}'}, + } + + pkg_coros = [get_package_sources(p, cargo_lock, git_repos) for p in cargo_lock['package']] + for pkg in await asyncio.gather(*pkg_coros): + if pkg is None: + continue + else: + pkg_sources, cargo_vendored_entry = pkg + package_sources.extend(pkg_sources) + cargo_vendored_sources.update(cargo_vendored_entry) + + logging.debug('Adding collected git repos:\n%s', json.dumps(list(git_repos), indent=4)) + git_repo_coros = [] + for git_url, git_repo in git_repos.items(): + for git_commit in git_repo['commits']: + git_repo_coros.append(get_git_repo_sources(git_url, git_commit, git_tarballs)) + sources.extend(sum(await asyncio.gather(*git_repo_coros), [])) + + sources.extend(package_sources) + + logging.debug('Vendored sources:\n%s', json.dumps(cargo_vendored_sources, indent=4)) + sources.append({ + 'type': 'inline', + 'contents': toml.dumps({ + 'source': cargo_vendored_sources, + }), + 'dest': CARGO_HOME, + 'dest-filename': 'config' + }) + return sources + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('cargo_lock', help='Path to the Cargo.lock file') + parser.add_argument('-o', '--output', required=False, help='Where to write generated sources') + parser.add_argument('-t', '--git-tarballs', action='store_true', help='Download git repos as tarballs') + parser.add_argument('-d', '--debug', action='store_true') + args = parser.parse_args() + if args.output is not None: + outfile = args.output + else: + outfile = 'generated-sources.json' + if args.debug: + loglevel = logging.DEBUG + else: + loglevel = logging.INFO + logging.basicConfig(level=loglevel) + + generated_sources = asyncio.run(generate_sources(load_toml(args.cargo_lock), + git_tarballs=args.git_tarballs)) + with open(outfile, 'w') as out: + json.dump(generated_sources, out, indent=4, sort_keys=False) + + +if __name__ == '__main__': + main() + diff --git a/cargo-sources.json b/cargo-sources.json new file mode 100644 index 0000000..f307901 --- /dev/null +++ b/cargo-sources.json @@ -0,0 +1,6898 @@ +[ + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/addr2line/addr2line-0.21.0.crate", + "sha256": "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb", + "dest": "cargo/vendor/addr2line-0.21.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb\", \"files\": {}}", + "dest": "cargo/vendor/addr2line-0.21.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/adler/adler-1.0.2.crate", + "sha256": "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe", + "dest": "cargo/vendor/adler-1.0.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe\", \"files\": {}}", + "dest": "cargo/vendor/adler-1.0.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/aes/aes-0.7.5.crate", + "sha256": "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8", + "dest": "cargo/vendor/aes-0.7.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8\", \"files\": {}}", + "dest": "cargo/vendor/aes-0.7.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/aes/aes-0.8.4.crate", + "sha256": "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0", + "dest": "cargo/vendor/aes-0.8.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0\", \"files\": {}}", + "dest": "cargo/vendor/aes-0.8.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/aho-corasick/aho-corasick-1.1.2.crate", + "sha256": "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0", + "dest": "cargo/vendor/aho-corasick-1.1.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0\", \"files\": {}}", + "dest": "cargo/vendor/aho-corasick-1.1.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/alsa/alsa-0.9.1.crate", + "sha256": "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43", + "dest": "cargo/vendor/alsa-0.9.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43\", \"files\": {}}", + "dest": "cargo/vendor/alsa-0.9.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/alsa-sys/alsa-sys-0.3.1.crate", + "sha256": "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527", + "dest": "cargo/vendor/alsa-sys-0.3.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527\", \"files\": {}}", + "dest": "cargo/vendor/alsa-sys-0.3.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/android-tzdata/android-tzdata-0.1.1.crate", + "sha256": "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0", + "dest": "cargo/vendor/android-tzdata-0.1.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0\", \"files\": {}}", + "dest": "cargo/vendor/android-tzdata-0.1.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/android_system_properties/android_system_properties-0.1.5.crate", + "sha256": "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311", + "dest": "cargo/vendor/android_system_properties-0.1.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311\", \"files\": {}}", + "dest": "cargo/vendor/android_system_properties-0.1.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/anstream/anstream-0.6.17.crate", + "sha256": "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338", + "dest": "cargo/vendor/anstream-0.6.17" + }, + { + "type": "inline", + "contents": "{\"package\": \"23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338\", \"files\": {}}", + "dest": "cargo/vendor/anstream-0.6.17", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/anstyle/anstyle-1.0.9.crate", + "sha256": "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56", + "dest": "cargo/vendor/anstyle-1.0.9" + }, + { + "type": "inline", + "contents": "{\"package\": \"8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56\", \"files\": {}}", + "dest": "cargo/vendor/anstyle-1.0.9", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/anstyle-parse/anstyle-parse-0.2.6.crate", + "sha256": "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9", + "dest": "cargo/vendor/anstyle-parse-0.2.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9\", \"files\": {}}", + "dest": "cargo/vendor/anstyle-parse-0.2.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/anstyle-query/anstyle-query-1.1.2.crate", + "sha256": "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c", + "dest": "cargo/vendor/anstyle-query-1.1.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c\", \"files\": {}}", + "dest": "cargo/vendor/anstyle-query-1.1.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/anstyle-wincon/anstyle-wincon-3.0.6.crate", + "sha256": "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125", + "dest": "cargo/vendor/anstyle-wincon-3.0.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125\", \"files\": {}}", + "dest": "cargo/vendor/anstyle-wincon-3.0.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/anyhow/anyhow-1.0.91.crate", + "sha256": "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8", + "dest": "cargo/vendor/anyhow-1.0.91" + }, + { + "type": "inline", + "contents": "{\"package\": \"c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8\", \"files\": {}}", + "dest": "cargo/vendor/anyhow-1.0.91", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/arrayvec/arrayvec-0.7.6.crate", + "sha256": "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50", + "dest": "cargo/vendor/arrayvec-0.7.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50\", \"files\": {}}", + "dest": "cargo/vendor/arrayvec-0.7.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/async-broadcast/async-broadcast-0.5.1.crate", + "sha256": "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b", + "dest": "cargo/vendor/async-broadcast-0.5.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b\", \"files\": {}}", + "dest": "cargo/vendor/async-broadcast-0.5.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/async-channel/async-channel-1.9.0.crate", + "sha256": "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35", + "dest": "cargo/vendor/async-channel-1.9.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35\", \"files\": {}}", + "dest": "cargo/vendor/async-channel-1.9.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/async-executor/async-executor-1.6.0.crate", + "sha256": "4b0c4a4f319e45986f347ee47fef8bf5e81c9abc3f6f58dc2391439f30df65f0", + "dest": "cargo/vendor/async-executor-1.6.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"4b0c4a4f319e45986f347ee47fef8bf5e81c9abc3f6f58dc2391439f30df65f0\", \"files\": {}}", + "dest": "cargo/vendor/async-executor-1.6.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/async-fs/async-fs-1.6.0.crate", + "sha256": "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06", + "dest": "cargo/vendor/async-fs-1.6.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06\", \"files\": {}}", + "dest": "cargo/vendor/async-fs-1.6.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/async-global-executor/async-global-executor-2.3.1.crate", + "sha256": "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776", + "dest": "cargo/vendor/async-global-executor-2.3.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776\", \"files\": {}}", + "dest": "cargo/vendor/async-global-executor-2.3.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/async-io/async-io-1.13.0.crate", + "sha256": "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af", + "dest": "cargo/vendor/async-io-1.13.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af\", \"files\": {}}", + "dest": "cargo/vendor/async-io-1.13.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/async-io/async-io-2.1.0.crate", + "sha256": "10da8f3146014722c89e7859e1d7bb97873125d7346d10ca642ffab794355828", + "dest": "cargo/vendor/async-io-2.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"10da8f3146014722c89e7859e1d7bb97873125d7346d10ca642ffab794355828\", \"files\": {}}", + "dest": "cargo/vendor/async-io-2.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/async-lock/async-lock-2.8.0.crate", + "sha256": "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b", + "dest": "cargo/vendor/async-lock-2.8.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b\", \"files\": {}}", + "dest": "cargo/vendor/async-lock-2.8.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/async-process/async-process-1.8.1.crate", + "sha256": "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88", + "dest": "cargo/vendor/async-process-1.8.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88\", \"files\": {}}", + "dest": "cargo/vendor/async-process-1.8.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/async-recursion/async-recursion-1.0.5.crate", + "sha256": "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0", + "dest": "cargo/vendor/async-recursion-1.0.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0\", \"files\": {}}", + "dest": "cargo/vendor/async-recursion-1.0.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/async-signal/async-signal-0.2.5.crate", + "sha256": "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5", + "dest": "cargo/vendor/async-signal-0.2.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5\", \"files\": {}}", + "dest": "cargo/vendor/async-signal-0.2.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/async-std/async-std-1.12.0.crate", + "sha256": "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d", + "dest": "cargo/vendor/async-std-1.12.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d\", \"files\": {}}", + "dest": "cargo/vendor/async-std-1.12.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/async-task/async-task-4.5.0.crate", + "sha256": "b4eb2cdb97421e01129ccb49169d8279ed21e829929144f4a22a6e54ac549ca1", + "dest": "cargo/vendor/async-task-4.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"b4eb2cdb97421e01129ccb49169d8279ed21e829929144f4a22a6e54ac549ca1\", \"files\": {}}", + "dest": "cargo/vendor/async-task-4.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/async-trait/async-trait-0.1.74.crate", + "sha256": "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9", + "dest": "cargo/vendor/async-trait-0.1.74" + }, + { + "type": "inline", + "contents": "{\"package\": \"a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9\", \"files\": {}}", + "dest": "cargo/vendor/async-trait-0.1.74", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/atomic-waker/atomic-waker-1.1.2.crate", + "sha256": "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0", + "dest": "cargo/vendor/atomic-waker-1.1.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0\", \"files\": {}}", + "dest": "cargo/vendor/atomic-waker-1.1.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/atomic_refcell/atomic_refcell-0.1.13.crate", + "sha256": "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c", + "dest": "cargo/vendor/atomic_refcell-0.1.13" + }, + { + "type": "inline", + "contents": "{\"package\": \"41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c\", \"files\": {}}", + "dest": "cargo/vendor/atomic_refcell-0.1.13", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/autocfg/autocfg-1.1.0.crate", + "sha256": "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa", + "dest": "cargo/vendor/autocfg-1.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa\", \"files\": {}}", + "dest": "cargo/vendor/autocfg-1.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/aws-lc-rs/aws-lc-rs-1.10.0.crate", + "sha256": "cdd82dba44d209fddb11c190e0a94b78651f95299598e472215667417a03ff1d", + "dest": "cargo/vendor/aws-lc-rs-1.10.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"cdd82dba44d209fddb11c190e0a94b78651f95299598e472215667417a03ff1d\", \"files\": {}}", + "dest": "cargo/vendor/aws-lc-rs-1.10.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/aws-lc-sys/aws-lc-sys-0.22.0.crate", + "sha256": "df7a4168111d7eb622a31b214057b8509c0a7e1794f44c546d742330dc793972", + "dest": "cargo/vendor/aws-lc-sys-0.22.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"df7a4168111d7eb622a31b214057b8509c0a7e1794f44c546d742330dc793972\", \"files\": {}}", + "dest": "cargo/vendor/aws-lc-sys-0.22.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/backtrace/backtrace-0.3.69.crate", + "sha256": "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837", + "dest": "cargo/vendor/backtrace-0.3.69" + }, + { + "type": "inline", + "contents": "{\"package\": \"2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837\", \"files\": {}}", + "dest": "cargo/vendor/backtrace-0.3.69", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/base64/base64-0.13.1.crate", + "sha256": "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8", + "dest": "cargo/vendor/base64-0.13.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8\", \"files\": {}}", + "dest": "cargo/vendor/base64-0.13.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/base64/base64-0.21.5.crate", + "sha256": "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9", + "dest": "cargo/vendor/base64-0.21.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9\", \"files\": {}}", + "dest": "cargo/vendor/base64-0.21.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/base64/base64-0.22.1.crate", + "sha256": "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6", + "dest": "cargo/vendor/base64-0.22.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6\", \"files\": {}}", + "dest": "cargo/vendor/base64-0.22.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/base64ct/base64ct-1.6.0.crate", + "sha256": "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b", + "dest": "cargo/vendor/base64ct-1.6.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b\", \"files\": {}}", + "dest": "cargo/vendor/base64ct-1.6.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/bindgen/bindgen-0.68.1.crate", + "sha256": "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078", + "dest": "cargo/vendor/bindgen-0.68.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078\", \"files\": {}}", + "dest": "cargo/vendor/bindgen-0.68.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/bindgen/bindgen-0.69.5.crate", + "sha256": "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088", + "dest": "cargo/vendor/bindgen-0.69.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088\", \"files\": {}}", + "dest": "cargo/vendor/bindgen-0.69.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/bitflags/bitflags-1.3.2.crate", + "sha256": "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a", + "dest": "cargo/vendor/bitflags-1.3.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a\", \"files\": {}}", + "dest": "cargo/vendor/bitflags-1.3.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/bitflags/bitflags-2.6.0.crate", + "sha256": "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de", + "dest": "cargo/vendor/bitflags-2.6.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de\", \"files\": {}}", + "dest": "cargo/vendor/bitflags-2.6.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/block/block-0.1.6.crate", + "sha256": "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a", + "dest": "cargo/vendor/block-0.1.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a\", \"files\": {}}", + "dest": "cargo/vendor/block-0.1.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/block-buffer/block-buffer-0.10.4.crate", + "sha256": "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71", + "dest": "cargo/vendor/block-buffer-0.10.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71\", \"files\": {}}", + "dest": "cargo/vendor/block-buffer-0.10.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/block-modes/block-modes-0.8.1.crate", + "sha256": "2cb03d1bed155d89dce0f845b7899b18a9a163e148fd004e1c28421a783e2d8e", + "dest": "cargo/vendor/block-modes-0.8.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"2cb03d1bed155d89dce0f845b7899b18a9a163e148fd004e1c28421a783e2d8e\", \"files\": {}}", + "dest": "cargo/vendor/block-modes-0.8.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/block-padding/block-padding-0.2.1.crate", + "sha256": "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae", + "dest": "cargo/vendor/block-padding-0.2.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae\", \"files\": {}}", + "dest": "cargo/vendor/block-padding-0.2.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/blocking/blocking-1.4.1.crate", + "sha256": "8c36a4d0d48574b3dd360b4b7d95cc651d2b6557b6402848a27d4b228a473e2a", + "dest": "cargo/vendor/blocking-1.4.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"8c36a4d0d48574b3dd360b4b7d95cc651d2b6557b6402848a27d4b228a473e2a\", \"files\": {}}", + "dest": "cargo/vendor/blocking-1.4.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/bumpalo/bumpalo-3.14.0.crate", + "sha256": "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec", + "dest": "cargo/vendor/bumpalo-3.14.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec\", \"files\": {}}", + "dest": "cargo/vendor/bumpalo-3.14.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/bytemuck/bytemuck-1.19.0.crate", + "sha256": "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d", + "dest": "cargo/vendor/bytemuck-1.19.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d\", \"files\": {}}", + "dest": "cargo/vendor/bytemuck-1.19.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/byteorder/byteorder-1.5.0.crate", + "sha256": "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b", + "dest": "cargo/vendor/byteorder-1.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b\", \"files\": {}}", + "dest": "cargo/vendor/byteorder-1.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/bytes/bytes-1.5.0.crate", + "sha256": "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223", + "dest": "cargo/vendor/bytes-1.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223\", \"files\": {}}", + "dest": "cargo/vendor/bytes-1.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/cairo-rs/cairo-rs-0.17.10.crate", + "sha256": "ab3603c4028a5e368d09b51c8b624b9a46edcd7c3778284077a6125af73c9f0a", + "dest": "cargo/vendor/cairo-rs-0.17.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"ab3603c4028a5e368d09b51c8b624b9a46edcd7c3778284077a6125af73c9f0a\", \"files\": {}}", + "dest": "cargo/vendor/cairo-rs-0.17.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/cairo-sys-rs/cairo-sys-rs-0.17.10.crate", + "sha256": "691d0c66b1fb4881be80a760cb8fe76ea97218312f9dfe2c9cc0f496ca279cb1", + "dest": "cargo/vendor/cairo-sys-rs-0.17.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"691d0c66b1fb4881be80a760cb8fe76ea97218312f9dfe2c9cc0f496ca279cb1\", \"files\": {}}", + "dest": "cargo/vendor/cairo-sys-rs-0.17.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/castaway/castaway-0.1.2.crate", + "sha256": "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6", + "dest": "cargo/vendor/castaway-0.1.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6\", \"files\": {}}", + "dest": "cargo/vendor/castaway-0.1.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/cc/cc-1.1.10.crate", + "sha256": "e9e8aabfac534be767c909e0690571677d49f41bd8465ae876fe043d52ba5292", + "dest": "cargo/vendor/cc-1.1.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"e9e8aabfac534be767c909e0690571677d49f41bd8465ae876fe043d52ba5292\", \"files\": {}}", + "dest": "cargo/vendor/cc-1.1.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/cesu8/cesu8-1.1.0.crate", + "sha256": "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c", + "dest": "cargo/vendor/cesu8-1.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c\", \"files\": {}}", + "dest": "cargo/vendor/cesu8-1.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/cexpr/cexpr-0.6.0.crate", + "sha256": "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766", + "dest": "cargo/vendor/cexpr-0.6.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766\", \"files\": {}}", + "dest": "cargo/vendor/cexpr-0.6.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/cfg-expr/cfg-expr-0.15.5.crate", + "sha256": "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3", + "dest": "cargo/vendor/cfg-expr-0.15.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3\", \"files\": {}}", + "dest": "cargo/vendor/cfg-expr-0.15.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/cfg-expr/cfg-expr-0.17.0.crate", + "sha256": "d0890061c4d3223e7267f3bad2ec40b997d64faac1c2815a4a9d95018e2b9e9c", + "dest": "cargo/vendor/cfg-expr-0.17.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"d0890061c4d3223e7267f3bad2ec40b997d64faac1c2815a4a9d95018e2b9e9c\", \"files\": {}}", + "dest": "cargo/vendor/cfg-expr-0.17.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/cfg-if/cfg-if-1.0.0.crate", + "sha256": "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd", + "dest": "cargo/vendor/cfg-if-1.0.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd\", \"files\": {}}", + "dest": "cargo/vendor/cfg-if-1.0.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/chrono/chrono-0.4.31.crate", + "sha256": "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38", + "dest": "cargo/vendor/chrono-0.4.31" + }, + { + "type": "inline", + "contents": "{\"package\": \"7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38\", \"files\": {}}", + "dest": "cargo/vendor/chrono-0.4.31", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/cipher/cipher-0.3.0.crate", + "sha256": "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7", + "dest": "cargo/vendor/cipher-0.3.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7\", \"files\": {}}", + "dest": "cargo/vendor/cipher-0.3.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/cipher/cipher-0.4.4.crate", + "sha256": "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad", + "dest": "cargo/vendor/cipher-0.4.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad\", \"files\": {}}", + "dest": "cargo/vendor/cipher-0.4.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/clang-sys/clang-sys-1.6.1.crate", + "sha256": "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f", + "dest": "cargo/vendor/clang-sys-1.6.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f\", \"files\": {}}", + "dest": "cargo/vendor/clang-sys-1.6.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/cmake/cmake-0.1.51.crate", + "sha256": "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a", + "dest": "cargo/vendor/cmake-0.1.51" + }, + { + "type": "inline", + "contents": "{\"package\": \"fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a\", \"files\": {}}", + "dest": "cargo/vendor/cmake-0.1.51", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/colorchoice/colorchoice-1.0.3.crate", + "sha256": "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990", + "dest": "cargo/vendor/colorchoice-1.0.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990\", \"files\": {}}", + "dest": "cargo/vendor/colorchoice-1.0.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/combine/combine-4.6.6.crate", + "sha256": "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4", + "dest": "cargo/vendor/combine-4.6.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4\", \"files\": {}}", + "dest": "cargo/vendor/combine-4.6.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/concurrent-queue/concurrent-queue-2.3.0.crate", + "sha256": "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400", + "dest": "cargo/vendor/concurrent-queue-2.3.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400\", \"files\": {}}", + "dest": "cargo/vendor/concurrent-queue-2.3.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/const-oid/const-oid-0.9.6.crate", + "sha256": "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8", + "dest": "cargo/vendor/const-oid-0.9.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8\", \"files\": {}}", + "dest": "cargo/vendor/const-oid-0.9.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/core-foundation/core-foundation-0.9.3.crate", + "sha256": "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146", + "dest": "cargo/vendor/core-foundation-0.9.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146\", \"files\": {}}", + "dest": "cargo/vendor/core-foundation-0.9.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/core-foundation-sys/core-foundation-sys-0.8.4.crate", + "sha256": "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa", + "dest": "cargo/vendor/core-foundation-sys-0.8.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa\", \"files\": {}}", + "dest": "cargo/vendor/core-foundation-sys-0.8.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/coreaudio-rs/coreaudio-rs-0.11.3.crate", + "sha256": "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace", + "dest": "cargo/vendor/coreaudio-rs-0.11.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace\", \"files\": {}}", + "dest": "cargo/vendor/coreaudio-rs-0.11.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/coreaudio-sys/coreaudio-sys-0.2.13.crate", + "sha256": "d8478e5bdad14dce236b9898ea002eabfa87cbe14f0aa538dbe3b6a4bec4332d", + "dest": "cargo/vendor/coreaudio-sys-0.2.13" + }, + { + "type": "inline", + "contents": "{\"package\": \"d8478e5bdad14dce236b9898ea002eabfa87cbe14f0aa538dbe3b6a4bec4332d\", \"files\": {}}", + "dest": "cargo/vendor/coreaudio-sys-0.2.13", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/cpal/cpal-0.15.3.crate", + "sha256": "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779", + "dest": "cargo/vendor/cpal-0.15.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779\", \"files\": {}}", + "dest": "cargo/vendor/cpal-0.15.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/cpufeatures/cpufeatures-0.2.11.crate", + "sha256": "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0", + "dest": "cargo/vendor/cpufeatures-0.2.11" + }, + { + "type": "inline", + "contents": "{\"package\": \"ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0\", \"files\": {}}", + "dest": "cargo/vendor/cpufeatures-0.2.11", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/crossbeam-utils/crossbeam-utils-0.8.16.crate", + "sha256": "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294", + "dest": "cargo/vendor/crossbeam-utils-0.8.16" + }, + { + "type": "inline", + "contents": "{\"package\": \"5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294\", \"files\": {}}", + "dest": "cargo/vendor/crossbeam-utils-0.8.16", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/crypto-common/crypto-common-0.1.6.crate", + "sha256": "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3", + "dest": "cargo/vendor/crypto-common-0.1.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3\", \"files\": {}}", + "dest": "cargo/vendor/crypto-common-0.1.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ctr/ctr-0.9.2.crate", + "sha256": "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835", + "dest": "cargo/vendor/ctr-0.9.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835\", \"files\": {}}", + "dest": "cargo/vendor/ctr-0.9.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/curl/curl-0.4.44.crate", + "sha256": "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22", + "dest": "cargo/vendor/curl-0.4.44" + }, + { + "type": "inline", + "contents": "{\"package\": \"509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22\", \"files\": {}}", + "dest": "cargo/vendor/curl-0.4.44", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/curl-sys/curl-sys-0.4.68+curl-8.4.0.crate", + "sha256": "b4a0d18d88360e374b16b2273c832b5e57258ffc1d4aa4f96b108e0738d5752f", + "dest": "cargo/vendor/curl-sys-0.4.68+curl-8.4.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"b4a0d18d88360e374b16b2273c832b5e57258ffc1d4aa4f96b108e0738d5752f\", \"files\": {}}", + "dest": "cargo/vendor/curl-sys-0.4.68+curl-8.4.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/darling/darling-0.20.10.crate", + "sha256": "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989", + "dest": "cargo/vendor/darling-0.20.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989\", \"files\": {}}", + "dest": "cargo/vendor/darling-0.20.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/darling_core/darling_core-0.20.10.crate", + "sha256": "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5", + "dest": "cargo/vendor/darling_core-0.20.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5\", \"files\": {}}", + "dest": "cargo/vendor/darling_core-0.20.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/darling_macro/darling_macro-0.20.10.crate", + "sha256": "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806", + "dest": "cargo/vendor/darling_macro-0.20.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806\", \"files\": {}}", + "dest": "cargo/vendor/darling_macro-0.20.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/dasp_sample/dasp_sample-0.11.0.crate", + "sha256": "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f", + "dest": "cargo/vendor/dasp_sample-0.11.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f\", \"files\": {}}", + "dest": "cargo/vendor/dasp_sample-0.11.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/data-encoding/data-encoding-2.6.0.crate", + "sha256": "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2", + "dest": "cargo/vendor/data-encoding-2.6.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2\", \"files\": {}}", + "dest": "cargo/vendor/data-encoding-2.6.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/der/der-0.7.9.crate", + "sha256": "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0", + "dest": "cargo/vendor/der-0.7.9" + }, + { + "type": "inline", + "contents": "{\"package\": \"f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0\", \"files\": {}}", + "dest": "cargo/vendor/der-0.7.9", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/deranged/deranged-0.3.11.crate", + "sha256": "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4", + "dest": "cargo/vendor/deranged-0.3.11" + }, + { + "type": "inline", + "contents": "{\"package\": \"b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4\", \"files\": {}}", + "dest": "cargo/vendor/deranged-0.3.11", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/derivative/derivative-2.2.0.crate", + "sha256": "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b", + "dest": "cargo/vendor/derivative-2.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b\", \"files\": {}}", + "dest": "cargo/vendor/derivative-2.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/derive_builder/derive_builder-0.20.2.crate", + "sha256": "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947", + "dest": "cargo/vendor/derive_builder-0.20.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947\", \"files\": {}}", + "dest": "cargo/vendor/derive_builder-0.20.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/derive_builder_core/derive_builder_core-0.20.2.crate", + "sha256": "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8", + "dest": "cargo/vendor/derive_builder_core-0.20.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8\", \"files\": {}}", + "dest": "cargo/vendor/derive_builder_core-0.20.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/derive_builder_macro/derive_builder_macro-0.20.2.crate", + "sha256": "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c", + "dest": "cargo/vendor/derive_builder_macro-0.20.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c\", \"files\": {}}", + "dest": "cargo/vendor/derive_builder_macro-0.20.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/digest/digest-0.10.7.crate", + "sha256": "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292", + "dest": "cargo/vendor/digest-0.10.7" + }, + { + "type": "inline", + "contents": "{\"package\": \"9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292\", \"files\": {}}", + "dest": "cargo/vendor/digest-0.10.7", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/dunce/dunce-1.0.5.crate", + "sha256": "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813", + "dest": "cargo/vendor/dunce-1.0.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813\", \"files\": {}}", + "dest": "cargo/vendor/dunce-1.0.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/either/either-1.13.0.crate", + "sha256": "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0", + "dest": "cargo/vendor/either-1.13.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0\", \"files\": {}}", + "dest": "cargo/vendor/either-1.13.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/encoding_rs/encoding_rs-0.8.33.crate", + "sha256": "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1", + "dest": "cargo/vendor/encoding_rs-0.8.33" + }, + { + "type": "inline", + "contents": "{\"package\": \"7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1\", \"files\": {}}", + "dest": "cargo/vendor/encoding_rs-0.8.33", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/enumflags2/enumflags2-0.7.8.crate", + "sha256": "5998b4f30320c9d93aed72f63af821bfdac50465b75428fce77b48ec482c3939", + "dest": "cargo/vendor/enumflags2-0.7.8" + }, + { + "type": "inline", + "contents": "{\"package\": \"5998b4f30320c9d93aed72f63af821bfdac50465b75428fce77b48ec482c3939\", \"files\": {}}", + "dest": "cargo/vendor/enumflags2-0.7.8", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/enumflags2_derive/enumflags2_derive-0.7.8.crate", + "sha256": "f95e2801cd355d4a1a3e3953ce6ee5ae9603a5c833455343a8bfe3f44d418246", + "dest": "cargo/vendor/enumflags2_derive-0.7.8" + }, + { + "type": "inline", + "contents": "{\"package\": \"f95e2801cd355d4a1a3e3953ce6ee5ae9603a5c833455343a8bfe3f44d418246\", \"files\": {}}", + "dest": "cargo/vendor/enumflags2_derive-0.7.8", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/env_filter/env_filter-0.1.2.crate", + "sha256": "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab", + "dest": "cargo/vendor/env_filter-0.1.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab\", \"files\": {}}", + "dest": "cargo/vendor/env_filter-0.1.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/env_logger/env_logger-0.10.0.crate", + "sha256": "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0", + "dest": "cargo/vendor/env_logger-0.10.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0\", \"files\": {}}", + "dest": "cargo/vendor/env_logger-0.10.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/env_logger/env_logger-0.11.5.crate", + "sha256": "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d", + "dest": "cargo/vendor/env_logger-0.11.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d\", \"files\": {}}", + "dest": "cargo/vendor/env_logger-0.11.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/equivalent/equivalent-1.0.1.crate", + "sha256": "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5", + "dest": "cargo/vendor/equivalent-1.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5\", \"files\": {}}", + "dest": "cargo/vendor/equivalent-1.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/errno/errno-0.3.5.crate", + "sha256": "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860", + "dest": "cargo/vendor/errno-0.3.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860\", \"files\": {}}", + "dest": "cargo/vendor/errno-0.3.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/event-listener/event-listener-2.5.3.crate", + "sha256": "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0", + "dest": "cargo/vendor/event-listener-2.5.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0\", \"files\": {}}", + "dest": "cargo/vendor/event-listener-2.5.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/event-listener/event-listener-3.0.1.crate", + "sha256": "01cec0252c2afff729ee6f00e903d479fba81784c8e2bd77447673471fdfaea1", + "dest": "cargo/vendor/event-listener-3.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"01cec0252c2afff729ee6f00e903d479fba81784c8e2bd77447673471fdfaea1\", \"files\": {}}", + "dest": "cargo/vendor/event-listener-3.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/fastrand/fastrand-1.9.0.crate", + "sha256": "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be", + "dest": "cargo/vendor/fastrand-1.9.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be\", \"files\": {}}", + "dest": "cargo/vendor/fastrand-1.9.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/fastrand/fastrand-2.0.1.crate", + "sha256": "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5", + "dest": "cargo/vendor/fastrand-2.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5\", \"files\": {}}", + "dest": "cargo/vendor/fastrand-2.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/field-offset/field-offset-0.3.6.crate", + "sha256": "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f", + "dest": "cargo/vendor/field-offset-0.3.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f\", \"files\": {}}", + "dest": "cargo/vendor/field-offset-0.3.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/fixedbitset/fixedbitset-0.4.2.crate", + "sha256": "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80", + "dest": "cargo/vendor/fixedbitset-0.4.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80\", \"files\": {}}", + "dest": "cargo/vendor/fixedbitset-0.4.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/fnv/fnv-1.0.7.crate", + "sha256": "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1", + "dest": "cargo/vendor/fnv-1.0.7" + }, + { + "type": "inline", + "contents": "{\"package\": \"3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1\", \"files\": {}}", + "dest": "cargo/vendor/fnv-1.0.7", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/form_urlencoded/form_urlencoded-1.2.0.crate", + "sha256": "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652", + "dest": "cargo/vendor/form_urlencoded-1.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652\", \"files\": {}}", + "dest": "cargo/vendor/form_urlencoded-1.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/fs_extra/fs_extra-1.3.0.crate", + "sha256": "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c", + "dest": "cargo/vendor/fs_extra-1.3.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c\", \"files\": {}}", + "dest": "cargo/vendor/fs_extra-1.3.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/futures/futures-0.3.29.crate", + "sha256": "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335", + "dest": "cargo/vendor/futures-0.3.29" + }, + { + "type": "inline", + "contents": "{\"package\": \"da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335\", \"files\": {}}", + "dest": "cargo/vendor/futures-0.3.29", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/futures-channel/futures-channel-0.3.31.crate", + "sha256": "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10", + "dest": "cargo/vendor/futures-channel-0.3.31" + }, + { + "type": "inline", + "contents": "{\"package\": \"2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10\", \"files\": {}}", + "dest": "cargo/vendor/futures-channel-0.3.31", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/futures-core/futures-core-0.3.31.crate", + "sha256": "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e", + "dest": "cargo/vendor/futures-core-0.3.31" + }, + { + "type": "inline", + "contents": "{\"package\": \"05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e\", \"files\": {}}", + "dest": "cargo/vendor/futures-core-0.3.31", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/futures-executor/futures-executor-0.3.29.crate", + "sha256": "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc", + "dest": "cargo/vendor/futures-executor-0.3.29" + }, + { + "type": "inline", + "contents": "{\"package\": \"0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc\", \"files\": {}}", + "dest": "cargo/vendor/futures-executor-0.3.29", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/futures-io/futures-io-0.3.31.crate", + "sha256": "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6", + "dest": "cargo/vendor/futures-io-0.3.31" + }, + { + "type": "inline", + "contents": "{\"package\": \"9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6\", \"files\": {}}", + "dest": "cargo/vendor/futures-io-0.3.31", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/futures-lite/futures-lite-1.13.0.crate", + "sha256": "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce", + "dest": "cargo/vendor/futures-lite-1.13.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce\", \"files\": {}}", + "dest": "cargo/vendor/futures-lite-1.13.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/futures-macro/futures-macro-0.3.31.crate", + "sha256": "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650", + "dest": "cargo/vendor/futures-macro-0.3.31" + }, + { + "type": "inline", + "contents": "{\"package\": \"162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650\", \"files\": {}}", + "dest": "cargo/vendor/futures-macro-0.3.31", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/futures-sink/futures-sink-0.3.31.crate", + "sha256": "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7", + "dest": "cargo/vendor/futures-sink-0.3.31" + }, + { + "type": "inline", + "contents": "{\"package\": \"e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7\", \"files\": {}}", + "dest": "cargo/vendor/futures-sink-0.3.31", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/futures-task/futures-task-0.3.31.crate", + "sha256": "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988", + "dest": "cargo/vendor/futures-task-0.3.31" + }, + { + "type": "inline", + "contents": "{\"package\": \"f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988\", \"files\": {}}", + "dest": "cargo/vendor/futures-task-0.3.31", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/futures-timer/futures-timer-3.0.3.crate", + "sha256": "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24", + "dest": "cargo/vendor/futures-timer-3.0.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24\", \"files\": {}}", + "dest": "cargo/vendor/futures-timer-3.0.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/futures-util/futures-util-0.3.31.crate", + "sha256": "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81", + "dest": "cargo/vendor/futures-util-0.3.31" + }, + { + "type": "inline", + "contents": "{\"package\": \"9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81\", \"files\": {}}", + "dest": "cargo/vendor/futures-util-0.3.31", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gdk-pixbuf/gdk-pixbuf-0.17.10.crate", + "sha256": "695d6bc846438c5708b07007537b9274d883373dd30858ca881d7d71b5540717", + "dest": "cargo/vendor/gdk-pixbuf-0.17.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"695d6bc846438c5708b07007537b9274d883373dd30858ca881d7d71b5540717\", \"files\": {}}", + "dest": "cargo/vendor/gdk-pixbuf-0.17.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gdk-pixbuf-sys/gdk-pixbuf-sys-0.17.10.crate", + "sha256": "9285ec3c113c66d7d0ab5676599176f1f42f4944ca1b581852215bf5694870cb", + "dest": "cargo/vendor/gdk-pixbuf-sys-0.17.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"9285ec3c113c66d7d0ab5676599176f1f42f4944ca1b581852215bf5694870cb\", \"files\": {}}", + "dest": "cargo/vendor/gdk-pixbuf-sys-0.17.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gdk4/gdk4-0.6.3.crate", + "sha256": "c3abf96408a26e3eddf881a7f893a1e111767137136e347745e8ea6ed12731ff", + "dest": "cargo/vendor/gdk4-0.6.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"c3abf96408a26e3eddf881a7f893a1e111767137136e347745e8ea6ed12731ff\", \"files\": {}}", + "dest": "cargo/vendor/gdk4-0.6.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gdk4-sys/gdk4-sys-0.6.3.crate", + "sha256": "1bc92aa1608c089c49393d014c38ac0390d01e4841e1fedaa75dbcef77aaed64", + "dest": "cargo/vendor/gdk4-sys-0.6.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"1bc92aa1608c089c49393d014c38ac0390d01e4841e1fedaa75dbcef77aaed64\", \"files\": {}}", + "dest": "cargo/vendor/gdk4-sys-0.6.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/generic-array/generic-array-0.14.7.crate", + "sha256": "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a", + "dest": "cargo/vendor/generic-array-0.14.7" + }, + { + "type": "inline", + "contents": "{\"package\": \"85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a\", \"files\": {}}", + "dest": "cargo/vendor/generic-array-0.14.7", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/getopts/getopts-0.2.21.crate", + "sha256": "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5", + "dest": "cargo/vendor/getopts-0.2.21" + }, + { + "type": "inline", + "contents": "{\"package\": \"14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5\", \"files\": {}}", + "dest": "cargo/vendor/getopts-0.2.21", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/getrandom/getrandom-0.2.10.crate", + "sha256": "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427", + "dest": "cargo/vendor/getrandom-0.2.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427\", \"files\": {}}", + "dest": "cargo/vendor/getrandom-0.2.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gettext-rs/gettext-rs-0.7.0.crate", + "sha256": "e49ea8a8fad198aaa1f9655a2524b64b70eb06b2f3ff37da407566c93054f364", + "dest": "cargo/vendor/gettext-rs-0.7.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"e49ea8a8fad198aaa1f9655a2524b64b70eb06b2f3ff37da407566c93054f364\", \"files\": {}}", + "dest": "cargo/vendor/gettext-rs-0.7.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gettext-sys/gettext-sys-0.21.3.crate", + "sha256": "c63ce2e00f56a206778276704bbe38564c8695249fdc8f354b4ef71c57c3839d", + "dest": "cargo/vendor/gettext-sys-0.21.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"c63ce2e00f56a206778276704bbe38564c8695249fdc8f354b4ef71c57c3839d\", \"files\": {}}", + "dest": "cargo/vendor/gettext-sys-0.21.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gimli/gimli-0.28.0.crate", + "sha256": "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0", + "dest": "cargo/vendor/gimli-0.28.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0\", \"files\": {}}", + "dest": "cargo/vendor/gimli-0.28.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gio/gio-0.17.10.crate", + "sha256": "a6973e92937cf98689b6a054a9e56c657ed4ff76de925e36fc331a15f0c5d30a", + "dest": "cargo/vendor/gio-0.17.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"a6973e92937cf98689b6a054a9e56c657ed4ff76de925e36fc331a15f0c5d30a\", \"files\": {}}", + "dest": "cargo/vendor/gio-0.17.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gio-sys/gio-sys-0.17.10.crate", + "sha256": "0ccf87c30a12c469b6d958950f6a9c09f2be20b7773f7e70d20b867fdf2628c3", + "dest": "cargo/vendor/gio-sys-0.17.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"0ccf87c30a12c469b6d958950f6a9c09f2be20b7773f7e70d20b867fdf2628c3\", \"files\": {}}", + "dest": "cargo/vendor/gio-sys-0.17.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gio-sys/gio-sys-0.20.4.crate", + "sha256": "4f7efc368de04755344f0084104835b6bb71df2c1d41e37d863947392a894779", + "dest": "cargo/vendor/gio-sys-0.20.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"4f7efc368de04755344f0084104835b6bb71df2c1d41e37d863947392a894779\", \"files\": {}}", + "dest": "cargo/vendor/gio-sys-0.20.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/glib/glib-0.17.10.crate", + "sha256": "d3fad45ba8d4d2cea612b432717e834f48031cd8853c8aaf43b2c79fec8d144b", + "dest": "cargo/vendor/glib-0.17.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"d3fad45ba8d4d2cea612b432717e834f48031cd8853c8aaf43b2c79fec8d144b\", \"files\": {}}", + "dest": "cargo/vendor/glib-0.17.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/glib/glib-0.20.4.crate", + "sha256": "adcf1ec6d3650bf9fdbc6cee242d4fcebc6f6bfd9bea5b929b6a8b7344eb85ff", + "dest": "cargo/vendor/glib-0.20.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"adcf1ec6d3650bf9fdbc6cee242d4fcebc6f6bfd9bea5b929b6a8b7344eb85ff\", \"files\": {}}", + "dest": "cargo/vendor/glib-0.20.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/glib-macros/glib-macros-0.17.10.crate", + "sha256": "eca5c79337338391f1ab8058d6698125034ce8ef31b72a442437fa6c8580de26", + "dest": "cargo/vendor/glib-macros-0.17.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"eca5c79337338391f1ab8058d6698125034ce8ef31b72a442437fa6c8580de26\", \"files\": {}}", + "dest": "cargo/vendor/glib-macros-0.17.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/glib-macros/glib-macros-0.20.4.crate", + "sha256": "a6bf88f70cd5720a6197639dcabcb378dd528d0cb68cb1f45e3b358bcb841cd7", + "dest": "cargo/vendor/glib-macros-0.20.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"a6bf88f70cd5720a6197639dcabcb378dd528d0cb68cb1f45e3b358bcb841cd7\", \"files\": {}}", + "dest": "cargo/vendor/glib-macros-0.20.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/glib-sys/glib-sys-0.17.10.crate", + "sha256": "d80aa6ea7bba0baac79222204aa786a6293078c210abe69ef1336911d4bdc4f0", + "dest": "cargo/vendor/glib-sys-0.17.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"d80aa6ea7bba0baac79222204aa786a6293078c210abe69ef1336911d4bdc4f0\", \"files\": {}}", + "dest": "cargo/vendor/glib-sys-0.17.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/glib-sys/glib-sys-0.20.4.crate", + "sha256": "5f9eca5d88cfa6a453b00d203287c34a2b7cac3a7831779aa2bb0b3c7233752b", + "dest": "cargo/vendor/glib-sys-0.20.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"5f9eca5d88cfa6a453b00d203287c34a2b7cac3a7831779aa2bb0b3c7233752b\", \"files\": {}}", + "dest": "cargo/vendor/glib-sys-0.20.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/glob/glob-0.3.1.crate", + "sha256": "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b", + "dest": "cargo/vendor/glob-0.3.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b\", \"files\": {}}", + "dest": "cargo/vendor/glob-0.3.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gloo-timers/gloo-timers-0.2.6.crate", + "sha256": "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c", + "dest": "cargo/vendor/gloo-timers-0.2.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c\", \"files\": {}}", + "dest": "cargo/vendor/gloo-timers-0.2.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gobject-sys/gobject-sys-0.17.10.crate", + "sha256": "cd34c3317740a6358ec04572c1bcfd3ac0b5b6529275fae255b237b314bb8062", + "dest": "cargo/vendor/gobject-sys-0.17.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"cd34c3317740a6358ec04572c1bcfd3ac0b5b6529275fae255b237b314bb8062\", \"files\": {}}", + "dest": "cargo/vendor/gobject-sys-0.17.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gobject-sys/gobject-sys-0.20.4.crate", + "sha256": "a4c674d2ff8478cf0ec29d2be730ed779fef54415a2fb4b565c52def62696462", + "dest": "cargo/vendor/gobject-sys-0.20.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"a4c674d2ff8478cf0ec29d2be730ed779fef54415a2fb4b565c52def62696462\", \"files\": {}}", + "dest": "cargo/vendor/gobject-sys-0.20.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/governor/governor-0.6.3.crate", + "sha256": "68a7f542ee6b35af73b06abc0dad1c1bae89964e4e253bc4b587b91c9637867b", + "dest": "cargo/vendor/governor-0.6.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"68a7f542ee6b35af73b06abc0dad1c1bae89964e4e253bc4b587b91c9637867b\", \"files\": {}}", + "dest": "cargo/vendor/governor-0.6.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/graphene-rs/graphene-rs-0.17.10.crate", + "sha256": "def4bb01265b59ed548b05455040d272d989b3012c42d4c1bbd39083cb9b40d9", + "dest": "cargo/vendor/graphene-rs-0.17.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"def4bb01265b59ed548b05455040d272d989b3012c42d4c1bbd39083cb9b40d9\", \"files\": {}}", + "dest": "cargo/vendor/graphene-rs-0.17.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/graphene-sys/graphene-sys-0.17.10.crate", + "sha256": "1856fc817e6a6675e36cea0bd9a3afe296f5d9709d1e2d3182803ac77f0ab21d", + "dest": "cargo/vendor/graphene-sys-0.17.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"1856fc817e6a6675e36cea0bd9a3afe296f5d9709d1e2d3182803ac77f0ab21d\", \"files\": {}}", + "dest": "cargo/vendor/graphene-sys-0.17.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gsk4/gsk4-0.6.3.crate", + "sha256": "6f01ef44fa7cac15e2da9978529383e6bee03e570ba5bf7036b4c10a15cc3a3c", + "dest": "cargo/vendor/gsk4-0.6.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"6f01ef44fa7cac15e2da9978529383e6bee03e570ba5bf7036b4c10a15cc3a3c\", \"files\": {}}", + "dest": "cargo/vendor/gsk4-0.6.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gsk4-sys/gsk4-sys-0.6.3.crate", + "sha256": "c07a84fb4dcf1323d29435aa85e2f5f58bef564342bef06775ec7bd0da1f01b0", + "dest": "cargo/vendor/gsk4-sys-0.6.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"c07a84fb4dcf1323d29435aa85e2f5f58bef564342bef06775ec7bd0da1f01b0\", \"files\": {}}", + "dest": "cargo/vendor/gsk4-sys-0.6.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gstreamer/gstreamer-0.23.2.crate", + "sha256": "49ecf3bcfc2ceb82ce02437f53ff2fcaee5e7d45ae697ab64a018408749779b9", + "dest": "cargo/vendor/gstreamer-0.23.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"49ecf3bcfc2ceb82ce02437f53ff2fcaee5e7d45ae697ab64a018408749779b9\", \"files\": {}}", + "dest": "cargo/vendor/gstreamer-0.23.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gstreamer-app/gstreamer-app-0.23.2.crate", + "sha256": "54a4ec9f0d2037349c82f589c1cbfe788a62f4941851924bb7c3929a6a790007", + "dest": "cargo/vendor/gstreamer-app-0.23.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"54a4ec9f0d2037349c82f589c1cbfe788a62f4941851924bb7c3929a6a790007\", \"files\": {}}", + "dest": "cargo/vendor/gstreamer-app-0.23.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gstreamer-app-sys/gstreamer-app-sys-0.23.2.crate", + "sha256": "08d5cac633c1ab7030c777c8c58c682a0c763bbc4127bccc370dabe39c01a12d", + "dest": "cargo/vendor/gstreamer-app-sys-0.23.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"08d5cac633c1ab7030c777c8c58c682a0c763bbc4127bccc370dabe39c01a12d\", \"files\": {}}", + "dest": "cargo/vendor/gstreamer-app-sys-0.23.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gstreamer-audio/gstreamer-audio-0.23.2.crate", + "sha256": "36d39b07213f83055fc705a384fa32ad581776b8e5b04c86f3a419ec5dfc0f81", + "dest": "cargo/vendor/gstreamer-audio-0.23.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"36d39b07213f83055fc705a384fa32ad581776b8e5b04c86f3a419ec5dfc0f81\", \"files\": {}}", + "dest": "cargo/vendor/gstreamer-audio-0.23.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gstreamer-audio-sys/gstreamer-audio-sys-0.23.2.crate", + "sha256": "d84744e7ac8f8bc0cf76b7be40f2d5be12e6cf197e4c6ca9d3438109c21e2f51", + "dest": "cargo/vendor/gstreamer-audio-sys-0.23.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"d84744e7ac8f8bc0cf76b7be40f2d5be12e6cf197e4c6ca9d3438109c21e2f51\", \"files\": {}}", + "dest": "cargo/vendor/gstreamer-audio-sys-0.23.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gstreamer-base/gstreamer-base-0.23.2.crate", + "sha256": "46ce7330d2995138a77192ea20961422ddee1578e1a47480acb820c43ceb0e2d", + "dest": "cargo/vendor/gstreamer-base-0.23.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"46ce7330d2995138a77192ea20961422ddee1578e1a47480acb820c43ceb0e2d\", \"files\": {}}", + "dest": "cargo/vendor/gstreamer-base-0.23.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gstreamer-base-sys/gstreamer-base-sys-0.23.2.crate", + "sha256": "7796e694c21c215447811c9cff694dce1fc6e02b0bbafb75cd8583b6aefe9e5f", + "dest": "cargo/vendor/gstreamer-base-sys-0.23.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"7796e694c21c215447811c9cff694dce1fc6e02b0bbafb75cd8583b6aefe9e5f\", \"files\": {}}", + "dest": "cargo/vendor/gstreamer-base-sys-0.23.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gstreamer-sys/gstreamer-sys-0.23.2.crate", + "sha256": "cb3859929db32f26a35818d0d9ed82f0887c9221ca402ddefaea2bb99833d535", + "dest": "cargo/vendor/gstreamer-sys-0.23.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"cb3859929db32f26a35818d0d9ed82f0887c9221ca402ddefaea2bb99833d535\", \"files\": {}}", + "dest": "cargo/vendor/gstreamer-sys-0.23.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gtk4/gtk4-0.6.6.crate", + "sha256": "b28a32a04cd75cef14a0983f8b0c669e0fe152a0a7725accdeb594e2c764c88b", + "dest": "cargo/vendor/gtk4-0.6.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"b28a32a04cd75cef14a0983f8b0c669e0fe152a0a7725accdeb594e2c764c88b\", \"files\": {}}", + "dest": "cargo/vendor/gtk4-0.6.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gtk4-macros/gtk4-macros-0.6.6.crate", + "sha256": "6a4d6b61570f76d3ee542d984da443b1cd69b6105264c61afec3abed08c2500f", + "dest": "cargo/vendor/gtk4-macros-0.6.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"6a4d6b61570f76d3ee542d984da443b1cd69b6105264c61afec3abed08c2500f\", \"files\": {}}", + "dest": "cargo/vendor/gtk4-macros-0.6.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/gtk4-sys/gtk4-sys-0.6.3.crate", + "sha256": "5f8283f707b07e019e76c7f2934bdd4180c277e08aa93f4c0d8dd07b7a34e22f", + "dest": "cargo/vendor/gtk4-sys-0.6.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"5f8283f707b07e019e76c7f2934bdd4180c277e08aa93f4c0d8dd07b7a34e22f\", \"files\": {}}", + "dest": "cargo/vendor/gtk4-sys-0.6.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/h2/h2-0.3.26.crate", + "sha256": "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8", + "dest": "cargo/vendor/h2-0.3.26" + }, + { + "type": "inline", + "contents": "{\"package\": \"81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8\", \"files\": {}}", + "dest": "cargo/vendor/h2-0.3.26", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/h2/h2-0.4.6.crate", + "sha256": "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205", + "dest": "cargo/vendor/h2-0.4.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205\", \"files\": {}}", + "dest": "cargo/vendor/h2-0.4.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/hashbrown/hashbrown-0.15.0.crate", + "sha256": "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb", + "dest": "cargo/vendor/hashbrown-0.15.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb\", \"files\": {}}", + "dest": "cargo/vendor/hashbrown-0.15.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/headers/headers-0.4.0.crate", + "sha256": "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9", + "dest": "cargo/vendor/headers-0.4.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9\", \"files\": {}}", + "dest": "cargo/vendor/headers-0.4.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/headers-core/headers-core-0.3.0.crate", + "sha256": "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4", + "dest": "cargo/vendor/headers-core-0.3.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4\", \"files\": {}}", + "dest": "cargo/vendor/headers-core-0.3.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/heck/heck-0.4.1.crate", + "sha256": "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8", + "dest": "cargo/vendor/heck-0.4.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8\", \"files\": {}}", + "dest": "cargo/vendor/heck-0.4.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/heck/heck-0.5.0.crate", + "sha256": "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea", + "dest": "cargo/vendor/heck-0.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea\", \"files\": {}}", + "dest": "cargo/vendor/heck-0.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/hermit-abi/hermit-abi-0.3.9.crate", + "sha256": "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024", + "dest": "cargo/vendor/hermit-abi-0.3.9" + }, + { + "type": "inline", + "contents": "{\"package\": \"d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024\", \"files\": {}}", + "dest": "cargo/vendor/hermit-abi-0.3.9", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/hex/hex-0.4.3.crate", + "sha256": "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70", + "dest": "cargo/vendor/hex-0.4.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70\", \"files\": {}}", + "dest": "cargo/vendor/hex-0.4.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/hkdf/hkdf-0.12.3.crate", + "sha256": "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437", + "dest": "cargo/vendor/hkdf-0.12.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437\", \"files\": {}}", + "dest": "cargo/vendor/hkdf-0.12.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/hmac/hmac-0.12.1.crate", + "sha256": "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e", + "dest": "cargo/vendor/hmac-0.12.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e\", \"files\": {}}", + "dest": "cargo/vendor/hmac-0.12.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/home/home-0.5.9.crate", + "sha256": "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5", + "dest": "cargo/vendor/home-0.5.9" + }, + { + "type": "inline", + "contents": "{\"package\": \"e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5\", \"files\": {}}", + "dest": "cargo/vendor/home-0.5.9", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/hostname/hostname-0.4.0.crate", + "sha256": "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba", + "dest": "cargo/vendor/hostname-0.4.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba\", \"files\": {}}", + "dest": "cargo/vendor/hostname-0.4.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/http/http-0.2.9.crate", + "sha256": "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482", + "dest": "cargo/vendor/http-0.2.9" + }, + { + "type": "inline", + "contents": "{\"package\": \"bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482\", \"files\": {}}", + "dest": "cargo/vendor/http-0.2.9", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/http/http-1.1.0.crate", + "sha256": "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258", + "dest": "cargo/vendor/http-1.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258\", \"files\": {}}", + "dest": "cargo/vendor/http-1.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/http-body/http-body-0.4.5.crate", + "sha256": "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1", + "dest": "cargo/vendor/http-body-0.4.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1\", \"files\": {}}", + "dest": "cargo/vendor/http-body-0.4.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/http-body/http-body-1.0.1.crate", + "sha256": "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184", + "dest": "cargo/vendor/http-body-1.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184\", \"files\": {}}", + "dest": "cargo/vendor/http-body-1.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/http-body-util/http-body-util-0.1.2.crate", + "sha256": "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f", + "dest": "cargo/vendor/http-body-util-0.1.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f\", \"files\": {}}", + "dest": "cargo/vendor/http-body-util-0.1.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/httparse/httparse-1.8.0.crate", + "sha256": "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904", + "dest": "cargo/vendor/httparse-1.8.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904\", \"files\": {}}", + "dest": "cargo/vendor/httparse-1.8.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/httpdate/httpdate-1.0.3.crate", + "sha256": "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9", + "dest": "cargo/vendor/httpdate-1.0.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9\", \"files\": {}}", + "dest": "cargo/vendor/httpdate-1.0.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/humantime/humantime-2.1.0.crate", + "sha256": "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4", + "dest": "cargo/vendor/humantime-2.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4\", \"files\": {}}", + "dest": "cargo/vendor/humantime-2.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/hyper/hyper-0.14.27.crate", + "sha256": "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468", + "dest": "cargo/vendor/hyper-0.14.27" + }, + { + "type": "inline", + "contents": "{\"package\": \"ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468\", \"files\": {}}", + "dest": "cargo/vendor/hyper-0.14.27", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/hyper/hyper-1.5.0.crate", + "sha256": "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a", + "dest": "cargo/vendor/hyper-1.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a\", \"files\": {}}", + "dest": "cargo/vendor/hyper-1.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/hyper-proxy2/hyper-proxy2-0.1.0.crate", + "sha256": "9043b7b23fb0bc4a1c7014c27b50a4fc42cc76206f71d34fc0dfe5b28ddc3faf", + "dest": "cargo/vendor/hyper-proxy2-0.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"9043b7b23fb0bc4a1c7014c27b50a4fc42cc76206f71d34fc0dfe5b28ddc3faf\", \"files\": {}}", + "dest": "cargo/vendor/hyper-proxy2-0.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/hyper-rustls/hyper-rustls-0.24.2.crate", + "sha256": "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590", + "dest": "cargo/vendor/hyper-rustls-0.24.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590\", \"files\": {}}", + "dest": "cargo/vendor/hyper-rustls-0.24.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/hyper-rustls/hyper-rustls-0.26.0.crate", + "sha256": "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c", + "dest": "cargo/vendor/hyper-rustls-0.26.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c\", \"files\": {}}", + "dest": "cargo/vendor/hyper-rustls-0.26.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/hyper-rustls/hyper-rustls-0.27.3.crate", + "sha256": "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333", + "dest": "cargo/vendor/hyper-rustls-0.27.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333\", \"files\": {}}", + "dest": "cargo/vendor/hyper-rustls-0.27.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/hyper-util/hyper-util-0.1.7.crate", + "sha256": "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9", + "dest": "cargo/vendor/hyper-util-0.1.7" + }, + { + "type": "inline", + "contents": "{\"package\": \"cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9\", \"files\": {}}", + "dest": "cargo/vendor/hyper-util-0.1.7", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/iana-time-zone/iana-time-zone-0.1.58.crate", + "sha256": "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20", + "dest": "cargo/vendor/iana-time-zone-0.1.58" + }, + { + "type": "inline", + "contents": "{\"package\": \"8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20\", \"files\": {}}", + "dest": "cargo/vendor/iana-time-zone-0.1.58", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/iana-time-zone-haiku/iana-time-zone-haiku-0.1.2.crate", + "sha256": "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f", + "dest": "cargo/vendor/iana-time-zone-haiku-0.1.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f\", \"files\": {}}", + "dest": "cargo/vendor/iana-time-zone-haiku-0.1.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ident_case/ident_case-1.0.1.crate", + "sha256": "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39", + "dest": "cargo/vendor/ident_case-1.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39\", \"files\": {}}", + "dest": "cargo/vendor/ident_case-1.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/idna/idna-0.4.0.crate", + "sha256": "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c", + "dest": "cargo/vendor/idna-0.4.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c\", \"files\": {}}", + "dest": "cargo/vendor/idna-0.4.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/if-addrs/if-addrs-0.12.0.crate", + "sha256": "bb2a33e9c38988ecbda730c85b0fd9ddcdf83c0305ac7fd21c8bb9f57f2f0cc8", + "dest": "cargo/vendor/if-addrs-0.12.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"bb2a33e9c38988ecbda730c85b0fd9ddcdf83c0305ac7fd21c8bb9f57f2f0cc8\", \"files\": {}}", + "dest": "cargo/vendor/if-addrs-0.12.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/indexmap/indexmap-2.6.0.crate", + "sha256": "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da", + "dest": "cargo/vendor/indexmap-2.6.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da\", \"files\": {}}", + "dest": "cargo/vendor/indexmap-2.6.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/inout/inout-0.1.3.crate", + "sha256": "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5", + "dest": "cargo/vendor/inout-0.1.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5\", \"files\": {}}", + "dest": "cargo/vendor/inout-0.1.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/instant/instant-0.1.12.crate", + "sha256": "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c", + "dest": "cargo/vendor/instant-0.1.12" + }, + { + "type": "inline", + "contents": "{\"package\": \"7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c\", \"files\": {}}", + "dest": "cargo/vendor/instant-0.1.12", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/io-lifetimes/io-lifetimes-1.0.11.crate", + "sha256": "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2", + "dest": "cargo/vendor/io-lifetimes-1.0.11" + }, + { + "type": "inline", + "contents": "{\"package\": \"eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2\", \"files\": {}}", + "dest": "cargo/vendor/io-lifetimes-1.0.11", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ipnet/ipnet-2.10.1.crate", + "sha256": "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708", + "dest": "cargo/vendor/ipnet-2.10.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708\", \"files\": {}}", + "dest": "cargo/vendor/ipnet-2.10.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/is-docker/is-docker-0.2.0.crate", + "sha256": "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3", + "dest": "cargo/vendor/is-docker-0.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3\", \"files\": {}}", + "dest": "cargo/vendor/is-docker-0.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/is-terminal/is-terminal-0.4.9.crate", + "sha256": "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b", + "dest": "cargo/vendor/is-terminal-0.4.9" + }, + { + "type": "inline", + "contents": "{\"package\": \"cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b\", \"files\": {}}", + "dest": "cargo/vendor/is-terminal-0.4.9", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/is-wsl/is-wsl-0.4.0.crate", + "sha256": "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5", + "dest": "cargo/vendor/is-wsl-0.4.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5\", \"files\": {}}", + "dest": "cargo/vendor/is-wsl-0.4.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/is_terminal_polyfill/is_terminal_polyfill-1.70.1.crate", + "sha256": "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf", + "dest": "cargo/vendor/is_terminal_polyfill-1.70.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf\", \"files\": {}}", + "dest": "cargo/vendor/is_terminal_polyfill-1.70.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/isahc/isahc-1.7.2.crate", + "sha256": "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9", + "dest": "cargo/vendor/isahc-1.7.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9\", \"files\": {}}", + "dest": "cargo/vendor/isahc-1.7.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/itertools/itertools-0.12.1.crate", + "sha256": "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569", + "dest": "cargo/vendor/itertools-0.12.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569\", \"files\": {}}", + "dest": "cargo/vendor/itertools-0.12.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/itertools/itertools-0.13.0.crate", + "sha256": "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186", + "dest": "cargo/vendor/itertools-0.13.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186\", \"files\": {}}", + "dest": "cargo/vendor/itertools-0.13.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/itoa/itoa-1.0.9.crate", + "sha256": "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38", + "dest": "cargo/vendor/itoa-1.0.9" + }, + { + "type": "inline", + "contents": "{\"package\": \"af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38\", \"files\": {}}", + "dest": "cargo/vendor/itoa-1.0.9", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/jni/jni-0.21.1.crate", + "sha256": "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97", + "dest": "cargo/vendor/jni-0.21.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97\", \"files\": {}}", + "dest": "cargo/vendor/jni-0.21.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/jni-sys/jni-sys-0.3.0.crate", + "sha256": "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130", + "dest": "cargo/vendor/jni-sys-0.3.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130\", \"files\": {}}", + "dest": "cargo/vendor/jni-sys-0.3.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/jobserver/jobserver-0.1.32.crate", + "sha256": "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0", + "dest": "cargo/vendor/jobserver-0.1.32" + }, + { + "type": "inline", + "contents": "{\"package\": \"48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0\", \"files\": {}}", + "dest": "cargo/vendor/jobserver-0.1.32", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/js-sys/js-sys-0.3.65.crate", + "sha256": "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8", + "dest": "cargo/vendor/js-sys-0.3.65" + }, + { + "type": "inline", + "contents": "{\"package\": \"54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8\", \"files\": {}}", + "dest": "cargo/vendor/js-sys-0.3.65", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/kv-log-macro/kv-log-macro-1.0.7.crate", + "sha256": "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f", + "dest": "cargo/vendor/kv-log-macro-1.0.7" + }, + { + "type": "inline", + "contents": "{\"package\": \"0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f\", \"files\": {}}", + "dest": "cargo/vendor/kv-log-macro-1.0.7", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/lazy_static/lazy_static-1.4.0.crate", + "sha256": "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646", + "dest": "cargo/vendor/lazy_static-1.4.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646\", \"files\": {}}", + "dest": "cargo/vendor/lazy_static-1.4.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/lazycell/lazycell-1.3.0.crate", + "sha256": "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55", + "dest": "cargo/vendor/lazycell-1.3.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55\", \"files\": {}}", + "dest": "cargo/vendor/lazycell-1.3.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/libadwaita/libadwaita-0.4.4.crate", + "sha256": "1ab9c0843f9f23ff25634df2743690c3a1faffe0a190e60c490878517eb81abf", + "dest": "cargo/vendor/libadwaita-0.4.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"1ab9c0843f9f23ff25634df2743690c3a1faffe0a190e60c490878517eb81abf\", \"files\": {}}", + "dest": "cargo/vendor/libadwaita-0.4.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/libadwaita-sys/libadwaita-sys-0.4.4.crate", + "sha256": "4231cb2499a9f0c4cdfa4885414b33e39901ddcac61150bc0bb4ff8a57ede404", + "dest": "cargo/vendor/libadwaita-sys-0.4.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"4231cb2499a9f0c4cdfa4885414b33e39901ddcac61150bc0bb4ff8a57ede404\", \"files\": {}}", + "dest": "cargo/vendor/libadwaita-sys-0.4.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/libc/libc-0.2.161.crate", + "sha256": "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1", + "dest": "cargo/vendor/libc-0.2.161" + }, + { + "type": "inline", + "contents": "{\"package\": \"8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1\", \"files\": {}}", + "dest": "cargo/vendor/libc-0.2.161", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/libloading/libloading-0.7.4.crate", + "sha256": "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f", + "dest": "cargo/vendor/libloading-0.7.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f\", \"files\": {}}", + "dest": "cargo/vendor/libloading-0.7.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/libm/libm-0.2.8.crate", + "sha256": "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058", + "dest": "cargo/vendor/libm-0.2.8" + }, + { + "type": "inline", + "contents": "{\"package\": \"4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058\", \"files\": {}}", + "dest": "cargo/vendor/libm-0.2.8", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/libmdns/libmdns-0.9.1.crate", + "sha256": "48854699e11b111433431b69cee2365fcab0b29b06993f48c257dfbaf6395862", + "dest": "cargo/vendor/libmdns-0.9.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"48854699e11b111433431b69cee2365fcab0b29b06993f48c257dfbaf6395862\", \"files\": {}}", + "dest": "cargo/vendor/libmdns-0.9.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/libnghttp2-sys/libnghttp2-sys-0.1.8+1.55.1.crate", + "sha256": "4fae956c192dadcdb5dace96db71fa0b827333cce7c7b38dc71446f024d8a340", + "dest": "cargo/vendor/libnghttp2-sys-0.1.8+1.55.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"4fae956c192dadcdb5dace96db71fa0b827333cce7c7b38dc71446f024d8a340\", \"files\": {}}", + "dest": "cargo/vendor/libnghttp2-sys-0.1.8+1.55.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/libpulse-binding/libpulse-binding-2.28.1.crate", + "sha256": "ed3557a2dfc380c8f061189a01c6ae7348354e0c9886038dc6c171219c08eaff", + "dest": "cargo/vendor/libpulse-binding-2.28.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"ed3557a2dfc380c8f061189a01c6ae7348354e0c9886038dc6c171219c08eaff\", \"files\": {}}", + "dest": "cargo/vendor/libpulse-binding-2.28.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/libpulse-simple-binding/libpulse-simple-binding-2.28.1.crate", + "sha256": "05fd6b68f33f6a251265e6ed1212dc3107caad7c5c6fdcd847b2e65ef58c308d", + "dest": "cargo/vendor/libpulse-simple-binding-2.28.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"05fd6b68f33f6a251265e6ed1212dc3107caad7c5c6fdcd847b2e65ef58c308d\", \"files\": {}}", + "dest": "cargo/vendor/libpulse-simple-binding-2.28.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/libpulse-simple-sys/libpulse-simple-sys-1.21.1.crate", + "sha256": "ea6613b4199d8b9f0edcfb623e020cb17bbd0bee8dd21f3c7cc938de561c4152", + "dest": "cargo/vendor/libpulse-simple-sys-1.21.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"ea6613b4199d8b9f0edcfb623e020cb17bbd0bee8dd21f3c7cc938de561c4152\", \"files\": {}}", + "dest": "cargo/vendor/libpulse-simple-sys-1.21.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/libpulse-sys/libpulse-sys-1.21.0.crate", + "sha256": "bc19e110fbf42c17260d30f6d3dc545f58491c7830d38ecb9aaca96e26067a9b", + "dest": "cargo/vendor/libpulse-sys-1.21.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"bc19e110fbf42c17260d30f6d3dc545f58491c7830d38ecb9aaca96e26067a9b\", \"files\": {}}", + "dest": "cargo/vendor/libpulse-sys-1.21.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/librespot/librespot-0.5.0.crate", + "sha256": "4ea500bb5673fbf4cc9dcbb7afdc228bf0cdb434cc3a0be79afa6bb385a8b5f8", + "dest": "cargo/vendor/librespot-0.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"4ea500bb5673fbf4cc9dcbb7afdc228bf0cdb434cc3a0be79afa6bb385a8b5f8\", \"files\": {}}", + "dest": "cargo/vendor/librespot-0.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/librespot-audio/librespot-audio-0.5.0.crate", + "sha256": "5fbda070a5598b32718e497f585f46891f7113e64aff20a13c0f2ba8fe7ccad9", + "dest": "cargo/vendor/librespot-audio-0.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"5fbda070a5598b32718e497f585f46891f7113e64aff20a13c0f2ba8fe7ccad9\", \"files\": {}}", + "dest": "cargo/vendor/librespot-audio-0.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/librespot-connect/librespot-connect-0.5.0.crate", + "sha256": "699745eaeff6f53b67e93fe973eb72c818cba58d10cfdbdd4c1bf7893f9b705b", + "dest": "cargo/vendor/librespot-connect-0.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"699745eaeff6f53b67e93fe973eb72c818cba58d10cfdbdd4c1bf7893f9b705b\", \"files\": {}}", + "dest": "cargo/vendor/librespot-connect-0.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/librespot-core/librespot-core-0.5.0.crate", + "sha256": "505a5ddd966231755994b60435607a1e8ae1d41c7f1169b078e0511bfb82d931", + "dest": "cargo/vendor/librespot-core-0.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"505a5ddd966231755994b60435607a1e8ae1d41c7f1169b078e0511bfb82d931\", \"files\": {}}", + "dest": "cargo/vendor/librespot-core-0.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/librespot-discovery/librespot-discovery-0.5.0.crate", + "sha256": "abca1d9420179c0875189cee50b1cd75ad91c6180f38d079eae3a62a40b4745f", + "dest": "cargo/vendor/librespot-discovery-0.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"abca1d9420179c0875189cee50b1cd75ad91c6180f38d079eae3a62a40b4745f\", \"files\": {}}", + "dest": "cargo/vendor/librespot-discovery-0.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/librespot-metadata/librespot-metadata-0.5.0.crate", + "sha256": "6a10ab5a390f65281e763cd09c617b173f0e665994eae3d242526924625fdc66", + "dest": "cargo/vendor/librespot-metadata-0.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"6a10ab5a390f65281e763cd09c617b173f0e665994eae3d242526924625fdc66\", \"files\": {}}", + "dest": "cargo/vendor/librespot-metadata-0.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/librespot-oauth/librespot-oauth-0.5.0.crate", + "sha256": "57bda94233b358fb41c04ed15507c61136c80efe876c6e05a10ddb9a182b144e", + "dest": "cargo/vendor/librespot-oauth-0.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"57bda94233b358fb41c04ed15507c61136c80efe876c6e05a10ddb9a182b144e\", \"files\": {}}", + "dest": "cargo/vendor/librespot-oauth-0.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/librespot-playback/librespot-playback-0.5.0.crate", + "sha256": "5b1bcfe1d72c5ac14c798c7e3e1c20e1fb6af2b9c254794545cfcb1f2a4627e2", + "dest": "cargo/vendor/librespot-playback-0.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"5b1bcfe1d72c5ac14c798c7e3e1c20e1fb6af2b9c254794545cfcb1f2a4627e2\", \"files\": {}}", + "dest": "cargo/vendor/librespot-playback-0.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/librespot-protocol/librespot-protocol-0.5.0.crate", + "sha256": "0d6f343f573e0469d3ff8a02b99bbd9789faa01e2ff167332542ac840a8b31e7", + "dest": "cargo/vendor/librespot-protocol-0.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"0d6f343f573e0469d3ff8a02b99bbd9789faa01e2ff167332542ac840a8b31e7\", \"files\": {}}", + "dest": "cargo/vendor/librespot-protocol-0.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/libz-sys/libz-sys-1.1.12.crate", + "sha256": "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b", + "dest": "cargo/vendor/libz-sys-1.1.12" + }, + { + "type": "inline", + "contents": "{\"package\": \"d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b\", \"files\": {}}", + "dest": "cargo/vendor/libz-sys-1.1.12", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/linux-raw-sys/linux-raw-sys-0.3.8.crate", + "sha256": "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519", + "dest": "cargo/vendor/linux-raw-sys-0.3.8" + }, + { + "type": "inline", + "contents": "{\"package\": \"ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519\", \"files\": {}}", + "dest": "cargo/vendor/linux-raw-sys-0.3.8", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/linux-raw-sys/linux-raw-sys-0.4.10.crate", + "sha256": "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f", + "dest": "cargo/vendor/linux-raw-sys-0.4.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f\", \"files\": {}}", + "dest": "cargo/vendor/linux-raw-sys-0.4.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/locale_config/locale_config-0.3.0.crate", + "sha256": "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934", + "dest": "cargo/vendor/locale_config-0.3.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934\", \"files\": {}}", + "dest": "cargo/vendor/locale_config-0.3.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/lock_api/lock_api-0.4.11.crate", + "sha256": "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45", + "dest": "cargo/vendor/lock_api-0.4.11" + }, + { + "type": "inline", + "contents": "{\"package\": \"3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45\", \"files\": {}}", + "dest": "cargo/vendor/lock_api-0.4.11", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/log/log-0.4.22.crate", + "sha256": "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24", + "dest": "cargo/vendor/log-0.4.22" + }, + { + "type": "inline", + "contents": "{\"package\": \"a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24\", \"files\": {}}", + "dest": "cargo/vendor/log-0.4.22", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/mach2/mach2-0.4.2.crate", + "sha256": "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709", + "dest": "cargo/vendor/mach2-0.4.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709\", \"files\": {}}", + "dest": "cargo/vendor/mach2-0.4.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/malloc_buf/malloc_buf-0.0.6.crate", + "sha256": "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb", + "dest": "cargo/vendor/malloc_buf-0.0.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb\", \"files\": {}}", + "dest": "cargo/vendor/malloc_buf-0.0.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/memchr/memchr-2.7.4.crate", + "sha256": "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3", + "dest": "cargo/vendor/memchr-2.7.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3\", \"files\": {}}", + "dest": "cargo/vendor/memchr-2.7.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/memoffset/memoffset-0.7.1.crate", + "sha256": "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4", + "dest": "cargo/vendor/memoffset-0.7.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4\", \"files\": {}}", + "dest": "cargo/vendor/memoffset-0.7.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/memoffset/memoffset-0.9.0.crate", + "sha256": "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c", + "dest": "cargo/vendor/memoffset-0.9.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c\", \"files\": {}}", + "dest": "cargo/vendor/memoffset-0.9.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/mime/mime-0.3.17.crate", + "sha256": "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a", + "dest": "cargo/vendor/mime-0.3.17" + }, + { + "type": "inline", + "contents": "{\"package\": \"6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a\", \"files\": {}}", + "dest": "cargo/vendor/mime-0.3.17", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/minimal-lexical/minimal-lexical-0.2.1.crate", + "sha256": "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a", + "dest": "cargo/vendor/minimal-lexical-0.2.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a\", \"files\": {}}", + "dest": "cargo/vendor/minimal-lexical-0.2.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/miniz_oxide/miniz_oxide-0.7.1.crate", + "sha256": "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7", + "dest": "cargo/vendor/miniz_oxide-0.7.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7\", \"files\": {}}", + "dest": "cargo/vendor/miniz_oxide-0.7.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/mio/mio-1.0.2.crate", + "sha256": "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec", + "dest": "cargo/vendor/mio-1.0.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec\", \"files\": {}}", + "dest": "cargo/vendor/mio-1.0.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/mirai-annotations/mirai-annotations-1.12.0.crate", + "sha256": "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1", + "dest": "cargo/vendor/mirai-annotations-1.12.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1\", \"files\": {}}", + "dest": "cargo/vendor/mirai-annotations-1.12.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/muldiv/muldiv-1.0.1.crate", + "sha256": "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0", + "dest": "cargo/vendor/muldiv-1.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0\", \"files\": {}}", + "dest": "cargo/vendor/muldiv-1.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/multimap/multimap-0.10.0.crate", + "sha256": "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03", + "dest": "cargo/vendor/multimap-0.10.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03\", \"files\": {}}", + "dest": "cargo/vendor/multimap-0.10.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ndk/ndk-0.8.0.crate", + "sha256": "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7", + "dest": "cargo/vendor/ndk-0.8.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7\", \"files\": {}}", + "dest": "cargo/vendor/ndk-0.8.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ndk-context/ndk-context-0.1.1.crate", + "sha256": "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b", + "dest": "cargo/vendor/ndk-context-0.1.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b\", \"files\": {}}", + "dest": "cargo/vendor/ndk-context-0.1.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ndk-sys/ndk-sys-0.5.0+25.2.9519653.crate", + "sha256": "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691", + "dest": "cargo/vendor/ndk-sys-0.5.0+25.2.9519653" + }, + { + "type": "inline", + "contents": "{\"package\": \"8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691\", \"files\": {}}", + "dest": "cargo/vendor/ndk-sys-0.5.0+25.2.9519653", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/nix/nix-0.26.4.crate", + "sha256": "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b", + "dest": "cargo/vendor/nix-0.26.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b\", \"files\": {}}", + "dest": "cargo/vendor/nix-0.26.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/no-std-compat/no-std-compat-0.4.1.crate", + "sha256": "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c", + "dest": "cargo/vendor/no-std-compat-0.4.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c\", \"files\": {}}", + "dest": "cargo/vendor/no-std-compat-0.4.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/nom/nom-7.1.3.crate", + "sha256": "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a", + "dest": "cargo/vendor/nom-7.1.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a\", \"files\": {}}", + "dest": "cargo/vendor/nom-7.1.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/nonzero_ext/nonzero_ext-0.3.0.crate", + "sha256": "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21", + "dest": "cargo/vendor/nonzero_ext-0.3.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21\", \"files\": {}}", + "dest": "cargo/vendor/nonzero_ext-0.3.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ntapi/ntapi-0.4.1.crate", + "sha256": "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4", + "dest": "cargo/vendor/ntapi-0.4.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4\", \"files\": {}}", + "dest": "cargo/vendor/ntapi-0.4.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num/num-0.4.1.crate", + "sha256": "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af", + "dest": "cargo/vendor/num-0.4.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af\", \"files\": {}}", + "dest": "cargo/vendor/num-0.4.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num-bigint/num-bigint-0.4.4.crate", + "sha256": "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0", + "dest": "cargo/vendor/num-bigint-0.4.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0\", \"files\": {}}", + "dest": "cargo/vendor/num-bigint-0.4.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num-bigint-dig/num-bigint-dig-0.8.4.crate", + "sha256": "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151", + "dest": "cargo/vendor/num-bigint-dig-0.8.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151\", \"files\": {}}", + "dest": "cargo/vendor/num-bigint-dig-0.8.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num-complex/num-complex-0.4.4.crate", + "sha256": "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214", + "dest": "cargo/vendor/num-complex-0.4.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214\", \"files\": {}}", + "dest": "cargo/vendor/num-complex-0.4.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num-conv/num-conv-0.1.0.crate", + "sha256": "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9", + "dest": "cargo/vendor/num-conv-0.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9\", \"files\": {}}", + "dest": "cargo/vendor/num-conv-0.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num-derive/num-derive-0.3.3.crate", + "sha256": "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d", + "dest": "cargo/vendor/num-derive-0.3.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d\", \"files\": {}}", + "dest": "cargo/vendor/num-derive-0.3.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num-derive/num-derive-0.4.2.crate", + "sha256": "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202", + "dest": "cargo/vendor/num-derive-0.4.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202\", \"files\": {}}", + "dest": "cargo/vendor/num-derive-0.4.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num-integer/num-integer-0.1.45.crate", + "sha256": "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9", + "dest": "cargo/vendor/num-integer-0.1.45" + }, + { + "type": "inline", + "contents": "{\"package\": \"225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9\", \"files\": {}}", + "dest": "cargo/vendor/num-integer-0.1.45", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num-iter/num-iter-0.1.43.crate", + "sha256": "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252", + "dest": "cargo/vendor/num-iter-0.1.43" + }, + { + "type": "inline", + "contents": "{\"package\": \"7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252\", \"files\": {}}", + "dest": "cargo/vendor/num-iter-0.1.43", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num-rational/num-rational-0.4.1.crate", + "sha256": "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0", + "dest": "cargo/vendor/num-rational-0.4.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0\", \"files\": {}}", + "dest": "cargo/vendor/num-rational-0.4.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num-traits/num-traits-0.2.17.crate", + "sha256": "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c", + "dest": "cargo/vendor/num-traits-0.2.17" + }, + { + "type": "inline", + "contents": "{\"package\": \"39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c\", \"files\": {}}", + "dest": "cargo/vendor/num-traits-0.2.17", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num_enum/num_enum-0.7.3.crate", + "sha256": "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179", + "dest": "cargo/vendor/num_enum-0.7.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179\", \"files\": {}}", + "dest": "cargo/vendor/num_enum-0.7.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num_enum_derive/num_enum_derive-0.7.3.crate", + "sha256": "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56", + "dest": "cargo/vendor/num_enum_derive-0.7.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56\", \"files\": {}}", + "dest": "cargo/vendor/num_enum_derive-0.7.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/num_threads/num_threads-0.1.7.crate", + "sha256": "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9", + "dest": "cargo/vendor/num_threads-0.1.7" + }, + { + "type": "inline", + "contents": "{\"package\": \"5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9\", \"files\": {}}", + "dest": "cargo/vendor/num_threads-0.1.7", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/oauth2/oauth2-4.4.2.crate", + "sha256": "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f", + "dest": "cargo/vendor/oauth2-4.4.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f\", \"files\": {}}", + "dest": "cargo/vendor/oauth2-4.4.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc/objc-0.2.7.crate", + "sha256": "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1", + "dest": "cargo/vendor/objc-0.2.7" + }, + { + "type": "inline", + "contents": "{\"package\": \"915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1\", \"files\": {}}", + "dest": "cargo/vendor/objc-0.2.7", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc-foundation/objc-foundation-0.1.1.crate", + "sha256": "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9", + "dest": "cargo/vendor/objc-foundation-0.1.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9\", \"files\": {}}", + "dest": "cargo/vendor/objc-foundation-0.1.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/objc_id/objc_id-0.1.1.crate", + "sha256": "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b", + "dest": "cargo/vendor/objc_id-0.1.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b\", \"files\": {}}", + "dest": "cargo/vendor/objc_id-0.1.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/object/object-0.32.1.crate", + "sha256": "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0", + "dest": "cargo/vendor/object-0.32.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0\", \"files\": {}}", + "dest": "cargo/vendor/object-0.32.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/oboe/oboe-0.6.1.crate", + "sha256": "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb", + "dest": "cargo/vendor/oboe-0.6.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb\", \"files\": {}}", + "dest": "cargo/vendor/oboe-0.6.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/oboe-sys/oboe-sys-0.6.1.crate", + "sha256": "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d", + "dest": "cargo/vendor/oboe-sys-0.6.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d\", \"files\": {}}", + "dest": "cargo/vendor/oboe-sys-0.6.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/once_cell/once_cell-1.18.0.crate", + "sha256": "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d", + "dest": "cargo/vendor/once_cell-1.18.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d\", \"files\": {}}", + "dest": "cargo/vendor/once_cell-1.18.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/opaque-debug/opaque-debug-0.3.0.crate", + "sha256": "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5", + "dest": "cargo/vendor/opaque-debug-0.3.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5\", \"files\": {}}", + "dest": "cargo/vendor/opaque-debug-0.3.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/open/open-5.3.0.crate", + "sha256": "61a877bf6abd716642a53ef1b89fb498923a4afca5c754f9050b4d081c05c4b3", + "dest": "cargo/vendor/open-5.3.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"61a877bf6abd716642a53ef1b89fb498923a4afca5c754f9050b4d081c05c4b3\", \"files\": {}}", + "dest": "cargo/vendor/open-5.3.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/openssl-probe/openssl-probe-0.1.5.crate", + "sha256": "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf", + "dest": "cargo/vendor/openssl-probe-0.1.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf\", \"files\": {}}", + "dest": "cargo/vendor/openssl-probe-0.1.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/openssl-sys/openssl-sys-0.9.94.crate", + "sha256": "2f55da20b29f956fb01f0add8683eb26ee13ebe3ebd935e49898717c6b4b2830", + "dest": "cargo/vendor/openssl-sys-0.9.94" + }, + { + "type": "inline", + "contents": "{\"package\": \"2f55da20b29f956fb01f0add8683eb26ee13ebe3ebd935e49898717c6b4b2830\", \"files\": {}}", + "dest": "cargo/vendor/openssl-sys-0.9.94", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/option-operations/option-operations-0.5.0.crate", + "sha256": "7c26d27bb1aeab65138e4bf7666045169d1717febcc9ff870166be8348b223d0", + "dest": "cargo/vendor/option-operations-0.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"7c26d27bb1aeab65138e4bf7666045169d1717febcc9ff870166be8348b223d0\", \"files\": {}}", + "dest": "cargo/vendor/option-operations-0.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ordered-stream/ordered-stream-0.2.0.crate", + "sha256": "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50", + "dest": "cargo/vendor/ordered-stream-0.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50\", \"files\": {}}", + "dest": "cargo/vendor/ordered-stream-0.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pango/pango-0.17.10.crate", + "sha256": "35be456fc620e61f62dff7ff70fbd54dcbaf0a4b920c0f16de1107c47d921d48", + "dest": "cargo/vendor/pango-0.17.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"35be456fc620e61f62dff7ff70fbd54dcbaf0a4b920c0f16de1107c47d921d48\", \"files\": {}}", + "dest": "cargo/vendor/pango-0.17.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pango-sys/pango-sys-0.17.10.crate", + "sha256": "3da69f9f3850b0d8990d462f8c709561975e95f689c1cdf0fecdebde78b35195", + "dest": "cargo/vendor/pango-sys-0.17.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"3da69f9f3850b0d8990d462f8c709561975e95f689c1cdf0fecdebde78b35195\", \"files\": {}}", + "dest": "cargo/vendor/pango-sys-0.17.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/parking/parking-2.2.0.crate", + "sha256": "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae", + "dest": "cargo/vendor/parking-2.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae\", \"files\": {}}", + "dest": "cargo/vendor/parking-2.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/parking_lot/parking_lot-0.12.1.crate", + "sha256": "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f", + "dest": "cargo/vendor/parking_lot-0.12.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f\", \"files\": {}}", + "dest": "cargo/vendor/parking_lot-0.12.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/parking_lot_core/parking_lot_core-0.9.9.crate", + "sha256": "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e", + "dest": "cargo/vendor/parking_lot_core-0.9.9" + }, + { + "type": "inline", + "contents": "{\"package\": \"4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e\", \"files\": {}}", + "dest": "cargo/vendor/parking_lot_core-0.9.9", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/paste/paste-1.0.14.crate", + "sha256": "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c", + "dest": "cargo/vendor/paste-1.0.14" + }, + { + "type": "inline", + "contents": "{\"package\": \"de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c\", \"files\": {}}", + "dest": "cargo/vendor/paste-1.0.14", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pathdiff/pathdiff-0.2.2.crate", + "sha256": "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361", + "dest": "cargo/vendor/pathdiff-0.2.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361\", \"files\": {}}", + "dest": "cargo/vendor/pathdiff-0.2.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pbkdf2/pbkdf2-0.12.2.crate", + "sha256": "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2", + "dest": "cargo/vendor/pbkdf2-0.12.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2\", \"files\": {}}", + "dest": "cargo/vendor/pbkdf2-0.12.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/peeking_take_while/peeking_take_while-0.1.2.crate", + "sha256": "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099", + "dest": "cargo/vendor/peeking_take_while-0.1.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099\", \"files\": {}}", + "dest": "cargo/vendor/peeking_take_while-0.1.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pem-rfc7468/pem-rfc7468-0.7.0.crate", + "sha256": "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412", + "dest": "cargo/vendor/pem-rfc7468-0.7.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412\", \"files\": {}}", + "dest": "cargo/vendor/pem-rfc7468-0.7.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/percent-encoding/percent-encoding-2.3.0.crate", + "sha256": "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94", + "dest": "cargo/vendor/percent-encoding-2.3.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94\", \"files\": {}}", + "dest": "cargo/vendor/percent-encoding-2.3.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/petgraph/petgraph-0.6.4.crate", + "sha256": "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9", + "dest": "cargo/vendor/petgraph-0.6.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9\", \"files\": {}}", + "dest": "cargo/vendor/petgraph-0.6.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pin-project/pin-project-1.1.3.crate", + "sha256": "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422", + "dest": "cargo/vendor/pin-project-1.1.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422\", \"files\": {}}", + "dest": "cargo/vendor/pin-project-1.1.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pin-project-internal/pin-project-internal-1.1.3.crate", + "sha256": "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405", + "dest": "cargo/vendor/pin-project-internal-1.1.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405\", \"files\": {}}", + "dest": "cargo/vendor/pin-project-internal-1.1.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pin-project-lite/pin-project-lite-0.2.13.crate", + "sha256": "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58", + "dest": "cargo/vendor/pin-project-lite-0.2.13" + }, + { + "type": "inline", + "contents": "{\"package\": \"8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58\", \"files\": {}}", + "dest": "cargo/vendor/pin-project-lite-0.2.13", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pin-utils/pin-utils-0.1.0.crate", + "sha256": "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184", + "dest": "cargo/vendor/pin-utils-0.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184\", \"files\": {}}", + "dest": "cargo/vendor/pin-utils-0.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/piper/piper-0.2.1.crate", + "sha256": "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4", + "dest": "cargo/vendor/piper-0.2.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4\", \"files\": {}}", + "dest": "cargo/vendor/piper-0.2.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pkcs1/pkcs1-0.7.5.crate", + "sha256": "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f", + "dest": "cargo/vendor/pkcs1-0.7.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f\", \"files\": {}}", + "dest": "cargo/vendor/pkcs1-0.7.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pkcs8/pkcs8-0.10.2.crate", + "sha256": "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7", + "dest": "cargo/vendor/pkcs8-0.10.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7\", \"files\": {}}", + "dest": "cargo/vendor/pkcs8-0.10.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/pkg-config/pkg-config-0.3.27.crate", + "sha256": "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964", + "dest": "cargo/vendor/pkg-config-0.3.27" + }, + { + "type": "inline", + "contents": "{\"package\": \"26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964\", \"files\": {}}", + "dest": "cargo/vendor/pkg-config-0.3.27", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/polling/polling-2.8.0.crate", + "sha256": "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce", + "dest": "cargo/vendor/polling-2.8.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce\", \"files\": {}}", + "dest": "cargo/vendor/polling-2.8.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/polling/polling-3.3.0.crate", + "sha256": "e53b6af1f60f36f8c2ac2aad5459d75a5a9b4be1e8cdd40264f315d78193e531", + "dest": "cargo/vendor/polling-3.3.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"e53b6af1f60f36f8c2ac2aad5459d75a5a9b4be1e8cdd40264f315d78193e531\", \"files\": {}}", + "dest": "cargo/vendor/polling-3.3.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/portable-atomic/portable-atomic-1.9.0.crate", + "sha256": "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2", + "dest": "cargo/vendor/portable-atomic-1.9.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2\", \"files\": {}}", + "dest": "cargo/vendor/portable-atomic-1.9.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/powerfmt/powerfmt-0.2.0.crate", + "sha256": "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391", + "dest": "cargo/vendor/powerfmt-0.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391\", \"files\": {}}", + "dest": "cargo/vendor/powerfmt-0.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ppv-lite86/ppv-lite86-0.2.17.crate", + "sha256": "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de", + "dest": "cargo/vendor/ppv-lite86-0.2.17" + }, + { + "type": "inline", + "contents": "{\"package\": \"5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de\", \"files\": {}}", + "dest": "cargo/vendor/ppv-lite86-0.2.17", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/prettyplease/prettyplease-0.2.25.crate", + "sha256": "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033", + "dest": "cargo/vendor/prettyplease-0.2.25" + }, + { + "type": "inline", + "contents": "{\"package\": \"64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033\", \"files\": {}}", + "dest": "cargo/vendor/prettyplease-0.2.25", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/priority-queue/priority-queue-2.1.1.crate", + "sha256": "714c75db297bc88a63783ffc6ab9f830698a6705aa0201416931759ef4c8183d", + "dest": "cargo/vendor/priority-queue-2.1.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"714c75db297bc88a63783ffc6ab9f830698a6705aa0201416931759ef4c8183d\", \"files\": {}}", + "dest": "cargo/vendor/priority-queue-2.1.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/proc-macro-crate/proc-macro-crate-1.3.1.crate", + "sha256": "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919", + "dest": "cargo/vendor/proc-macro-crate-1.3.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919\", \"files\": {}}", + "dest": "cargo/vendor/proc-macro-crate-1.3.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/proc-macro-crate/proc-macro-crate-3.2.0.crate", + "sha256": "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b", + "dest": "cargo/vendor/proc-macro-crate-3.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b\", \"files\": {}}", + "dest": "cargo/vendor/proc-macro-crate-3.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/proc-macro-error/proc-macro-error-1.0.4.crate", + "sha256": "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c", + "dest": "cargo/vendor/proc-macro-error-1.0.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c\", \"files\": {}}", + "dest": "cargo/vendor/proc-macro-error-1.0.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/proc-macro-error-attr/proc-macro-error-attr-1.0.4.crate", + "sha256": "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869", + "dest": "cargo/vendor/proc-macro-error-attr-1.0.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869\", \"files\": {}}", + "dest": "cargo/vendor/proc-macro-error-attr-1.0.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/proc-macro2/proc-macro2-1.0.89.crate", + "sha256": "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e", + "dest": "cargo/vendor/proc-macro2-1.0.89" + }, + { + "type": "inline", + "contents": "{\"package\": \"f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e\", \"files\": {}}", + "dest": "cargo/vendor/proc-macro2-1.0.89", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/protobuf/protobuf-3.7.1.crate", + "sha256": "a3a7c64d9bf75b1b8d981124c14c179074e8caa7dfe7b6a12e6222ddcd0c8f72", + "dest": "cargo/vendor/protobuf-3.7.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"a3a7c64d9bf75b1b8d981124c14c179074e8caa7dfe7b6a12e6222ddcd0c8f72\", \"files\": {}}", + "dest": "cargo/vendor/protobuf-3.7.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/protobuf-codegen/protobuf-codegen-3.7.1.crate", + "sha256": "e26b833f144769a30e04b1db0146b2aaa53fd2fd83acf10a6b5f996606c18144", + "dest": "cargo/vendor/protobuf-codegen-3.7.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"e26b833f144769a30e04b1db0146b2aaa53fd2fd83acf10a6b5f996606c18144\", \"files\": {}}", + "dest": "cargo/vendor/protobuf-codegen-3.7.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/protobuf-parse/protobuf-parse-3.7.1.crate", + "sha256": "322330e133eab455718444b4e033ebfac7c6528972c784fcde28d2cc783c6257", + "dest": "cargo/vendor/protobuf-parse-3.7.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"322330e133eab455718444b4e033ebfac7c6528972c784fcde28d2cc783c6257\", \"files\": {}}", + "dest": "cargo/vendor/protobuf-parse-3.7.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/protobuf-support/protobuf-support-3.7.1.crate", + "sha256": "b088fd20b938a875ea00843b6faf48579462630015c3788d397ad6a786663252", + "dest": "cargo/vendor/protobuf-support-3.7.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"b088fd20b938a875ea00843b6faf48579462630015c3788d397ad6a786663252\", \"files\": {}}", + "dest": "cargo/vendor/protobuf-support-3.7.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/quick-xml/quick-xml-0.36.2.crate", + "sha256": "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe", + "dest": "cargo/vendor/quick-xml-0.36.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe\", \"files\": {}}", + "dest": "cargo/vendor/quick-xml-0.36.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/quote/quote-1.0.37.crate", + "sha256": "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af", + "dest": "cargo/vendor/quote-1.0.37" + }, + { + "type": "inline", + "contents": "{\"package\": \"b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af\", \"files\": {}}", + "dest": "cargo/vendor/quote-1.0.37", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rand/rand-0.8.5.crate", + "sha256": "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404", + "dest": "cargo/vendor/rand-0.8.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404\", \"files\": {}}", + "dest": "cargo/vendor/rand-0.8.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rand_chacha/rand_chacha-0.3.1.crate", + "sha256": "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88", + "dest": "cargo/vendor/rand_chacha-0.3.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88\", \"files\": {}}", + "dest": "cargo/vendor/rand_chacha-0.3.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rand_core/rand_core-0.6.4.crate", + "sha256": "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c", + "dest": "cargo/vendor/rand_core-0.6.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c\", \"files\": {}}", + "dest": "cargo/vendor/rand_core-0.6.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rand_distr/rand_distr-0.4.3.crate", + "sha256": "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31", + "dest": "cargo/vendor/rand_distr-0.4.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31\", \"files\": {}}", + "dest": "cargo/vendor/rand_distr-0.4.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/redox_syscall/redox_syscall-0.4.1.crate", + "sha256": "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa", + "dest": "cargo/vendor/redox_syscall-0.4.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa\", \"files\": {}}", + "dest": "cargo/vendor/redox_syscall-0.4.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ref_filter_map/ref_filter_map-1.0.1.crate", + "sha256": "2b5ceb840e4009da4841ed22a15eb49f64fdd00a2138945c5beacf506b2fb5ed", + "dest": "cargo/vendor/ref_filter_map-1.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"2b5ceb840e4009da4841ed22a15eb49f64fdd00a2138945c5beacf506b2fb5ed\", \"files\": {}}", + "dest": "cargo/vendor/ref_filter_map-1.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/regex/regex-1.10.2.crate", + "sha256": "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343", + "dest": "cargo/vendor/regex-1.10.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343\", \"files\": {}}", + "dest": "cargo/vendor/regex-1.10.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/regex-automata/regex-automata-0.4.3.crate", + "sha256": "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f", + "dest": "cargo/vendor/regex-automata-0.4.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f\", \"files\": {}}", + "dest": "cargo/vendor/regex-automata-0.4.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/regex-syntax/regex-syntax-0.8.2.crate", + "sha256": "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f", + "dest": "cargo/vendor/regex-syntax-0.8.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f\", \"files\": {}}", + "dest": "cargo/vendor/regex-syntax-0.8.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/reqwest/reqwest-0.11.27.crate", + "sha256": "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62", + "dest": "cargo/vendor/reqwest-0.11.27" + }, + { + "type": "inline", + "contents": "{\"package\": \"dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62\", \"files\": {}}", + "dest": "cargo/vendor/reqwest-0.11.27", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ring/ring-0.17.8.crate", + "sha256": "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d", + "dest": "cargo/vendor/ring-0.17.8" + }, + { + "type": "inline", + "contents": "{\"package\": \"c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d\", \"files\": {}}", + "dest": "cargo/vendor/ring-0.17.8", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rodio/rodio-0.19.0.crate", + "sha256": "6006a627c1a38d37f3d3a85c6575418cfe34a5392d60a686d0071e1c8d427acb", + "dest": "cargo/vendor/rodio-0.19.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"6006a627c1a38d37f3d3a85c6575418cfe34a5392d60a686d0071e1c8d427acb\", \"files\": {}}", + "dest": "cargo/vendor/rodio-0.19.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rsa/rsa-0.9.6.crate", + "sha256": "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc", + "dest": "cargo/vendor/rsa-0.9.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc\", \"files\": {}}", + "dest": "cargo/vendor/rsa-0.9.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustc-demangle/rustc-demangle-0.1.23.crate", + "sha256": "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76", + "dest": "cargo/vendor/rustc-demangle-0.1.23" + }, + { + "type": "inline", + "contents": "{\"package\": \"d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76\", \"files\": {}}", + "dest": "cargo/vendor/rustc-demangle-0.1.23", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustc-hash/rustc-hash-1.1.0.crate", + "sha256": "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2", + "dest": "cargo/vendor/rustc-hash-1.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2\", \"files\": {}}", + "dest": "cargo/vendor/rustc-hash-1.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustc_version/rustc_version-0.4.0.crate", + "sha256": "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366", + "dest": "cargo/vendor/rustc_version-0.4.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366\", \"files\": {}}", + "dest": "cargo/vendor/rustc_version-0.4.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustix/rustix-0.37.27.crate", + "sha256": "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2", + "dest": "cargo/vendor/rustix-0.37.27" + }, + { + "type": "inline", + "contents": "{\"package\": \"fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2\", \"files\": {}}", + "dest": "cargo/vendor/rustix-0.37.27", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustix/rustix-0.38.21.crate", + "sha256": "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3", + "dest": "cargo/vendor/rustix-0.38.21" + }, + { + "type": "inline", + "contents": "{\"package\": \"2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3\", \"files\": {}}", + "dest": "cargo/vendor/rustix-0.38.21", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustls/rustls-0.21.12.crate", + "sha256": "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e", + "dest": "cargo/vendor/rustls-0.21.12" + }, + { + "type": "inline", + "contents": "{\"package\": \"3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e\", \"files\": {}}", + "dest": "cargo/vendor/rustls-0.21.12", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustls/rustls-0.22.4.crate", + "sha256": "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432", + "dest": "cargo/vendor/rustls-0.22.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432\", \"files\": {}}", + "dest": "cargo/vendor/rustls-0.22.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustls/rustls-0.23.15.crate", + "sha256": "5fbb44d7acc4e873d613422379f69f237a1b141928c02f6bc6ccfddddc2d7993", + "dest": "cargo/vendor/rustls-0.23.15" + }, + { + "type": "inline", + "contents": "{\"package\": \"5fbb44d7acc4e873d613422379f69f237a1b141928c02f6bc6ccfddddc2d7993\", \"files\": {}}", + "dest": "cargo/vendor/rustls-0.23.15", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustls-native-certs/rustls-native-certs-0.7.3.crate", + "sha256": "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5", + "dest": "cargo/vendor/rustls-native-certs-0.7.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5\", \"files\": {}}", + "dest": "cargo/vendor/rustls-native-certs-0.7.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustls-native-certs/rustls-native-certs-0.8.0.crate", + "sha256": "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a", + "dest": "cargo/vendor/rustls-native-certs-0.8.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a\", \"files\": {}}", + "dest": "cargo/vendor/rustls-native-certs-0.8.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustls-pemfile/rustls-pemfile-1.0.4.crate", + "sha256": "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c", + "dest": "cargo/vendor/rustls-pemfile-1.0.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c\", \"files\": {}}", + "dest": "cargo/vendor/rustls-pemfile-1.0.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustls-pemfile/rustls-pemfile-2.2.0.crate", + "sha256": "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50", + "dest": "cargo/vendor/rustls-pemfile-2.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50\", \"files\": {}}", + "dest": "cargo/vendor/rustls-pemfile-2.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustls-pki-types/rustls-pki-types-1.10.0.crate", + "sha256": "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b", + "dest": "cargo/vendor/rustls-pki-types-1.10.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b\", \"files\": {}}", + "dest": "cargo/vendor/rustls-pki-types-1.10.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustls-webpki/rustls-webpki-0.101.7.crate", + "sha256": "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765", + "dest": "cargo/vendor/rustls-webpki-0.101.7" + }, + { + "type": "inline", + "contents": "{\"package\": \"8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765\", \"files\": {}}", + "dest": "cargo/vendor/rustls-webpki-0.101.7", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustls-webpki/rustls-webpki-0.102.8.crate", + "sha256": "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9", + "dest": "cargo/vendor/rustls-webpki-0.102.8" + }, + { + "type": "inline", + "contents": "{\"package\": \"64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9\", \"files\": {}}", + "dest": "cargo/vendor/rustls-webpki-0.102.8", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/rustversion/rustversion-1.0.18.crate", + "sha256": "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248", + "dest": "cargo/vendor/rustversion-1.0.18" + }, + { + "type": "inline", + "contents": "{\"package\": \"0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248\", \"files\": {}}", + "dest": "cargo/vendor/rustversion-1.0.18", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/ryu/ryu-1.0.15.crate", + "sha256": "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741", + "dest": "cargo/vendor/ryu-1.0.15" + }, + { + "type": "inline", + "contents": "{\"package\": \"1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741\", \"files\": {}}", + "dest": "cargo/vendor/ryu-1.0.15", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/same-file/same-file-1.0.6.crate", + "sha256": "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502", + "dest": "cargo/vendor/same-file-1.0.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502\", \"files\": {}}", + "dest": "cargo/vendor/same-file-1.0.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/schannel/schannel-0.1.22.crate", + "sha256": "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88", + "dest": "cargo/vendor/schannel-0.1.22" + }, + { + "type": "inline", + "contents": "{\"package\": \"0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88\", \"files\": {}}", + "dest": "cargo/vendor/schannel-0.1.22", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/scopeguard/scopeguard-1.2.0.crate", + "sha256": "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49", + "dest": "cargo/vendor/scopeguard-1.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49\", \"files\": {}}", + "dest": "cargo/vendor/scopeguard-1.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/sct/sct-0.7.1.crate", + "sha256": "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414", + "dest": "cargo/vendor/sct-0.7.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414\", \"files\": {}}", + "dest": "cargo/vendor/sct-0.7.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/secret-service/secret-service-3.0.1.crate", + "sha256": "5da1a5ad4d28c03536f82f77d9f36603f5e37d8869ac98f0a750d5b5686d8d95", + "dest": "cargo/vendor/secret-service-3.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"5da1a5ad4d28c03536f82f77d9f36603f5e37d8869ac98f0a750d5b5686d8d95\", \"files\": {}}", + "dest": "cargo/vendor/secret-service-3.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/security-framework/security-framework-2.10.0.crate", + "sha256": "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6", + "dest": "cargo/vendor/security-framework-2.10.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6\", \"files\": {}}", + "dest": "cargo/vendor/security-framework-2.10.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/security-framework-sys/security-framework-sys-2.11.0.crate", + "sha256": "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7", + "dest": "cargo/vendor/security-framework-sys-2.11.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7\", \"files\": {}}", + "dest": "cargo/vendor/security-framework-sys-2.11.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/semver/semver-1.0.20.crate", + "sha256": "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090", + "dest": "cargo/vendor/semver-1.0.20" + }, + { + "type": "inline", + "contents": "{\"package\": \"836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090\", \"files\": {}}", + "dest": "cargo/vendor/semver-1.0.20", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/serde/serde-1.0.213.crate", + "sha256": "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1", + "dest": "cargo/vendor/serde-1.0.213" + }, + { + "type": "inline", + "contents": "{\"package\": \"3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1\", \"files\": {}}", + "dest": "cargo/vendor/serde-1.0.213", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/serde_derive/serde_derive-1.0.213.crate", + "sha256": "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5", + "dest": "cargo/vendor/serde_derive-1.0.213" + }, + { + "type": "inline", + "contents": "{\"package\": \"7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5\", \"files\": {}}", + "dest": "cargo/vendor/serde_derive-1.0.213", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/serde_json/serde_json-1.0.108.crate", + "sha256": "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b", + "dest": "cargo/vendor/serde_json-1.0.108" + }, + { + "type": "inline", + "contents": "{\"package\": \"3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b\", \"files\": {}}", + "dest": "cargo/vendor/serde_json-1.0.108", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/serde_path_to_error/serde_path_to_error-0.1.16.crate", + "sha256": "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6", + "dest": "cargo/vendor/serde_path_to_error-0.1.16" + }, + { + "type": "inline", + "contents": "{\"package\": \"af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6\", \"files\": {}}", + "dest": "cargo/vendor/serde_path_to_error-0.1.16", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/serde_repr/serde_repr-0.1.17.crate", + "sha256": "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145", + "dest": "cargo/vendor/serde_repr-0.1.17" + }, + { + "type": "inline", + "contents": "{\"package\": \"3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145\", \"files\": {}}", + "dest": "cargo/vendor/serde_repr-0.1.17", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/serde_spanned/serde_spanned-0.6.4.crate", + "sha256": "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80", + "dest": "cargo/vendor/serde_spanned-0.6.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80\", \"files\": {}}", + "dest": "cargo/vendor/serde_spanned-0.6.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/serde_urlencoded/serde_urlencoded-0.7.1.crate", + "sha256": "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd", + "dest": "cargo/vendor/serde_urlencoded-0.7.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd\", \"files\": {}}", + "dest": "cargo/vendor/serde_urlencoded-0.7.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/sha1/sha1-0.10.6.crate", + "sha256": "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba", + "dest": "cargo/vendor/sha1-0.10.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba\", \"files\": {}}", + "dest": "cargo/vendor/sha1-0.10.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/sha2/sha2-0.10.8.crate", + "sha256": "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8", + "dest": "cargo/vendor/sha2-0.10.8" + }, + { + "type": "inline", + "contents": "{\"package\": \"793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8\", \"files\": {}}", + "dest": "cargo/vendor/sha2-0.10.8", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/shannon/shannon-0.2.0.crate", + "sha256": "7ea5b41c9427b56caa7b808cb548a04fb50bb5b9e98590b53f28064ff4174561", + "dest": "cargo/vendor/shannon-0.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"7ea5b41c9427b56caa7b808cb548a04fb50bb5b9e98590b53f28064ff4174561\", \"files\": {}}", + "dest": "cargo/vendor/shannon-0.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/shell-words/shell-words-1.1.0.crate", + "sha256": "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde", + "dest": "cargo/vendor/shell-words-1.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde\", \"files\": {}}", + "dest": "cargo/vendor/shell-words-1.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/shlex/shlex-1.2.0.crate", + "sha256": "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380", + "dest": "cargo/vendor/shlex-1.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380\", \"files\": {}}", + "dest": "cargo/vendor/shlex-1.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/signal-hook-registry/signal-hook-registry-1.4.1.crate", + "sha256": "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1", + "dest": "cargo/vendor/signal-hook-registry-1.4.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1\", \"files\": {}}", + "dest": "cargo/vendor/signal-hook-registry-1.4.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/signature/signature-2.2.0.crate", + "sha256": "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de", + "dest": "cargo/vendor/signature-2.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de\", \"files\": {}}", + "dest": "cargo/vendor/signature-2.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/slab/slab-0.4.9.crate", + "sha256": "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67", + "dest": "cargo/vendor/slab-0.4.9" + }, + { + "type": "inline", + "contents": "{\"package\": \"8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67\", \"files\": {}}", + "dest": "cargo/vendor/slab-0.4.9", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/sluice/sluice-0.5.5.crate", + "sha256": "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5", + "dest": "cargo/vendor/sluice-0.5.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5\", \"files\": {}}", + "dest": "cargo/vendor/sluice-0.5.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/smallvec/smallvec-1.13.2.crate", + "sha256": "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67", + "dest": "cargo/vendor/smallvec-1.13.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67\", \"files\": {}}", + "dest": "cargo/vendor/smallvec-1.13.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/socket2/socket2-0.4.10.crate", + "sha256": "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d", + "dest": "cargo/vendor/socket2-0.4.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d\", \"files\": {}}", + "dest": "cargo/vendor/socket2-0.4.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/socket2/socket2-0.5.5.crate", + "sha256": "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9", + "dest": "cargo/vendor/socket2-0.5.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9\", \"files\": {}}", + "dest": "cargo/vendor/socket2-0.5.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/spin/spin-0.5.2.crate", + "sha256": "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d", + "dest": "cargo/vendor/spin-0.5.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d\", \"files\": {}}", + "dest": "cargo/vendor/spin-0.5.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/spin/spin-0.9.8.crate", + "sha256": "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67", + "dest": "cargo/vendor/spin-0.9.8" + }, + { + "type": "inline", + "contents": "{\"package\": \"6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67\", \"files\": {}}", + "dest": "cargo/vendor/spin-0.9.8", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/spinning_top/spinning_top-0.3.0.crate", + "sha256": "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300", + "dest": "cargo/vendor/spinning_top-0.3.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300\", \"files\": {}}", + "dest": "cargo/vendor/spinning_top-0.3.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/spki/spki-0.7.3.crate", + "sha256": "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d", + "dest": "cargo/vendor/spki-0.7.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d\", \"files\": {}}", + "dest": "cargo/vendor/spki-0.7.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/static_assertions/static_assertions-1.1.0.crate", + "sha256": "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f", + "dest": "cargo/vendor/static_assertions-1.1.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f\", \"files\": {}}", + "dest": "cargo/vendor/static_assertions-1.1.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/strsim/strsim-0.11.1.crate", + "sha256": "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f", + "dest": "cargo/vendor/strsim-0.11.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f\", \"files\": {}}", + "dest": "cargo/vendor/strsim-0.11.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/subtle/subtle-2.6.1.crate", + "sha256": "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292", + "dest": "cargo/vendor/subtle-2.6.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292\", \"files\": {}}", + "dest": "cargo/vendor/subtle-2.6.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/symphonia/symphonia-0.5.4.crate", + "sha256": "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9", + "dest": "cargo/vendor/symphonia-0.5.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9\", \"files\": {}}", + "dest": "cargo/vendor/symphonia-0.5.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/symphonia-bundle-mp3/symphonia-bundle-mp3-0.5.4.crate", + "sha256": "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4", + "dest": "cargo/vendor/symphonia-bundle-mp3-0.5.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4\", \"files\": {}}", + "dest": "cargo/vendor/symphonia-bundle-mp3-0.5.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/symphonia-codec-vorbis/symphonia-codec-vorbis-0.5.4.crate", + "sha256": "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30", + "dest": "cargo/vendor/symphonia-codec-vorbis-0.5.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30\", \"files\": {}}", + "dest": "cargo/vendor/symphonia-codec-vorbis-0.5.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/symphonia-core/symphonia-core-0.5.4.crate", + "sha256": "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3", + "dest": "cargo/vendor/symphonia-core-0.5.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3\", \"files\": {}}", + "dest": "cargo/vendor/symphonia-core-0.5.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/symphonia-format-ogg/symphonia-format-ogg-0.5.4.crate", + "sha256": "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931", + "dest": "cargo/vendor/symphonia-format-ogg-0.5.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931\", \"files\": {}}", + "dest": "cargo/vendor/symphonia-format-ogg-0.5.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/symphonia-metadata/symphonia-metadata-0.5.4.crate", + "sha256": "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c", + "dest": "cargo/vendor/symphonia-metadata-0.5.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c\", \"files\": {}}", + "dest": "cargo/vendor/symphonia-metadata-0.5.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/symphonia-utils-xiph/symphonia-utils-xiph-0.5.4.crate", + "sha256": "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe", + "dest": "cargo/vendor/symphonia-utils-xiph-0.5.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe\", \"files\": {}}", + "dest": "cargo/vendor/symphonia-utils-xiph-0.5.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/syn/syn-1.0.109.crate", + "sha256": "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237", + "dest": "cargo/vendor/syn-1.0.109" + }, + { + "type": "inline", + "contents": "{\"package\": \"72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237\", \"files\": {}}", + "dest": "cargo/vendor/syn-1.0.109", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/syn/syn-2.0.85.crate", + "sha256": "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56", + "dest": "cargo/vendor/syn-2.0.85" + }, + { + "type": "inline", + "contents": "{\"package\": \"5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56\", \"files\": {}}", + "dest": "cargo/vendor/syn-2.0.85", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/sync_wrapper/sync_wrapper-0.1.2.crate", + "sha256": "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160", + "dest": "cargo/vendor/sync_wrapper-0.1.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160\", \"files\": {}}", + "dest": "cargo/vendor/sync_wrapper-0.1.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/sysinfo/sysinfo-0.31.4.crate", + "sha256": "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be", + "dest": "cargo/vendor/sysinfo-0.31.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be\", \"files\": {}}", + "dest": "cargo/vendor/sysinfo-0.31.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/system-configuration/system-configuration-0.5.1.crate", + "sha256": "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7", + "dest": "cargo/vendor/system-configuration-0.5.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7\", \"files\": {}}", + "dest": "cargo/vendor/system-configuration-0.5.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/system-configuration-sys/system-configuration-sys-0.5.0.crate", + "sha256": "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9", + "dest": "cargo/vendor/system-configuration-sys-0.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9\", \"files\": {}}", + "dest": "cargo/vendor/system-configuration-sys-0.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/system-deps/system-deps-6.2.0.crate", + "sha256": "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331", + "dest": "cargo/vendor/system-deps-6.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331\", \"files\": {}}", + "dest": "cargo/vendor/system-deps-6.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/system-deps/system-deps-7.0.3.crate", + "sha256": "66d23aaf9f331227789a99e8de4c91bf46703add012bdfd45fdecdfb2975a005", + "dest": "cargo/vendor/system-deps-7.0.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"66d23aaf9f331227789a99e8de4c91bf46703add012bdfd45fdecdfb2975a005\", \"files\": {}}", + "dest": "cargo/vendor/system-deps-7.0.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/target-lexicon/target-lexicon-0.12.16.crate", + "sha256": "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1", + "dest": "cargo/vendor/target-lexicon-0.12.16" + }, + { + "type": "inline", + "contents": "{\"package\": \"61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1\", \"files\": {}}", + "dest": "cargo/vendor/target-lexicon-0.12.16", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/temp-dir/temp-dir-0.1.11.crate", + "sha256": "af547b166dd1ea4b472165569fc456cfb6818116f854690b0ff205e636523dab", + "dest": "cargo/vendor/temp-dir-0.1.11" + }, + { + "type": "inline", + "contents": "{\"package\": \"af547b166dd1ea4b472165569fc456cfb6818116f854690b0ff205e636523dab\", \"files\": {}}", + "dest": "cargo/vendor/temp-dir-0.1.11", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tempfile/tempfile-3.8.1.crate", + "sha256": "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5", + "dest": "cargo/vendor/tempfile-3.8.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5\", \"files\": {}}", + "dest": "cargo/vendor/tempfile-3.8.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/termcolor/termcolor-1.3.0.crate", + "sha256": "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64", + "dest": "cargo/vendor/termcolor-1.3.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64\", \"files\": {}}", + "dest": "cargo/vendor/termcolor-1.3.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/thiserror/thiserror-1.0.50.crate", + "sha256": "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2", + "dest": "cargo/vendor/thiserror-1.0.50" + }, + { + "type": "inline", + "contents": "{\"package\": \"f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2\", \"files\": {}}", + "dest": "cargo/vendor/thiserror-1.0.50", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/thiserror-impl/thiserror-impl-1.0.50.crate", + "sha256": "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8", + "dest": "cargo/vendor/thiserror-impl-1.0.50" + }, + { + "type": "inline", + "contents": "{\"package\": \"266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8\", \"files\": {}}", + "dest": "cargo/vendor/thiserror-impl-1.0.50", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/thread-id/thread-id-4.2.1.crate", + "sha256": "f0ec81c46e9eb50deaa257be2f148adf052d1fb7701cfd55ccfab2525280b70b", + "dest": "cargo/vendor/thread-id-4.2.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"f0ec81c46e9eb50deaa257be2f148adf052d1fb7701cfd55ccfab2525280b70b\", \"files\": {}}", + "dest": "cargo/vendor/thread-id-4.2.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/time/time-0.3.36.crate", + "sha256": "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885", + "dest": "cargo/vendor/time-0.3.36" + }, + { + "type": "inline", + "contents": "{\"package\": \"5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885\", \"files\": {}}", + "dest": "cargo/vendor/time-0.3.36", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/time-core/time-core-0.1.2.crate", + "sha256": "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3", + "dest": "cargo/vendor/time-core-0.1.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3\", \"files\": {}}", + "dest": "cargo/vendor/time-core-0.1.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/time-macros/time-macros-0.2.18.crate", + "sha256": "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf", + "dest": "cargo/vendor/time-macros-0.2.18" + }, + { + "type": "inline", + "contents": "{\"package\": \"3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf\", \"files\": {}}", + "dest": "cargo/vendor/time-macros-0.2.18", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tinyvec/tinyvec-1.6.0.crate", + "sha256": "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50", + "dest": "cargo/vendor/tinyvec-1.6.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50\", \"files\": {}}", + "dest": "cargo/vendor/tinyvec-1.6.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tinyvec_macros/tinyvec_macros-0.1.1.crate", + "sha256": "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20", + "dest": "cargo/vendor/tinyvec_macros-0.1.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20\", \"files\": {}}", + "dest": "cargo/vendor/tinyvec_macros-0.1.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tokio/tokio-1.41.0.crate", + "sha256": "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb", + "dest": "cargo/vendor/tokio-1.41.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb\", \"files\": {}}", + "dest": "cargo/vendor/tokio-1.41.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tokio-macros/tokio-macros-2.4.0.crate", + "sha256": "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752", + "dest": "cargo/vendor/tokio-macros-2.4.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752\", \"files\": {}}", + "dest": "cargo/vendor/tokio-macros-2.4.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tokio-rustls/tokio-rustls-0.24.1.crate", + "sha256": "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081", + "dest": "cargo/vendor/tokio-rustls-0.24.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081\", \"files\": {}}", + "dest": "cargo/vendor/tokio-rustls-0.24.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tokio-rustls/tokio-rustls-0.25.0.crate", + "sha256": "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f", + "dest": "cargo/vendor/tokio-rustls-0.25.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f\", \"files\": {}}", + "dest": "cargo/vendor/tokio-rustls-0.25.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tokio-rustls/tokio-rustls-0.26.0.crate", + "sha256": "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4", + "dest": "cargo/vendor/tokio-rustls-0.26.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4\", \"files\": {}}", + "dest": "cargo/vendor/tokio-rustls-0.26.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tokio-stream/tokio-stream-0.1.14.crate", + "sha256": "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842", + "dest": "cargo/vendor/tokio-stream-0.1.14" + }, + { + "type": "inline", + "contents": "{\"package\": \"397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842\", \"files\": {}}", + "dest": "cargo/vendor/tokio-stream-0.1.14", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tokio-tungstenite/tokio-tungstenite-0.24.0.crate", + "sha256": "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9", + "dest": "cargo/vendor/tokio-tungstenite-0.24.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9\", \"files\": {}}", + "dest": "cargo/vendor/tokio-tungstenite-0.24.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tokio-util/tokio-util-0.7.10.crate", + "sha256": "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15", + "dest": "cargo/vendor/tokio-util-0.7.10" + }, + { + "type": "inline", + "contents": "{\"package\": \"5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15\", \"files\": {}}", + "dest": "cargo/vendor/tokio-util-0.7.10", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/toml/toml-0.8.6.crate", + "sha256": "8ff9e3abce27ee2c9a37f9ad37238c1bdd4e789c84ba37df76aa4d528f5072cc", + "dest": "cargo/vendor/toml-0.8.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"8ff9e3abce27ee2c9a37f9ad37238c1bdd4e789c84ba37df76aa4d528f5072cc\", \"files\": {}}", + "dest": "cargo/vendor/toml-0.8.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/toml_datetime/toml_datetime-0.6.8.crate", + "sha256": "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41", + "dest": "cargo/vendor/toml_datetime-0.6.8" + }, + { + "type": "inline", + "contents": "{\"package\": \"0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41\", \"files\": {}}", + "dest": "cargo/vendor/toml_datetime-0.6.8", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/toml_edit/toml_edit-0.19.15.crate", + "sha256": "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421", + "dest": "cargo/vendor/toml_edit-0.19.15" + }, + { + "type": "inline", + "contents": "{\"package\": \"1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421\", \"files\": {}}", + "dest": "cargo/vendor/toml_edit-0.19.15", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/toml_edit/toml_edit-0.20.7.crate", + "sha256": "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81", + "dest": "cargo/vendor/toml_edit-0.20.7" + }, + { + "type": "inline", + "contents": "{\"package\": \"70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81\", \"files\": {}}", + "dest": "cargo/vendor/toml_edit-0.20.7", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/toml_edit/toml_edit-0.22.22.crate", + "sha256": "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5", + "dest": "cargo/vendor/toml_edit-0.22.22" + }, + { + "type": "inline", + "contents": "{\"package\": \"4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5\", \"files\": {}}", + "dest": "cargo/vendor/toml_edit-0.22.22", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tower/tower-0.4.13.crate", + "sha256": "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c", + "dest": "cargo/vendor/tower-0.4.13" + }, + { + "type": "inline", + "contents": "{\"package\": \"b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c\", \"files\": {}}", + "dest": "cargo/vendor/tower-0.4.13", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tower-layer/tower-layer-0.3.3.crate", + "sha256": "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e", + "dest": "cargo/vendor/tower-layer-0.3.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e\", \"files\": {}}", + "dest": "cargo/vendor/tower-layer-0.3.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tower-service/tower-service-0.3.2.crate", + "sha256": "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52", + "dest": "cargo/vendor/tower-service-0.3.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52\", \"files\": {}}", + "dest": "cargo/vendor/tower-service-0.3.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tracing/tracing-0.1.40.crate", + "sha256": "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef", + "dest": "cargo/vendor/tracing-0.1.40" + }, + { + "type": "inline", + "contents": "{\"package\": \"c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef\", \"files\": {}}", + "dest": "cargo/vendor/tracing-0.1.40", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tracing-attributes/tracing-attributes-0.1.27.crate", + "sha256": "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7", + "dest": "cargo/vendor/tracing-attributes-0.1.27" + }, + { + "type": "inline", + "contents": "{\"package\": \"34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7\", \"files\": {}}", + "dest": "cargo/vendor/tracing-attributes-0.1.27", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tracing-core/tracing-core-0.1.32.crate", + "sha256": "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54", + "dest": "cargo/vendor/tracing-core-0.1.32" + }, + { + "type": "inline", + "contents": "{\"package\": \"c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54\", \"files\": {}}", + "dest": "cargo/vendor/tracing-core-0.1.32", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tracing-futures/tracing-futures-0.2.5.crate", + "sha256": "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2", + "dest": "cargo/vendor/tracing-futures-0.2.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2\", \"files\": {}}", + "dest": "cargo/vendor/tracing-futures-0.2.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/try-lock/try-lock-0.2.4.crate", + "sha256": "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed", + "dest": "cargo/vendor/try-lock-0.2.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed\", \"files\": {}}", + "dest": "cargo/vendor/try-lock-0.2.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/tungstenite/tungstenite-0.24.0.crate", + "sha256": "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a", + "dest": "cargo/vendor/tungstenite-0.24.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a\", \"files\": {}}", + "dest": "cargo/vendor/tungstenite-0.24.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/typenum/typenum-1.17.0.crate", + "sha256": "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825", + "dest": "cargo/vendor/typenum-1.17.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825\", \"files\": {}}", + "dest": "cargo/vendor/typenum-1.17.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/uds_windows/uds_windows-1.0.2.crate", + "sha256": "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d", + "dest": "cargo/vendor/uds_windows-1.0.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d\", \"files\": {}}", + "dest": "cargo/vendor/uds_windows-1.0.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/unicode-bidi/unicode-bidi-0.3.13.crate", + "sha256": "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460", + "dest": "cargo/vendor/unicode-bidi-0.3.13" + }, + { + "type": "inline", + "contents": "{\"package\": \"92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460\", \"files\": {}}", + "dest": "cargo/vendor/unicode-bidi-0.3.13", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/unicode-ident/unicode-ident-1.0.12.crate", + "sha256": "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b", + "dest": "cargo/vendor/unicode-ident-1.0.12" + }, + { + "type": "inline", + "contents": "{\"package\": \"3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b\", \"files\": {}}", + "dest": "cargo/vendor/unicode-ident-1.0.12", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/unicode-normalization/unicode-normalization-0.1.22.crate", + "sha256": "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921", + "dest": "cargo/vendor/unicode-normalization-0.1.22" + }, + { + "type": "inline", + "contents": "{\"package\": \"5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921\", \"files\": {}}", + "dest": "cargo/vendor/unicode-normalization-0.1.22", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/unicode-width/unicode-width-0.1.11.crate", + "sha256": "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85", + "dest": "cargo/vendor/unicode-width-0.1.11" + }, + { + "type": "inline", + "contents": "{\"package\": \"e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85\", \"files\": {}}", + "dest": "cargo/vendor/unicode-width-0.1.11", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/untrusted/untrusted-0.9.0.crate", + "sha256": "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1", + "dest": "cargo/vendor/untrusted-0.9.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1\", \"files\": {}}", + "dest": "cargo/vendor/untrusted-0.9.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/url/url-2.4.1.crate", + "sha256": "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5", + "dest": "cargo/vendor/url-2.4.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5\", \"files\": {}}", + "dest": "cargo/vendor/url-2.4.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/utf-8/utf-8-0.7.6.crate", + "sha256": "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9", + "dest": "cargo/vendor/utf-8-0.7.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9\", \"files\": {}}", + "dest": "cargo/vendor/utf-8-0.7.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/utf8parse/utf8parse-0.2.2.crate", + "sha256": "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821", + "dest": "cargo/vendor/utf8parse-0.2.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821\", \"files\": {}}", + "dest": "cargo/vendor/utf8parse-0.2.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/uuid/uuid-1.5.0.crate", + "sha256": "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc", + "dest": "cargo/vendor/uuid-1.5.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc\", \"files\": {}}", + "dest": "cargo/vendor/uuid-1.5.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/value-bag/value-bag-1.10.0.crate", + "sha256": "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2", + "dest": "cargo/vendor/value-bag-1.10.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2\", \"files\": {}}", + "dest": "cargo/vendor/value-bag-1.10.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/vcpkg/vcpkg-0.2.15.crate", + "sha256": "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426", + "dest": "cargo/vendor/vcpkg-0.2.15" + }, + { + "type": "inline", + "contents": "{\"package\": \"accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426\", \"files\": {}}", + "dest": "cargo/vendor/vcpkg-0.2.15", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/vergen/vergen-9.0.1.crate", + "sha256": "349ed9e45296a581f455bc18039878f409992999bc1d5da12a6800eb18c8752f", + "dest": "cargo/vendor/vergen-9.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"349ed9e45296a581f455bc18039878f409992999bc1d5da12a6800eb18c8752f\", \"files\": {}}", + "dest": "cargo/vendor/vergen-9.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/vergen-gitcl/vergen-gitcl-1.0.1.crate", + "sha256": "2a3a7f91caabecefc3c249fd864b11d4abe315c166fbdb568964421bccfd2b7a", + "dest": "cargo/vendor/vergen-gitcl-1.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"2a3a7f91caabecefc3c249fd864b11d4abe315c166fbdb568964421bccfd2b7a\", \"files\": {}}", + "dest": "cargo/vendor/vergen-gitcl-1.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/vergen-lib/vergen-lib-0.1.4.crate", + "sha256": "229eaddb0050920816cf051e619affaf18caa3dd512de8de5839ccbc8e53abb0", + "dest": "cargo/vendor/vergen-lib-0.1.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"229eaddb0050920816cf051e619affaf18caa3dd512de8de5839ccbc8e53abb0\", \"files\": {}}", + "dest": "cargo/vendor/vergen-lib-0.1.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/version-compare/version-compare-0.1.1.crate", + "sha256": "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29", + "dest": "cargo/vendor/version-compare-0.1.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29\", \"files\": {}}", + "dest": "cargo/vendor/version-compare-0.1.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/version-compare/version-compare-0.2.0.crate", + "sha256": "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b", + "dest": "cargo/vendor/version-compare-0.2.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b\", \"files\": {}}", + "dest": "cargo/vendor/version-compare-0.2.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/version_check/version_check-0.9.4.crate", + "sha256": "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f", + "dest": "cargo/vendor/version_check-0.9.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f\", \"files\": {}}", + "dest": "cargo/vendor/version_check-0.9.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/waker-fn/waker-fn-1.1.1.crate", + "sha256": "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690", + "dest": "cargo/vendor/waker-fn-1.1.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690\", \"files\": {}}", + "dest": "cargo/vendor/waker-fn-1.1.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/walkdir/walkdir-2.4.0.crate", + "sha256": "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee", + "dest": "cargo/vendor/walkdir-2.4.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee\", \"files\": {}}", + "dest": "cargo/vendor/walkdir-2.4.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/want/want-0.3.1.crate", + "sha256": "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e", + "dest": "cargo/vendor/want-0.3.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e\", \"files\": {}}", + "dest": "cargo/vendor/want-0.3.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wasi/wasi-0.11.0+wasi-snapshot-preview1.crate", + "sha256": "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423", + "dest": "cargo/vendor/wasi-0.11.0+wasi-snapshot-preview1" + }, + { + "type": "inline", + "contents": "{\"package\": \"9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423\", \"files\": {}}", + "dest": "cargo/vendor/wasi-0.11.0+wasi-snapshot-preview1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wasm-bindgen/wasm-bindgen-0.2.95.crate", + "sha256": "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e", + "dest": "cargo/vendor/wasm-bindgen-0.2.95" + }, + { + "type": "inline", + "contents": "{\"package\": \"128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e\", \"files\": {}}", + "dest": "cargo/vendor/wasm-bindgen-0.2.95", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wasm-bindgen-backend/wasm-bindgen-backend-0.2.95.crate", + "sha256": "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358", + "dest": "cargo/vendor/wasm-bindgen-backend-0.2.95" + }, + { + "type": "inline", + "contents": "{\"package\": \"cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358\", \"files\": {}}", + "dest": "cargo/vendor/wasm-bindgen-backend-0.2.95", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wasm-bindgen-futures/wasm-bindgen-futures-0.4.38.crate", + "sha256": "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02", + "dest": "cargo/vendor/wasm-bindgen-futures-0.4.38" + }, + { + "type": "inline", + "contents": "{\"package\": \"9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02\", \"files\": {}}", + "dest": "cargo/vendor/wasm-bindgen-futures-0.4.38", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wasm-bindgen-macro/wasm-bindgen-macro-0.2.95.crate", + "sha256": "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56", + "dest": "cargo/vendor/wasm-bindgen-macro-0.2.95" + }, + { + "type": "inline", + "contents": "{\"package\": \"e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56\", \"files\": {}}", + "dest": "cargo/vendor/wasm-bindgen-macro-0.2.95", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wasm-bindgen-macro-support/wasm-bindgen-macro-support-0.2.95.crate", + "sha256": "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68", + "dest": "cargo/vendor/wasm-bindgen-macro-support-0.2.95" + }, + { + "type": "inline", + "contents": "{\"package\": \"26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68\", \"files\": {}}", + "dest": "cargo/vendor/wasm-bindgen-macro-support-0.2.95", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/wasm-bindgen-shared/wasm-bindgen-shared-0.2.95.crate", + "sha256": "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d", + "dest": "cargo/vendor/wasm-bindgen-shared-0.2.95" + }, + { + "type": "inline", + "contents": "{\"package\": \"65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d\", \"files\": {}}", + "dest": "cargo/vendor/wasm-bindgen-shared-0.2.95", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/web-sys/web-sys-0.3.65.crate", + "sha256": "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85", + "dest": "cargo/vendor/web-sys-0.3.65" + }, + { + "type": "inline", + "contents": "{\"package\": \"5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85\", \"files\": {}}", + "dest": "cargo/vendor/web-sys-0.3.65", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/webpki/webpki-0.22.4.crate", + "sha256": "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53", + "dest": "cargo/vendor/webpki-0.22.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53\", \"files\": {}}", + "dest": "cargo/vendor/webpki-0.22.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/webpki-roots/webpki-roots-0.25.4.crate", + "sha256": "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1", + "dest": "cargo/vendor/webpki-roots-0.25.4" + }, + { + "type": "inline", + "contents": "{\"package\": \"5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1\", \"files\": {}}", + "dest": "cargo/vendor/webpki-roots-0.25.4", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/which/which-4.4.2.crate", + "sha256": "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7", + "dest": "cargo/vendor/which-4.4.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7\", \"files\": {}}", + "dest": "cargo/vendor/which-4.4.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/winapi/winapi-0.3.9.crate", + "sha256": "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419", + "dest": "cargo/vendor/winapi-0.3.9" + }, + { + "type": "inline", + "contents": "{\"package\": \"5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419\", \"files\": {}}", + "dest": "cargo/vendor/winapi-0.3.9", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/winapi-i686-pc-windows-gnu/winapi-i686-pc-windows-gnu-0.4.0.crate", + "sha256": "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6", + "dest": "cargo/vendor/winapi-i686-pc-windows-gnu-0.4.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6\", \"files\": {}}", + "dest": "cargo/vendor/winapi-i686-pc-windows-gnu-0.4.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/winapi-util/winapi-util-0.1.6.crate", + "sha256": "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596", + "dest": "cargo/vendor/winapi-util-0.1.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596\", \"files\": {}}", + "dest": "cargo/vendor/winapi-util-0.1.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/winapi-x86_64-pc-windows-gnu/winapi-x86_64-pc-windows-gnu-0.4.0.crate", + "sha256": "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f", + "dest": "cargo/vendor/winapi-x86_64-pc-windows-gnu-0.4.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f\", \"files\": {}}", + "dest": "cargo/vendor/winapi-x86_64-pc-windows-gnu-0.4.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows/windows-0.52.0.crate", + "sha256": "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be", + "dest": "cargo/vendor/windows-0.52.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be\", \"files\": {}}", + "dest": "cargo/vendor/windows-0.52.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows/windows-0.54.0.crate", + "sha256": "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49", + "dest": "cargo/vendor/windows-0.54.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49\", \"files\": {}}", + "dest": "cargo/vendor/windows-0.54.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows/windows-0.57.0.crate", + "sha256": "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143", + "dest": "cargo/vendor/windows-0.57.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143\", \"files\": {}}", + "dest": "cargo/vendor/windows-0.57.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-core/windows-core-0.51.1.crate", + "sha256": "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64", + "dest": "cargo/vendor/windows-core-0.51.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64\", \"files\": {}}", + "dest": "cargo/vendor/windows-core-0.51.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-core/windows-core-0.52.0.crate", + "sha256": "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9", + "dest": "cargo/vendor/windows-core-0.52.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9\", \"files\": {}}", + "dest": "cargo/vendor/windows-core-0.52.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-core/windows-core-0.54.0.crate", + "sha256": "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65", + "dest": "cargo/vendor/windows-core-0.54.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65\", \"files\": {}}", + "dest": "cargo/vendor/windows-core-0.54.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-core/windows-core-0.57.0.crate", + "sha256": "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d", + "dest": "cargo/vendor/windows-core-0.57.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d\", \"files\": {}}", + "dest": "cargo/vendor/windows-core-0.57.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-implement/windows-implement-0.57.0.crate", + "sha256": "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7", + "dest": "cargo/vendor/windows-implement-0.57.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7\", \"files\": {}}", + "dest": "cargo/vendor/windows-implement-0.57.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-interface/windows-interface-0.57.0.crate", + "sha256": "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7", + "dest": "cargo/vendor/windows-interface-0.57.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7\", \"files\": {}}", + "dest": "cargo/vendor/windows-interface-0.57.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-result/windows-result-0.1.2.crate", + "sha256": "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8", + "dest": "cargo/vendor/windows-result-0.1.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8\", \"files\": {}}", + "dest": "cargo/vendor/windows-result-0.1.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-sys/windows-sys-0.45.0.crate", + "sha256": "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0", + "dest": "cargo/vendor/windows-sys-0.45.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0\", \"files\": {}}", + "dest": "cargo/vendor/windows-sys-0.45.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-sys/windows-sys-0.48.0.crate", + "sha256": "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9", + "dest": "cargo/vendor/windows-sys-0.48.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9\", \"files\": {}}", + "dest": "cargo/vendor/windows-sys-0.48.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-sys/windows-sys-0.52.0.crate", + "sha256": "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d", + "dest": "cargo/vendor/windows-sys-0.52.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d\", \"files\": {}}", + "dest": "cargo/vendor/windows-sys-0.52.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-sys/windows-sys-0.59.0.crate", + "sha256": "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b", + "dest": "cargo/vendor/windows-sys-0.59.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b\", \"files\": {}}", + "dest": "cargo/vendor/windows-sys-0.59.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-targets/windows-targets-0.42.2.crate", + "sha256": "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071", + "dest": "cargo/vendor/windows-targets-0.42.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071\", \"files\": {}}", + "dest": "cargo/vendor/windows-targets-0.42.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-targets/windows-targets-0.48.5.crate", + "sha256": "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c", + "dest": "cargo/vendor/windows-targets-0.48.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c\", \"files\": {}}", + "dest": "cargo/vendor/windows-targets-0.48.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows-targets/windows-targets-0.52.6.crate", + "sha256": "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973", + "dest": "cargo/vendor/windows-targets-0.52.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973\", \"files\": {}}", + "dest": "cargo/vendor/windows-targets-0.52.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_aarch64_gnullvm/windows_aarch64_gnullvm-0.42.2.crate", + "sha256": "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8", + "dest": "cargo/vendor/windows_aarch64_gnullvm-0.42.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8\", \"files\": {}}", + "dest": "cargo/vendor/windows_aarch64_gnullvm-0.42.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_aarch64_gnullvm/windows_aarch64_gnullvm-0.48.5.crate", + "sha256": "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8", + "dest": "cargo/vendor/windows_aarch64_gnullvm-0.48.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8\", \"files\": {}}", + "dest": "cargo/vendor/windows_aarch64_gnullvm-0.48.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_aarch64_gnullvm/windows_aarch64_gnullvm-0.52.6.crate", + "sha256": "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3", + "dest": "cargo/vendor/windows_aarch64_gnullvm-0.52.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3\", \"files\": {}}", + "dest": "cargo/vendor/windows_aarch64_gnullvm-0.52.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_aarch64_msvc/windows_aarch64_msvc-0.42.2.crate", + "sha256": "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43", + "dest": "cargo/vendor/windows_aarch64_msvc-0.42.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43\", \"files\": {}}", + "dest": "cargo/vendor/windows_aarch64_msvc-0.42.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_aarch64_msvc/windows_aarch64_msvc-0.48.5.crate", + "sha256": "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc", + "dest": "cargo/vendor/windows_aarch64_msvc-0.48.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc\", \"files\": {}}", + "dest": "cargo/vendor/windows_aarch64_msvc-0.48.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_aarch64_msvc/windows_aarch64_msvc-0.52.6.crate", + "sha256": "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469", + "dest": "cargo/vendor/windows_aarch64_msvc-0.52.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469\", \"files\": {}}", + "dest": "cargo/vendor/windows_aarch64_msvc-0.52.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_i686_gnu/windows_i686_gnu-0.42.2.crate", + "sha256": "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f", + "dest": "cargo/vendor/windows_i686_gnu-0.42.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f\", \"files\": {}}", + "dest": "cargo/vendor/windows_i686_gnu-0.42.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_i686_gnu/windows_i686_gnu-0.48.5.crate", + "sha256": "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e", + "dest": "cargo/vendor/windows_i686_gnu-0.48.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e\", \"files\": {}}", + "dest": "cargo/vendor/windows_i686_gnu-0.48.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_i686_gnu/windows_i686_gnu-0.52.6.crate", + "sha256": "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b", + "dest": "cargo/vendor/windows_i686_gnu-0.52.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b\", \"files\": {}}", + "dest": "cargo/vendor/windows_i686_gnu-0.52.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_i686_gnullvm/windows_i686_gnullvm-0.52.6.crate", + "sha256": "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66", + "dest": "cargo/vendor/windows_i686_gnullvm-0.52.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66\", \"files\": {}}", + "dest": "cargo/vendor/windows_i686_gnullvm-0.52.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_i686_msvc/windows_i686_msvc-0.42.2.crate", + "sha256": "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060", + "dest": "cargo/vendor/windows_i686_msvc-0.42.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060\", \"files\": {}}", + "dest": "cargo/vendor/windows_i686_msvc-0.42.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_i686_msvc/windows_i686_msvc-0.48.5.crate", + "sha256": "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406", + "dest": "cargo/vendor/windows_i686_msvc-0.48.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406\", \"files\": {}}", + "dest": "cargo/vendor/windows_i686_msvc-0.48.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_i686_msvc/windows_i686_msvc-0.52.6.crate", + "sha256": "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66", + "dest": "cargo/vendor/windows_i686_msvc-0.52.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66\", \"files\": {}}", + "dest": "cargo/vendor/windows_i686_msvc-0.52.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_x86_64_gnu/windows_x86_64_gnu-0.42.2.crate", + "sha256": "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36", + "dest": "cargo/vendor/windows_x86_64_gnu-0.42.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36\", \"files\": {}}", + "dest": "cargo/vendor/windows_x86_64_gnu-0.42.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_x86_64_gnu/windows_x86_64_gnu-0.48.5.crate", + "sha256": "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e", + "dest": "cargo/vendor/windows_x86_64_gnu-0.48.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e\", \"files\": {}}", + "dest": "cargo/vendor/windows_x86_64_gnu-0.48.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_x86_64_gnu/windows_x86_64_gnu-0.52.6.crate", + "sha256": "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78", + "dest": "cargo/vendor/windows_x86_64_gnu-0.52.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78\", \"files\": {}}", + "dest": "cargo/vendor/windows_x86_64_gnu-0.52.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_x86_64_gnullvm/windows_x86_64_gnullvm-0.42.2.crate", + "sha256": "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3", + "dest": "cargo/vendor/windows_x86_64_gnullvm-0.42.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3\", \"files\": {}}", + "dest": "cargo/vendor/windows_x86_64_gnullvm-0.42.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_x86_64_gnullvm/windows_x86_64_gnullvm-0.48.5.crate", + "sha256": "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc", + "dest": "cargo/vendor/windows_x86_64_gnullvm-0.48.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc\", \"files\": {}}", + "dest": "cargo/vendor/windows_x86_64_gnullvm-0.48.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_x86_64_gnullvm/windows_x86_64_gnullvm-0.52.6.crate", + "sha256": "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d", + "dest": "cargo/vendor/windows_x86_64_gnullvm-0.52.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d\", \"files\": {}}", + "dest": "cargo/vendor/windows_x86_64_gnullvm-0.52.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_x86_64_msvc/windows_x86_64_msvc-0.42.2.crate", + "sha256": "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0", + "dest": "cargo/vendor/windows_x86_64_msvc-0.42.2" + }, + { + "type": "inline", + "contents": "{\"package\": \"9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0\", \"files\": {}}", + "dest": "cargo/vendor/windows_x86_64_msvc-0.42.2", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_x86_64_msvc/windows_x86_64_msvc-0.48.5.crate", + "sha256": "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538", + "dest": "cargo/vendor/windows_x86_64_msvc-0.48.5" + }, + { + "type": "inline", + "contents": "{\"package\": \"ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538\", \"files\": {}}", + "dest": "cargo/vendor/windows_x86_64_msvc-0.48.5", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/windows_x86_64_msvc/windows_x86_64_msvc-0.52.6.crate", + "sha256": "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec", + "dest": "cargo/vendor/windows_x86_64_msvc-0.52.6" + }, + { + "type": "inline", + "contents": "{\"package\": \"589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec\", \"files\": {}}", + "dest": "cargo/vendor/windows_x86_64_msvc-0.52.6", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/winnow/winnow-0.5.18.crate", + "sha256": "176b6138793677221d420fd2f0aeeced263f197688b36484660da767bca2fa32", + "dest": "cargo/vendor/winnow-0.5.18" + }, + { + "type": "inline", + "contents": "{\"package\": \"176b6138793677221d420fd2f0aeeced263f197688b36484660da767bca2fa32\", \"files\": {}}", + "dest": "cargo/vendor/winnow-0.5.18", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/winnow/winnow-0.6.20.crate", + "sha256": "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b", + "dest": "cargo/vendor/winnow-0.6.20" + }, + { + "type": "inline", + "contents": "{\"package\": \"36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b\", \"files\": {}}", + "dest": "cargo/vendor/winnow-0.6.20", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/winreg/winreg-0.50.0.crate", + "sha256": "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1", + "dest": "cargo/vendor/winreg-0.50.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1\", \"files\": {}}", + "dest": "cargo/vendor/winreg-0.50.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/xdg-home/xdg-home-1.0.0.crate", + "sha256": "2769203cd13a0c6015d515be729c526d041e9cf2c0cc478d57faee85f40c6dcd", + "dest": "cargo/vendor/xdg-home-1.0.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"2769203cd13a0c6015d515be729c526d041e9cf2c0cc478d57faee85f40c6dcd\", \"files\": {}}", + "dest": "cargo/vendor/xdg-home-1.0.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zbus/zbus-3.14.1.crate", + "sha256": "31de390a2d872e4cd04edd71b425e29853f786dc99317ed72d73d6fcf5ebb948", + "dest": "cargo/vendor/zbus-3.14.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"31de390a2d872e4cd04edd71b425e29853f786dc99317ed72d73d6fcf5ebb948\", \"files\": {}}", + "dest": "cargo/vendor/zbus-3.14.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zbus_macros/zbus_macros-3.14.1.crate", + "sha256": "41d1794a946878c0e807f55a397187c11fc7a038ba5d868e7db4f3bd7760bc9d", + "dest": "cargo/vendor/zbus_macros-3.14.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"41d1794a946878c0e807f55a397187c11fc7a038ba5d868e7db4f3bd7760bc9d\", \"files\": {}}", + "dest": "cargo/vendor/zbus_macros-3.14.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zbus_names/zbus_names-2.6.0.crate", + "sha256": "fb80bb776dbda6e23d705cf0123c3b95df99c4ebeaec6c2599d4a5419902b4a9", + "dest": "cargo/vendor/zbus_names-2.6.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"fb80bb776dbda6e23d705cf0123c3b95df99c4ebeaec6c2599d4a5419902b4a9\", \"files\": {}}", + "dest": "cargo/vendor/zbus_names-2.6.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zerocopy/zerocopy-0.7.35.crate", + "sha256": "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0", + "dest": "cargo/vendor/zerocopy-0.7.35" + }, + { + "type": "inline", + "contents": "{\"package\": \"1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0\", \"files\": {}}", + "dest": "cargo/vendor/zerocopy-0.7.35", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zerocopy-derive/zerocopy-derive-0.7.35.crate", + "sha256": "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e", + "dest": "cargo/vendor/zerocopy-derive-0.7.35" + }, + { + "type": "inline", + "contents": "{\"package\": \"fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e\", \"files\": {}}", + "dest": "cargo/vendor/zerocopy-derive-0.7.35", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zeroize/zeroize-1.8.1.crate", + "sha256": "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde", + "dest": "cargo/vendor/zeroize-1.8.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde\", \"files\": {}}", + "dest": "cargo/vendor/zeroize-1.8.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zvariant/zvariant-3.15.0.crate", + "sha256": "44b291bee0d960c53170780af148dca5fa260a63cdd24f1962fa82e03e53338c", + "dest": "cargo/vendor/zvariant-3.15.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"44b291bee0d960c53170780af148dca5fa260a63cdd24f1962fa82e03e53338c\", \"files\": {}}", + "dest": "cargo/vendor/zvariant-3.15.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zvariant_derive/zvariant_derive-3.15.0.crate", + "sha256": "934d7a7dfc310d6ee06c87ffe88ef4eca7d3e37bb251dece2ef93da8f17d8ecd", + "dest": "cargo/vendor/zvariant_derive-3.15.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"934d7a7dfc310d6ee06c87ffe88ef4eca7d3e37bb251dece2ef93da8f17d8ecd\", \"files\": {}}", + "dest": "cargo/vendor/zvariant_derive-3.15.0", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/zvariant_utils/zvariant_utils-1.0.1.crate", + "sha256": "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200", + "dest": "cargo/vendor/zvariant_utils-1.0.1" + }, + { + "type": "inline", + "contents": "{\"package\": \"7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200\", \"files\": {}}", + "dest": "cargo/vendor/zvariant_utils-1.0.1", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "inline", + "contents": "[source.vendored-sources]\ndirectory = \"cargo/vendor\"\n\n[source.crates-io]\nreplace-with = \"vendored-sources\"\n", + "dest": "cargo", + "dest-filename": "config" + } +] \ No newline at end of file diff --git a/data/appstream/1.png b/data/appstream/1.png new file mode 100644 index 0000000000000000000000000000000000000000..ea1f28ba7cf02a3aca690d0c53a8fa5b223e6efc GIT binary patch literal 36723 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

pI`bh{lGi`2cB$L1N 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 literal 0 HcmV?d00001 diff --git a/data/appstream/2.png b/data/appstream/2.png new file mode 100644 index 0000000000000000000000000000000000000000..808afdbbec53257346de0e96182027611f4257b2 GIT binary patch literal 129394 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

%ZnqZ< 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�*j=Jyx!RovAAP|# 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

hht*AeB%iJJ5ngYcHJEwlJ0OoJWrtQioU> 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 z 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 + + Adwaita Icon Template + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + GNOME Design Team + + + + + Adwaita Icon Template + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hicolor + Symbolic + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/dev.alextren.Spot.appdata.xml b/data/dev.alextren.Spot.appdata.xml new file mode 100644 index 0000000..bd19894 --- /dev/null +++ b/data/dev.alextren.Spot.appdata.xml @@ -0,0 +1,356 @@ + + + dev.alextren.Spot + https://github.com/xou816/spot + Spot +

Listen to music on Spotify + dev.alextren.Spot.desktop + CC0-1.0 + MIT + + + 360 + + + keyboard + pointing + touch + + + + workstation + mobile + + +

+ Listen to music on Spotify. + Requires a premium account. +

+

Current features:

+
    +
  • playback control (play/pause, prev/next, seeking)
  • +
  • play queue with shuffle option
  • +
  • selection mode: easily browse and select mutliple tracks to queue them
  • +
  • browse your saved albums and playlists
  • +
  • search albums and artists
  • +
  • view an artist's releases
  • +
  • view users' playlists
  • +
  • credentials management with Secret Service
  • +
  • MPRIS integration
  • +
+
+ + + https://raw.githubusercontent.com/xou816/spot/master/data/appstream/1.png + + + https://raw.githubusercontent.com/xou816/spot/master/data/appstream/2.png + + + https://raw.githubusercontent.com/xou816/spot/master/data/appstream/3.png + + + + +

+ It's been a while, hasn't it? I'm back, but please allow everyone some time to work through the issues and pull requests :) +

+
    +
  • added an option for gapless playback (thanks @supremesnickers!)
  • +
  • fixed theme management (thanks @ctsk!)
  • +
  • fixed the behaviour of the "previous" button for consistency with other media players (thanks @SomewhereOutInSpace!)
  • +
  • the user will now be prompted to unlock the keyring if needed (thanks @rainDiX!)
  • +
  • added playlist creation and edition
  • +
  • more bug fixes
  • +
+

+ Thanks to all the contributors who are somehow still around :) If you feel like you've been unjustly forgotten from the AUTHORS or TRANSLATORS file, feel free to open a PR. +

+
+
+ + +

+ What's new: +

+
    +
  • login dialog now uses a password entry (thanks @Toorero!)
  • +
  • fix a startup crash with some locales
  • +
+
+
+ + +

+ New features: +

+
    +
  • save tracks to library
  • +
  • basic Spotify URI handling (thanks @bbb651!)
  • +
  • settings dialog (thanks @sei0o!)
  • +
  • brand new icon (thanks @bertob!)
  • +
  • bug fixes
  • +
+

+ Thank you to everyone involved in this release! +

+
+
+ + + +

Bugfix release:

+
    +
  • fix a startup crash with some locales (thanks @jakubiszon26!)
  • +
  • fix a crash when attempting to view some users' profile (thanks @juxuanu!)
  • +
+
+
+ + +

+ There has been no release for a while, but this one packs quite a few features: +

+
    +
  • redesigned several parts of the application: headers, selection tools, library, album view... (thanks @jannuary!)
  • +
  • many, many, many more subtle design changes and tweaks still owed to @jannuary
  • +
  • playlists are now accessible from the sidebar (thanks @abegert!)
  • +
  • the album art is visible for individual songs, and the release year is visible for albums (thanks @abegert and @sei0o!)
  • +
  • volume control over MPRIS (thanks @Diegovsky!)
  • +
  • as always, updated translations for yet more languages; thanks to the many people involved on POEditor!
  • +
+

+ Thank you so much to everyone involved for this release, including those I could not mention. If you're willing to contribute, do not hesitate to seek advice from those trusted contributors, and be sure to direct your UX questions towards @jannuary (as long as they're fine with it!). +

+
+
+ + +

Quick fix for a startup crash, sorry for the inconvenience. Thanks @xRMG412!

+
+
+ + +

+ What's new: +

+
    +
  • browse saved tracks from Spot
  • +
  • a status page is now displayed when no albums or playlists have been added (thanks @Diegovsky!)
  • +
  • change the access point port from GSettings; this should help users running Spot behind a firewall (thanks @sei0o!)
  • +
  • display a warning in the login dialog if Caps Lock is enabled (thanks @przebor!)
  • +
  • added Purism form-factor metadata; this should improve the discoverability of Spot (thanks @1peter10!), and generally improved the appstream metadata to match the updated guidelines
  • +
  • various bugfixes (clear credentials on auth failure, load all tracks for long albums...)
  • +
+

Thank you to the many contributors to this release, as well as to all those contributing translations on POEditor!

+
+
+ + +

+ What's new: +

+
    +
  • ported app to GTK4 and libadwaita
  • +
  • MPRIS: handle shuffling and loop status
  • +
  • improved album size on small screens
  • +
+
+
+ + +

+ What's new: +

+
    +
  • added a new album info modal with full release details (thanks TotalDarkness-NRF!)
  • +
  • improved player controls: added the ability to repeat a single track or the whole queue (thanks TotalDarkness-NRF!)
  • +
  • improved login credentials management: (re)use auth token when possible instead of always using email and password (thanks nn1ks!)
  • +
  • MPRIS fixes: raising the player is now properly implemented (thanks eladyn!)
  • +
  • ...actually fix an issue with playlists not being modifiable when login in with email
  • +
+

+ Special thanks to TotalDarkness-NRF for contributing several major features for this version. +

+
+
+ + +

+ What's new: +

+
    +
  • ability to remove tracks from writable playlists
  • +
  • improved login error handling
  • +
  • fixed an issue with playlists not being modifiable when login in with email
  • +
  • MPRIS: the desktop entry for Spot is now properly referenced (thanks nicolasfella!)
  • +
  • quality: various clippy fixes (thanks nn1ks!)
  • +
  • new translations: Turkish, Indonesian and Brazilian Portuguese (thanks YusufOzmen01, cho2 and lucraraujo, as well as ondras12345 for the reviews!)
  • +
  • +
+
+
+ + +

+ What's new: +

+
    +
  • long playlists are now handled (somewhat) properly, although this has some drawbacks (shuffling isn't so random...)
  • +
  • the MPRIS implementation now supports seeking, and should report the proper album art and album name; thanks Douile!
  • +
  • selection tools now allow adding to a playlist (not yet removing, though)
  • +
  • Russian translation
  • +
+

+ Thanks to all contributors, translators and bug reporters! +

+
+
+ + +

+ What's new: +

+
    +
  • new selection tools: move tracks up and down in queue, quickly select multiple tracks from current view...
  • +
  • touch and hold a song list (or right click) to enter selection mode
  • +
  • Portuguese translation
  • +
  • fixed session restoration requiring manually repeating last action after a long period of inactivity
  • +
  • fixed parsing for playlists with local tracks
  • +
+
+
+ + +

+ What's new: +

+
    +
  • browse users' playlists by clicking on their name in playlists you follow (thanks a bunch, Douile!)
  • +
  • Catalan, Czech, Polish and Spanish translations; thanks to all the translators involved!
  • +
  • bug fixes
  • +
+
+
+ + +

+ This release fixes the broken "Search" icon, and adds French, German and Dutch translations. Thanks to all translators for their contribution! +

+
+
+ + +

+ New features: +

+
    +
  • redesigned seekbar, login dialog and search (type from any screen to start a search)
  • +
  • song durations in playlist widgets (thanks realJavabot!)
  • +
  • play queue management (queue/dequeue/clear queue)
  • +
  • selection mode: enter selection mode to easily queue multiple tracks
  • +
  • keyboards shortcuts
  • +
  • various playback options through GSettings (no GUI yet)
  • +
+
+
+ + +

+ New feature: contextual menus for songs are now available everywhere, allowing you to easily navigate to related artists or share tracks. Thanks Douile! +

+

+ This release also includes numerous fixes, including a few crashes, performance issues, and most importantly playlists being truncated. +

+
+
+ + +

+ New feature: the main window can now be closed without stopping playback. Use Quit or Ctrl+Q to exit the app. +

+

+ This release fixes numerous issues, including: main window being too large and too tall for phones, startup crash for playlist without images, "About" dialog not closing in qtile, keyboard navigation being broken in the login dialog. Special thanks to Douile for contributing fixes for many of these isssues! +

+
+
+ + +

+ Hotfix for a crash in search results +

+
+
+ + +

+ New features: +

+
    +
  • search results now include artists
  • +
  • albums can be saved and unsaved from the library
  • +
+
+
+ + +

+ New features: +

+
    +
  • browse saved playlists
  • +
  • quick access to "Now playing"
  • +
  • artists top tracks
  • +
  • API token renewal
  • +
+
+
+ + +

+ New features: +

+
    +
  • adaptive UI with libhandy
  • +
+
+
+ + +

+ New features: +

+
    +
  • minimal MPRIS integration
  • +
  • "About" dialog
  • +
+
+
+ + +

+ New features: +

+
    +
  • improved playlist widget, added menu to jump from "Now playing" to related albums
  • +
  • added in-app notifications for some errors
  • +
+

Fixes:

+
    +
  • fixed an issue where songs would be skipped in a playlist when autoplaying
  • +
  • fixed character encoding in search queries
  • +
+
+
+ + +

Notables changes: symbolic icons everyhere (thanks gabmus!), shuffle play, logout

+
+
+ + +

Initial release

+
+
+
+ diff --git a/data/dev.alextren.Spot.desktop b/data/dev.alextren.Spot.desktop new file mode 100644 index 0000000..e06a59d --- /dev/null +++ b/data/dev.alextren.Spot.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Version=1.5 +Name=Spot +Exec=spot %u +GenericName=Music Player +Icon=dev.alextren.Spot +Terminal=false +Type=Application +Categories=GTK;GNOME;Music;AudioVideo; +MimeType=x-scheme-handler/spotify; +StartupNotify=true +X-Purism-FormFactor=Workstation;Mobile; +SingleMainWindow=true diff --git a/data/dev.alextren.Spot.gschema.xml b/data/dev.alextren.Spot.gschema.xml new file mode 100644 index 0000000..bd97fc3 --- /dev/null +++ b/data/dev.alextren.Spot.gschema.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + 'system' + The theme preference + + + 1080 + The width of the window + + + 720 + The height of the window + + + false + A flag to enable maximized mode + + + '160' + Songs bitrate (96, 160, 320kbps) + + + 'pulseaudio' + Audio backend + + + true + A flag to enable gap-less playback + + + 'default' + Alsa device (if audio backend is 'alsa') + + + 0 + Port to communicate with Spotify's server (access point). Setting to 0 (default) allows Spot to use servers running on any port. + + + \ No newline at end of file diff --git a/data/hicolor/scalable/apps/dev.alextren.Spot.svg b/data/hicolor/scalable/apps/dev.alextren.Spot.svg new file mode 100644 index 0000000..71f857d --- /dev/null +++ b/data/hicolor/scalable/apps/dev.alextren.Spot.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/hicolor/symbolic/apps/dev.alextren.Spot-symbolic.svg b/data/hicolor/symbolic/apps/dev.alextren.Spot-symbolic.svg new file mode 100644 index 0000000..b4029bc --- /dev/null +++ b/data/hicolor/symbolic/apps/dev.alextren.Spot-symbolic.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/data/meson.build b/data/meson.build new file mode 100644 index 0000000..99ecc46 --- /dev/null +++ b/data/meson.build @@ -0,0 +1,31 @@ +install_data('dev.alextren.Spot.desktop', + install_dir: get_option('datadir') / 'applications' +) + +install_subdir('hicolor', + install_dir: get_option('datadir') / 'icons' +) + +install_data('dev.alextren.Spot.appdata.xml', + install_dir: get_option('datadir') / 'appdata' +) + +install_data('dev.alextren.Spot.gschema.xml', + install_dir: get_option('datadir') / 'glib-2.0/schemas' +) + +compile_schemas = find_program('glib-compile-schemas', required: true) +if compile_schemas.found() + test('Validate schema file', compile_schemas, + args: ['--strict', '--dry-run', meson.current_source_dir()] + ) +endif + +appstream_util = find_program('appstream-util', required: false) +if appstream_util.found() + test( + 'Validate appstream appdata', + appstream_util, + args: ['validate-relax', meson.current_source_dir() / 'dev.alextren.Spot.appdata.xml'] + ) +endif diff --git a/dev.alextren.Spot.development.json b/dev.alextren.Spot.development.json new file mode 100644 index 0000000..e8ba03d --- /dev/null +++ b/dev.alextren.Spot.development.json @@ -0,0 +1,68 @@ +{ + "app-id": "dev.alextren.Spot", + "runtime": "org.gnome.Platform", + "runtime-version": "master", + "sdk": "org.gnome.Sdk", + "sdk-extensions": [ + "org.freedesktop.Sdk.Extension.rust-stable" + ], + "command": "spot", + "finish-args": [ + "--share=network", + "--share=ipc", + "--socket=fallback-x11", + "--socket=wayland", + "--socket=pulseaudio", + "--device=dri", + "--talk-name=org.freedesktop.secrets", + "--own-name=org.mpris.MediaPlayer2.Spot" + ], + "separate-locales": false, + "build-options": { + "append-path": "/usr/lib/sdk/rust-stable/bin", + "env": { + "RUST_BACKTRACE": "full", + "RUST_LOG": "spot=debug" + } + }, + "cleanup": [ + "/include", + "/lib/pkgconfig", + "/man", + "/share/doc", + "/share/gtk-doc", + "/share/man", + "/share/pkgconfig", + "*.la", + "*.a" + ], + "modules": [ + { + "name": "blueprint-compiler", + "buildsystem": "meson", + "sources": [ + { + "type": "archive", + "url": "https://gitlab.gnome.org/jwestman/blueprint-compiler/-/archive/v0.8.1/blueprint-compiler-v0.8.1.tar.gz", + "sha256": "9207697cfac6e87a3c0ccf463be1a95c3bd06aa017c966a7e352ad5bc486cf3c" + } + ] + }, + { + "name": "spot", + "builddir": true, + "buildsystem": "meson", + "config-opts": [ + "-Doffline=true", + "-Dbuildtype=debug" + ], + "sources": [ + { + "type": "dir", + "path": "." + }, + "cargo-sources.json" + ] + } + ] +} diff --git a/dev.alextren.Spot.snapshots.json b/dev.alextren.Spot.snapshots.json new file mode 100644 index 0000000..6ae609c --- /dev/null +++ b/dev.alextren.Spot.snapshots.json @@ -0,0 +1,71 @@ +{ + "app-id": "dev.alextren.Spot", + "runtime": "org.gnome.Platform", + "runtime-version": "44", + "sdk": "org.gnome.Sdk", + "sdk-extensions": [ + "org.freedesktop.Sdk.Extension.rust-stable" + ], + "command": "spot", + "finish-args": [ + "--share=network", + "--share=ipc", + "--socket=fallback-x11", + "--socket=wayland", + "--socket=pulseaudio", + "--device=dri", + "--talk-name=org.freedesktop.secrets", + "--own-name=org.mpris.MediaPlayer2.Spot" + ], + "separate-locales": false, + "build-options": { + "append-path": "/usr/lib/sdk/rust-stable/bin", + "env": { + "CARGO_HOME": "/run/build/spot/cargo", + "RUST_BACKTRACE": "full", + "RUST_LOG": "spot=debug" + }, + "strip": false, + "no-debuginfo": true + }, + "cleanup": [ + "/include", + "/lib/pkgconfig", + "/man", + "/share/doc", + "/share/gtk-doc", + "/share/man", + "/share/pkgconfig", + "*.la", + "*.a" + ], + "modules": [ + { + "name": "blueprint-compiler", + "buildsystem": "meson", + "sources": [ + { + "type": "archive", + "url": "https://gitlab.gnome.org/jwestman/blueprint-compiler/-/archive/v0.8.1/blueprint-compiler-v0.8.1.tar.gz", + "sha256": "9207697cfac6e87a3c0ccf463be1a95c3bd06aa017c966a7e352ad5bc486cf3c" + } + ] + }, + { + "name": "spot", + "builddir": true, + "buildsystem": "meson", + "config-opts": [ + "-Doffline=true", + "-Dbuildtype=debug" + ], + "sources": [ + { + "type": "dir", + "path": "." + }, + "cargo-sources.json" + ] + } + ] +} diff --git a/doc/.latexmkrc b/doc/.latexmkrc new file mode 100644 index 0000000..b4730ac --- /dev/null +++ b/doc/.latexmkrc @@ -0,0 +1,4 @@ +$pdf_mode = 1; +$pdf_previewer = ''; +$pdflatex = 'lualatex -interaction=nonstopmode -synctex=1 -shell-escape %O %S'; +$out_dir = 'target'; diff --git a/doc/Dockerfile b/doc/Dockerfile new file mode 100644 index 0000000..3de29a9 --- /dev/null +++ b/doc/Dockerfile @@ -0,0 +1,4 @@ +FROM atrendel/doxerlive:15-basic +RUN apk add gettext py3-pygments +ADD Makefile /var/doxerlive/ +RUN make install diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..34b9ad6 --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,30 @@ +all: install build + +build: doc.tex + latexmk -c doc.tex + +watch: doc.tex + latexmk -pvc doc.tex + +DEPENDENCIES = luatex $\ + fontspec $\ + lm $\ + xcolor $\ + xcolor-solarized $\ + fontawesome $\ + xifthen $\ + ifmtarg $\ + pgf $\ + pgf-blur $\ + ec $\ + etoolbox $\ + xkeyval $\ + minted kvoptions fancyvrb fvextra upquote float ifplatform pdftexcmds xstring lineno framed catchfile + +install: + tlmgr update --self + tlmgr update texlive-scripts + tlmgr install $(DEPENDENCIES) + +clean: + latexmk -C diff --git a/doc/doc.pdf b/doc/doc.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5c6f72086e7491ba8292a3b8a6a89d54e0b733d9 GIT binary patch literal 44585 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, >=latex, line width=.5mm, rounded corners, shorten >=1pt] +\tikzstyle{smalllink}=[->, >=latex, line width=.2mm, rounded corners, shorten >=1pt, dashed, gray] + + +\begin{document} + +\section{Data flow within Spot} + +\subsection{Overview} + +\paragraph{Single source of truth.} There is a single place that is considered the source of truth for anything that is related to the app state, and that is, well, the \texttt{AppState}. The app state aggregates the state of the UI, as well as the player state. This makes it easier to keep things in sync -- when possible, anything state-related should be read from the app state over some local, possibly out-of-date state. + +\paragraph{Centralized.} That state is centralized and unique. This allows various parts of the application to access any part of it, and conversely makes it easy to perform state updates that affect various and sometimes unrelated parts of the application. + +\paragraph{Controlled mutations.} There is only one way to modify the app state, and that is by dispatching \emph{actions} -- plain structs that represent a mutation to the state. Updates to the state produce \emph{events}, which \texttt{EventListeners} can use to update the UI. + +\begin{figure}[!h] + + \centering + + \begin{tikzpicture} + + \node[box, fill=spotifygreen, minimum height=2cm] (model) at (0, 0) {\ttfamily AppModel}; + \node[box] (ui) at (-4, -3) {Gtk widgets}; + \node[box] (listeners) at (4, -3) {Listeners}; + + \draw[link] (ui) edge[bend left=20] node[above, sloped] {\footnotesize actions} (model); + \draw[link] (model) edge[bend left=10] node[above, sloped] {\footnotesize events} (listeners); + \draw[link] (listeners) edge[bend left=60] node[below, sloped] {\footnotesize update} (ui); + \draw[link, dashed] (listeners) edge[bend left=10] node[below, sloped] {\footnotesize read-only access} (model); + + \end{tikzpicture} + + \caption{The data flow and and its relation to the UI -- the \texttt{AppModel} enforces read-only access to the state.} + \label{fig:flow} + +\end{figure} + +This draws heavy inspiration from the Flux architecture\footnote{See https://facebook.github.io/flux/docs/in-depth-overview for instance}; the one big difference here is that there is no way to automatically find out which portion of the UI should be updated. Instead, listeners are responsible for figuring out the updates to apply based on the events. + +It should be noted that the app state is only readable from the main thread for simplicity. + +\subsection{How actions are handled} + +Here is the relevant part of the code\footnote{Variables have been renamed for clarity...} related to handling actions and notifying listeners: + +\begin{minted}{rust} +let events = self.model.update_state(action); + +for event in events.iter() { + for listener in self.listeners.iter_mut() { + listener.on_event(event); + } +} +\end{minted} + +That first line is the only time that the app state is borrowed mutably -- to apply actions. + +On the technical side: all actions being dispatched, synchronous or not, are eventually sent through a \texttt{futures::channel::mpsc} channel. The consumer on the other end of the channel is a future that will be executed by GLib. This allows Gtk to process \emph{all actions} at its own pace, as part of its main loop. + +Note: futures are used a lot in the code to perform asynchronous operations such as calls to the Spotify API. To ease the use of futures, the dispatcher allows working with asynchronous actions, that is, futures that output one or more actions. Again, these futures are eventually handled in the main Gtk loop. + +\subsection{A listener: the player subsystem} + +Any element that wishes to update the state or react to changes from the state has to follow that same pattern. For instance, the ``player'' part of Spot receives \texttt{Commands} (mapped from events by a \texttt{PlayerNotifier}) to start playing music, and dispatches actions back the app through a \texttt{SpotifyPlayerDelegate} (see figure \ref{fig:player}). + +These two extra elements add some indirection so that the player is not too strongly coupled to the rest of the app (it does not and should not care about most events, afterall!). Moreover, those commands are handled in a separate thread where the player lives. + +\begin{figure}[!ht] + \centering + + \begin{tikzpicture} + + \node[box, fill=spotifygreen, minimum height=2cm] (model) at (0, 0) {\ttfamily AppModel}; + + \draw[smalllink] (-5, -3) node[sizedbox] (listeners) {Components} + -- +(0, -1.5) node[right] {update} + -- +(0, -2) node[sizedbox] (ui) {Gtk widgets}; + + \draw[smalllink] (5, -3) node[sizedbox, fill=gray] (notifier) {\ttfamily PlayerNotifier} + -- +(0, -1.5) node[right] {command} + -- +(0, -2) node[sizedbox, fill=gray] (player) {\ttfamily SpotifyPlayer} + -- +(0, -3.5) node[right] {calls} + -- +(0, -4) node[sizedbox, fill=gray] (delegate) {\parbox{\textwidth}{\ttfamily SpotifyPlayer Delegate}}; + + \draw[link] (ui) edge[bend right=32] node[below, sloped] {\footnotesize actions} (model); + \draw[link] (model) edge[bend right=10] node[above, sloped] {\footnotesize events} (listeners); + + \draw[link] (model) edge[bend left=10] node[above, sloped] {\footnotesize events} (notifier); + \draw[link] (delegate) edge[bend left=32] node[below, sloped] {\footnotesize actions} (model); + + \draw[dashed, gray] ($(notifier.north west) + (-0.25, 0.25)$) rectangle ($(delegate.south east) + (0.25, -0.25)$); + + \draw[dashed, gray] ($(listeners.north west) + (-0.25, 0.25)$) rectangle ($(ui.south east) + (0.25, -0.25)$); + + + \end{tikzpicture} + + \caption{The player subsystem} + \label{fig:player} + +\end{figure} + + +\subsection{Another listener: the MPRIS subsystem} + +Similarly, the MPRIS subsystem follows that same pattern. It spawns a small DBUS server that translates DBUS messages to actions, and an \texttt{AppPlaybackStateListener} listens to incoming events. + +One major difference is that the MPRIS server maintains its own state here, since the app state cannot be accessed from outside the main thread. To make sure this local state stays in sync, DBUS messages should not alter the local state directly -- instead, we should wait for a roundtrip through the app and incoming events. + +\section{Components} + +\subsection{Overview} + +Components are thin wrappers around Gtk widgets, dedicated to binding them so that they produce the right actions, and updating them when specific events occur by conforming to \texttt{EventListener}. + +\subsection{Modeling interactions} + +Components should have some associated \texttt{struct} to model the interactions with the rest of the app. Let's consider the play/pause button as an example. Its behavior is defined in the \texttt{PlaybackModel}: + +\begin{minted}{rust} +impl PlaybackModel { + fn is_playing(&self) -> bool { /**/ } + fn toggle_playback(&self) { /**/ } +} +\end{minted} + +What we need to make our button work is a way to know its current state (is a song playing?) and a way to change that state (toggling on activation). Note that it would be tempting to simply query the widget's state, which \emph{should} be in sync with the actual playback state, but what we should really do instead is query the app state, which is the one source of truth for anything state-related. + +Why do this? First, toggling the playback might fail (e.g. if no song is playing), but more importantly something else could alter the playback state (e.g. a DBUS query). + +\begin{minted}{rust} +fn is_playing(&self) -> bool { + self.app_model.get_state().playback.is_playing() +} +\end{minted} + +As for toggling the playback, remember that we can only mutate the state through actions (the \mintinline{rust}|get_state| call above returns some \mintinline{rust}|Deref|). In other words, we express what kind of action we want to perform, with no guarantee that it'll succeed. + +\begin{minted}{rust} +fn toggle_playback(&self) { + self.dispatcher.dispatch(PlaybackAction::TogglePlay.into()); +} +\end{minted} + +\subsection{Binding the widget} + +All that's left is binding the widget to our model. By wrapping our model in an \texttt{Rc}, it becomes easy to clone it into the kind of \texttt{'static} closure Gtk needs. + +\begin{minted}{rust} +// model is an Rc +widget.connect_play_pause(clone!(@weak model => move || model.toggle_playback())); +\end{minted} + +Finally, we need our component to listen to relevant events, and update our widget accordingly. + +\begin{minted}{rust} +impl EventListener for PlaybackControl { + fn on_event(&mut self, event: &AppEvent) { + match event { + AppEvent::PlaybackEvent(PlaybackEvent::PlaybackPaused) + | AppEvent::PlaybackEvent(PlaybackEvent::PlaybackResumed) => { + let is_playing = self.model.is_playing(); + self.widget.set_playing(is_playing); + } + /**/ + } + } +} +\end{minted} + +\end{document} diff --git a/doc/enter.sh b/doc/enter.sh new file mode 100755 index 0000000..59d5ed0 --- /dev/null +++ b/doc/enter.sh @@ -0,0 +1,3 @@ +#!/bin/sh +docker build --network=host -t spot-doc . +docker run --rm -it -e THEUID="$(id -u "$USER")" -v "$PWD":/var/doxerlive spot-doc ash diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..7f725e5 --- /dev/null +++ b/flake.lock @@ -0,0 +1,112 @@ +{ + "nodes": { + "crane": { + "locked": { + "lastModified": 1731098351, + "narHash": "sha256-HQkYvKvaLQqNa10KEFGgWHfMAbWBfFp+4cAgkut+NNE=", + "owner": "ipetkov", + "repo": "crane", + "rev": "ef80ead953c1b28316cc3f8613904edc2eb90c28", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1731245184, + "narHash": "sha256-vmLS8+x+gHRv1yzj3n+GTAEObwmhxmkkukB2DwtJRdU=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "aebe249544837ce42588aa4b2e7972222ba12e8f", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1728538411, + "narHash": "sha256-f0SBJz1eZ2yOuKUr5CA9BHULGXVSn6miBuUWdTyhUhU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b69de56fac8c2b6f8fd27f2eca01dcda8e0a4221", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "crane": "crane", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1731464916, + "narHash": "sha256-WZ5rpjr/wCt7yBOUsvDE2i22hYz9g8W921jlwVktRQ4=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "2c19bad6e881b5a154cafb7f9106879b5b356d1f", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..4892b24 --- /dev/null +++ b/flake.nix @@ -0,0 +1,89 @@ +{ + description = "A GUI for yt-dlp written in Rust"; + + inputs = { + flake-utils.url = "github:numtide/flake-utils"; + nixpkgs.url = "github:nixos/nixpkgs?ref=nixpkgs-unstable"; + rust-overlay.url = "github:oxalica/rust-overlay"; + crane = { + url = "github:ipetkov/crane"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { + self, + nixpkgs, + flake-utils, + crane, + rust-overlay, + ... + }: + flake-utils.lib.eachDefaultSystem (system: + let + craneLib = (crane.mkLib nixpkgs.legacyPackages.${system}); + + pkgs = import nixpkgs { inherit system; overlays = [ rust-overlay.overlays.default ]; }; + + nativeBuildInputs = with pkgs; [ + gettext + meson + ninja + pkg-config + gtk4 # for gtk-update-icon-cache + glib # for glib-compile-schemas + desktop-file-utils + cargo + rustPlatform.cargoSetupHook + rustc + wrapGAppsHook4 + blueprint-compiler + ]; + + buildInputs = with pkgs; [ + glib + gtk4 + libadwaita + libhandy + openssl + alsa-lib + libpulseaudio + gst_all_1.gst-plugins-base + gst_all_1.gstreamer + ]; + + cargoArtifacts = craneLib.buildDepsOnly ({ + src = craneLib.cleanCargoSource (craneLib.path ./.); + inherit buildInputs nativeBuildInputs; + pname = "spot"; + }); + in with pkgs; { + packages = rec { + spot = craneLib.buildPackage { + src = craneLib.path ./.; + + inherit buildInputs nativeBuildInputs cargoArtifacts; + + }; + + default = spot; + }; + + devShells.default = mkShell { + inherit buildInputs nativeBuildInputs; + + packages = with pkgs; [ + (rust-bin.stable.latest.default.override { + extensions = [ "rust-src" "rust-analyzer" ]; + }) + meson + ninja + cargo + ]; + }; + }) // { + overlays.default = final: prev: { + inherit (self.packages.${final.system}) spot; + }; + }; +} diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..2d4e994 --- /dev/null +++ b/meson.build @@ -0,0 +1,25 @@ +project( + 'spot', + version: '0.5.0', + meson_version: '>= 0.59.0', + default_options: ['warning_level=2', 'buildtype=release'], +) + + +subdir('data') +subdir('po') +subdir('src') + +flatpak_cargo_generator = find_program(meson.project_source_root() / 'build-aux/flatpak-cargo-generator.py') + +cargo_sources = custom_target( + 'cargo-update-sources', + build_by_default: false, + output: 'cargo-sources.json', + input: meson.project_source_root() / 'Cargo.lock', + command: [ + flatpak_cargo_generator, + '@INPUT@', + '-o', '@SOURCE_ROOT@/cargo-sources.json' + ] +) diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..6ecf18f --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,2 @@ +option('offline', type: 'boolean', value: true) +option('features', type: 'string', value: '') diff --git a/po/LINGUAS b/po/LINGUAS new file mode 100644 index 0000000..c687aff --- /dev/null +++ b/po/LINGUAS @@ -0,0 +1 @@ +en ru fr nl de ca cs pl es pt tr id pt-br fi eu ia nb sl ja ar it uk bn bg et diff --git a/po/POTFILES b/po/POTFILES new file mode 100644 index 0000000..88ec00a --- /dev/null +++ b/po/POTFILES @@ -0,0 +1,47 @@ +# grep gettext src/**/*.rs | cut -d: -f1 | uniq +src/app/batch_loader.rs +src/app/components/device_selector/widget.rs +src/app/components/labels.rs +src/app/components/login/login_model.rs +src/app/components/mod.rs +src/app/components/navigation/factory.rs +src/app/components/notification/mod.rs +src/app/components/playback/playback_controls.rs +src/app/components/playback/playback_info.rs +src/app/components/selection/component.rs +src/app/components/sidebar/sidebar_item.rs +src/app/components/sidebar/sidebar.rs +src/app/components/user_menu/user_menu.rs +src/app/state/login_state.rs +src/connect/player.rs +src/main.rs + +# find src -name "*.blp" -print +src/window.blp +src/app/components/saved_playlists/saved_playlists.blp +src/app/components/artist_details/artist_details.blp +src/app/components/saved_tracks/saved_tracks.blp +src/app/components/search/search.blp +src/app/components/settings/settings.blp +src/app/components/artist/artist.blp +src/app/components/user_details/user_details.blp +src/app/components/selection/selection_toolbar.blp +src/app/components/scrolling_header/scrolling_header.blp +src/app/components/details/album_header.blp +src/app/components/details/release_details.blp +src/app/components/details/details.blp +src/app/components/now_playing/now_playing.blp +src/app/components/login/login.blp +src/app/components/playlist_details/playlist_details.blp +src/app/components/playlist_details/playlist_header.blp +src/app/components/playlist_details/playlist_headerbar.blp +src/app/components/headerbar/headerbar.blp +src/app/components/device_selector/device_selector.blp +src/app/components/sidebar/create_playlist.blp +src/app/components/sidebar/sidebar_row.blp +src/app/components/album/album.blp +src/app/components/playlist/song.blp +src/app/components/playback/playback_widget.blp +src/app/components/playback/playback_info.blp +src/app/components/playback/playback_controls.blp +src/app/components/library/library.blp diff --git a/po/ar.po b/po/ar.po new file mode 100644 index 0000000..98e1763 --- /dev/null +++ b/po/ar.po @@ -0,0 +1,393 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Spot\n" +"Language: ar\n" +"Plural-Forms: nplurals=6; plural= n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +msgid "View album" +msgstr "شاهد ألبوم" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +msgid "Copy link" +msgstr "نسخ الرابط" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.blp:36 +msgid "Add to queue" +msgstr "أضف إلى الصف" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +msgid "Remove from queue" +msgstr "أحدف من الصف" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +msgid "Add to {}" +msgstr "أضف إلى {}" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:68 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "لايمكن حفظ كلمة المرور. تحقق من أن ال Session Keyring مفتوح." + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129 +msgid "An error occured. Check logs for details!" +msgstr "حدث خطأ. تحقق من الlog لمزيد التفاصيل!" + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +msgid "Library" +msgstr "مكتبة" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +msgid "Playlists" +msgstr "قائمة أغاني" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/now_playing/now_playing_model.rs:134 +#: src/app/components/sidebar/sidebar_item.rs:43 +msgid "Now playing" +msgstr "يشتغل الأن" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.blp:33 +msgid "No song playing" +msgstr "لا توجد أغنية طور التشغيل" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +msgid "About" +msgstr "عن" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +msgid "Quit" +msgstr "غادر" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +msgid "Log out" +msgstr "تسجيل الخروج" + +#: src/app/state/login_state.rs:115 +msgid "Connection restored" +msgstr "إعادة الاتصال" + +#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.blp:26 +msgid "Top tracks" +msgstr "أفضل الأغاني" + +#. Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.blp:54 +msgid "Releases" +msgstr "أصدارات" + +#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). +#: src/app/components/login/login.blp:49 +msgid "Login to Spotify Premium" +msgstr "قم بتسجيل الدخول إلى Spotify Premium" + +#. Placeholder for the username field +#: src/app/components/login/login.blp:76 +msgid "Username" +msgstr "أسم المستخدم" + +#. Placeholder for the password field +#: src/app/components/login/login.blp:94 +msgid "Password" +msgstr "كلمة السر" + +#. This error is shown when authentication fails. +#: src/app/components/login/login.blp:116 +msgid "Authentication failed!" +msgstr "فشل عملىة التحقق!" + +#. Log in button label +#: src/app/components/login/login.blp:131 +msgid "Log in" +msgstr "تسجيل دخول" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:69 +msgid "Albums" +msgstr "ألبومات" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:100 +msgid "Artists" +msgstr "الفنانين" + +#: src/app/components/navigation/factory.rs:85 +#: src/app/components/sidebar/sidebar_item.rs:41 +msgid "Saved tracks" +msgstr "أغاني محفوظة" + +#: src/app/components/playback/playback_controls.rs:64 +msgid "Pause" +msgstr "أوقف" + +#: src/app/components/playback/playback_controls.rs:66 +msgid "Play" +msgstr "شغل" + +#: src/app/components/playback/playback_controls.blp:15 +msgid "Shuffle" +msgstr "عشوائي" + +#: src/app/components/playback/playback_controls.blp:24 +msgid "Previous" +msgstr "سابق" + +#: src/app/components/playback/playback_controls.blp:46 +msgid "Next" +msgstr "التالي" + +#: src/app/components/playback/playback_controls.blp:55 +msgid "Repeat" +msgstr "كرر" + +#. A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:26 +msgid "You have no saved playlists." +msgstr "ليس لديك قوائم تشغيل محفوظة." + +#. A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:30 +msgid "Your playlists will be shown here." +msgstr "قائمات التشغيل خاصتك ستظهر هنا." + +#. A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.blp:25 +msgid "You have no saved albums." +msgstr "ليس لديك ألبومات مسجلة." + +#. A description of what happens when the user has saved albums. +#: src/app/components/library/library.blp:29 +msgid "Your library will be shown here." +msgstr "المكتبة خاصتك ستظهر هنا." + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "{} تم تحديد صفر أغنية" +msgstr[1] "{} تم تحديد أغنية واحدة" +msgstr[2] "{} تم تحديد أغنيتان" +msgstr[3] "{} تم تحديد بعض الأغاني" +msgstr[4] "{} تم تحديد عديد الأغاني" +msgstr[5] "{} الأغاني الأخرى المحددة" + +#. Title for the empty search page (initial state). +#: src/app/components/search/search.blp:110 +msgid "Search Spotify." +msgstr "أبحث في Spotify." + +#. Subtitle for the empty search page (initial state). +#: src/app/components/search/search.blp:114 +msgid "Type to search." +msgstr "أكتب للبجث." + +#: src/app/components/selection/selection_toolbar.blp:61 +msgid "Remove" +msgstr "أحدف" + +#. playlist2-symbolic +#: src/app/components/selection/selection_toolbar.blp:49 +msgid "Add to playlist..." +msgstr "أضف إلى قائمة التشغيل..." + +#. Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.blp:53 +#: src/app/components/headerbar/headerbar.blp:49 +msgid "Cancel" +msgstr "ألغاء" + +#. Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.blp:63 +msgid "Select all" +msgstr "اختر الكل" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +msgid "More from {}" +msgstr "{} المزيد من" + +#. This refers to a music label +#: src/app/components/details/release_details.blp:41 +msgid "Label" +msgstr "الناشر" + +#. This refers to a release date +#: src/app/components/details/release_details.blp:52 +msgid "Released" +msgstr "أطلقت" + +#. This refers to a number of tracks +#: src/app/components/details/release_details.blp:63 +msgid "Tracks" +msgstr "الأغاني" + +#. This refers to the duration of a release +#: src/app/components/details/release_details.ui:68 +msgid "Duration" +msgstr "المدة" + +#: src/app/components/details/release_details.blp:72 +msgid "Copyright" +msgstr "حقوق النشر" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +msgid "{} by {}" +msgstr "{}بواسطة {}" + +#: src/app/components/sidebar/sidebar.rs:49 +msgid "Unnamed playlist" +msgstr "" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "" + +#: src/main.rs:75 +msgid "Failed to open link!" +msgstr "" + +#. Header for a group of preference items regarding audio +#: src/app/components/settings/settings.blp:13 +msgid "Audio" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:18 +msgid "Audio Backend" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:30 +msgid "ALSA Device" +msgstr "" + +#. Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.blp:34 +msgid "Applied only if audio backend is ALSA" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:44 +msgid "Audio Quality" +msgstr "" + +#: src/app/components/settings/settings.blp:47 +msgid "Normal" +msgstr "" + +#: src/app/components/settings/settings.blp:48 +msgid "High" +msgstr "" + +#: src/app/components/settings/settings.blp:49 +msgid "Very high" +msgstr "" + +#. Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.blp:72 +msgid "Appearance" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:77 +msgid "Theme" +msgstr "" + +#: src/app/components/settings/settings.blp:80 +msgid "Light" +msgstr "" + +#: src/app/components/settings/settings.blp:81 +msgid "Dark" +msgstr "" + +#. Header for a group of preference items regarding network +#: src/app/components/settings/settings.blp:91 +msgid "Network" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:96 +msgid "Access Point Port" +msgstr "" + +#. Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.blp:100 +msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine." +msgstr "" + +#: src/app/components/selection/selection_toolbar.blp:78 +msgid "Save to library" +msgstr "" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "" + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:57 +msgid "Gapless playback" +msgstr "" + +#: src/app/components/settings/settings.blp:82 +msgid "System" +msgstr "" + +#. Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.blp:69 +msgid "Done" +msgstr "" + +#. label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.blp:10 +msgid "Name" +msgstr "" + +#. Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.blp:30 +msgid "Create" +msgstr "" + diff --git a/po/bg.po b/po/bg.po new file mode 100644 index 0000000..ac8ad69 --- /dev/null +++ b/po/bg.po @@ -0,0 +1,389 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Spot\n" +"Language: bg\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +msgid "View album" +msgstr "" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +msgid "Copy link" +msgstr "" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.blp:36 +msgid "Add to queue" +msgstr "" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +msgid "Remove from queue" +msgstr "" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +msgid "Add to {}" +msgstr "" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:68 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "" + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129 +msgid "An error occured. Check logs for details!" +msgstr "" + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +msgid "Library" +msgstr "" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +msgid "Playlists" +msgstr "" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/now_playing/now_playing_model.rs:134 +#: src/app/components/sidebar/sidebar_item.rs:43 +msgid "Now playing" +msgstr "" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.blp:33 +msgid "No song playing" +msgstr "" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +msgid "About" +msgstr "" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +msgid "Quit" +msgstr "" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +msgid "Log out" +msgstr "" + +#: src/app/state/login_state.rs:115 +msgid "Connection restored" +msgstr "" + +#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.blp:26 +msgid "Top tracks" +msgstr "" + +#. Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.blp:54 +msgid "Releases" +msgstr "" + +#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). +#: src/app/components/login/login.blp:49 +msgid "Login to Spotify Premium" +msgstr "" + +#. Placeholder for the username field +#: src/app/components/login/login.blp:76 +msgid "Username" +msgstr "" + +#. Placeholder for the password field +#: src/app/components/login/login.blp:94 +msgid "Password" +msgstr "" + +#. This error is shown when authentication fails. +#: src/app/components/login/login.blp:116 +msgid "Authentication failed!" +msgstr "" + +#. Log in button label +#: src/app/components/login/login.blp:131 +msgid "Log in" +msgstr "" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:69 +msgid "Albums" +msgstr "" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:100 +msgid "Artists" +msgstr "" + +#: src/app/components/navigation/factory.rs:85 +#: src/app/components/sidebar/sidebar_item.rs:41 +msgid "Saved tracks" +msgstr "" + +#: src/app/components/playback/playback_controls.rs:64 +msgid "Pause" +msgstr "" + +#: src/app/components/playback/playback_controls.rs:66 +msgid "Play" +msgstr "" + +#: src/app/components/playback/playback_controls.blp:15 +msgid "Shuffle" +msgstr "" + +#: src/app/components/playback/playback_controls.blp:24 +msgid "Previous" +msgstr "" + +#: src/app/components/playback/playback_controls.blp:46 +msgid "Next" +msgstr "" + +#: src/app/components/playback/playback_controls.blp:55 +msgid "Repeat" +msgstr "" + +#. A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:26 +msgid "You have no saved playlists." +msgstr "" + +#. A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:30 +msgid "Your playlists will be shown here." +msgstr "" + +#. A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.blp:25 +msgid "You have no saved albums." +msgstr "" + +#. A description of what happens when the user has saved albums. +#: src/app/components/library/library.blp:29 +msgid "Your library will be shown here." +msgstr "" + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "" +msgstr[1] "" + +#. Title for the empty search page (initial state). +#: src/app/components/search/search.blp:110 +msgid "Search Spotify." +msgstr "" + +#. Subtitle for the empty search page (initial state). +#: src/app/components/search/search.blp:114 +msgid "Type to search." +msgstr "" + +#: src/app/components/selection/selection_toolbar.blp:61 +msgid "Remove" +msgstr "" + +#. playlist2-symbolic +#: src/app/components/selection/selection_toolbar.blp:49 +msgid "Add to playlist..." +msgstr "" + +#. Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.blp:53 +#: src/app/components/headerbar/headerbar.blp:49 +msgid "Cancel" +msgstr "" + +#. Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.blp:63 +msgid "Select all" +msgstr "" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +msgid "More from {}" +msgstr "" + +#. This refers to a music label +#: src/app/components/details/release_details.blp:41 +msgid "Label" +msgstr "" + +#. This refers to a release date +#: src/app/components/details/release_details.blp:52 +msgid "Released" +msgstr "" + +#. This refers to a number of tracks +#: src/app/components/details/release_details.blp:63 +msgid "Tracks" +msgstr "" + +#. This refers to the duration of a release +#: src/app/components/details/release_details.ui:68 +msgid "Duration" +msgstr "" + +#: src/app/components/details/release_details.blp:72 +msgid "Copyright" +msgstr "" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +msgid "{} by {}" +msgstr "" + +#: src/app/components/sidebar/sidebar.rs:49 +msgid "Unnamed playlist" +msgstr "" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "" + +#: src/main.rs:75 +msgid "Failed to open link!" +msgstr "" + +#. Header for a group of preference items regarding audio +#: src/app/components/settings/settings.blp:13 +msgid "Audio" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:18 +msgid "Audio Backend" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:30 +msgid "ALSA Device" +msgstr "" + +#. Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.blp:34 +msgid "Applied only if audio backend is ALSA" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:44 +msgid "Audio Quality" +msgstr "" + +#: src/app/components/settings/settings.blp:47 +msgid "Normal" +msgstr "" + +#: src/app/components/settings/settings.blp:48 +msgid "High" +msgstr "" + +#: src/app/components/settings/settings.blp:49 +msgid "Very high" +msgstr "" + +#. Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.blp:72 +msgid "Appearance" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:77 +msgid "Theme" +msgstr "" + +#: src/app/components/settings/settings.blp:80 +msgid "Light" +msgstr "" + +#: src/app/components/settings/settings.blp:81 +msgid "Dark" +msgstr "" + +#. Header for a group of preference items regarding network +#: src/app/components/settings/settings.blp:91 +msgid "Network" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:96 +msgid "Access Point Port" +msgstr "" + +#. Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.blp:100 +msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine." +msgstr "" + +#: src/app/components/selection/selection_toolbar.blp:78 +msgid "Save to library" +msgstr "" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "" + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:57 +msgid "Gapless playback" +msgstr "" + +#: src/app/components/settings/settings.blp:82 +msgid "System" +msgstr "" + +#. Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.blp:69 +msgid "Done" +msgstr "" + +#. label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.blp:10 +msgid "Name" +msgstr "" + +#. Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.blp:30 +msgid "Create" +msgstr "" + diff --git a/po/bn.po b/po/bn.po new file mode 100644 index 0000000..97735cb --- /dev/null +++ b/po/bn.po @@ -0,0 +1,438 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Spot\n" +"Language: bn\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +#, fuzzy +msgid "View album" +msgstr "অ্যালবাম দেখুন" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +#, fuzzy +msgid "Copy link" +msgstr "লিংক কপি করুন" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.blp:36 +#, fuzzy +msgid "Add to queue" +msgstr "সারিতে যোগ করুন" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +#, fuzzy +msgid "Remove from queue" +msgstr "সারি থেকে সরান" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +#, fuzzy +msgid "Add to {}" +msgstr "{} তে যোগ করুন" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:68 +#, fuzzy +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "পাসওয়ার্ড সেভ করা যায়নি। সেশন কীরিং আনলক করা আছে কিনা নিশ্চিত করুন।" + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129 +#, fuzzy +msgid "An error occured. Check logs for details!" +msgstr "একটি ত্রুটি ঘটেছে। বিস্তারিত জানতে লগ চেক করুন!" + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +#, fuzzy +msgid "Library" +msgstr "লাইব্রেরী" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +#, fuzzy +msgid "Playlists" +msgstr "প্লেলিস্ট" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/now_playing/now_playing_model.rs:134 +#: src/app/components/sidebar/sidebar_item.rs:43 +#, fuzzy +msgid "Now playing" +msgstr "এখন প্লে হচ্ছে" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.blp:33 +#, fuzzy +msgid "No song playing" +msgstr "কোনো গান প্লে হচ্ছেনা" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +#, fuzzy +msgid "About" +msgstr "অ্যাবাউট" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +#, fuzzy +msgid "Quit" +msgstr "প্রস্থান করুন" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +#, fuzzy +msgid "Log out" +msgstr "লগ আউট" + +#: src/app/state/login_state.rs:115 +#, fuzzy +msgid "Connection restored" +msgstr "সংযোগ পুনরুদ্ধার করা হয়েছে" + +#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.blp:26 +#, fuzzy +msgid "Top tracks" +msgstr "টপ ট্র্যাকগুলি" + +#. Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.blp:54 +#, fuzzy +msgid "Releases" +msgstr "রিলিজ" + +#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). +#: src/app/components/login/login.blp:49 +#, fuzzy +msgid "Login to Spotify Premium" +msgstr "স্পটিফাই প্রিমিয়ামে লগইন করুন" + +#. Placeholder for the username field +#: src/app/components/login/login.blp:76 +#, fuzzy +msgid "Username" +msgstr "ইউজারনেম" + +#. Placeholder for the password field +#: src/app/components/login/login.blp:94 +#, fuzzy +msgid "Password" +msgstr "পাসওয়ার্ড" + +#. This error is shown when authentication fails. +#: src/app/components/login/login.blp:116 +#, fuzzy +msgid "Authentication failed!" +msgstr "অথেন্টিকেশন ব্যর্থ হয়েছে!" + +#. Log in button label +#: src/app/components/login/login.blp:131 +#, fuzzy +msgid "Log in" +msgstr "লগ ইন" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:69 +#, fuzzy +msgid "Albums" +msgstr "অ্যালবাম" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:100 +#, fuzzy +msgid "Artists" +msgstr "আর্টিস্ট" + +#: src/app/components/navigation/factory.rs:85 +#: src/app/components/sidebar/sidebar_item.rs:41 +#, fuzzy +msgid "Saved tracks" +msgstr "সেভ করা ট্র্যাকগুলি" + +#: src/app/components/playback/playback_controls.rs:64 +#, fuzzy +msgid "Pause" +msgstr "পজ" + +#: src/app/components/playback/playback_controls.rs:66 +#, fuzzy +msgid "Play" +msgstr "প্লে" + +#: src/app/components/playback/playback_controls.blp:15 +#, fuzzy +msgid "Shuffle" +msgstr "শাফল" + +#: src/app/components/playback/playback_controls.blp:24 +#, fuzzy +msgid "Previous" +msgstr "পূর্ববর্তী" + +#: src/app/components/playback/playback_controls.blp:46 +#, fuzzy +msgid "Next" +msgstr "পরবর্তী" + +#: src/app/components/playback/playback_controls.blp:55 +#, fuzzy +msgid "Repeat" +msgstr "রিপিট" + +#. A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:26 +#, fuzzy +msgid "You have no saved playlists." +msgstr "আপনার কোনো সেভ করা প্লেলিস্ট নেই।" + +#. A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:30 +#, fuzzy +msgid "Your playlists will be shown here." +msgstr "আপনার প্লেলিস্ট এখানে দেখানো হবে।" + +#. A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.blp:25 +#, fuzzy +msgid "You have no saved albums." +msgstr "আপনার কোনো সেভ করা অ্যালবাম নেই।" + +#. A description of what happens when the user has saved albums. +#: src/app/components/library/library.blp:29 +#, fuzzy +msgid "Your library will be shown here." +msgstr "আপনার লাইব্রেরী এখানে দেখানো হবে।" + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +#, fuzzy +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "{} টি গান সিলেক্ট করা হয়েছে" +msgstr[1] "{} টি গান সিলেক্ট করা হয়েছে" + +#. Title for the empty search page (initial state). +#: src/app/components/search/search.blp:110 +#, fuzzy +msgid "Search Spotify." +msgstr "স্পটিফাইয়ে খুঁজুন।" + +#. Subtitle for the empty search page (initial state). +#: src/app/components/search/search.blp:114 +#, fuzzy +msgid "Type to search." +msgstr "খোঁজার জন্য এখানে টাইপ করুন।" + +#: src/app/components/selection/selection_toolbar.blp:61 +#, fuzzy +msgid "Remove" +msgstr "রিমুভ করুন" + +#. playlist2-symbolic +#: src/app/components/selection/selection_toolbar.blp:49 +#, fuzzy +msgid "Add to playlist..." +msgstr "প্লেলিস্টে যোগ করুন..." + +#. Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.blp:53 +#: src/app/components/headerbar/headerbar.blp:49 +#, fuzzy +msgid "Cancel" +msgstr "বাতিল করুন" + +#. Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.blp:63 +#, fuzzy +msgid "Select all" +msgstr "সবকিছু সিলেক্ট করুন" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +#, fuzzy +msgid "More from {}" +msgstr "{} -র থেকে আরও" + +#. This refers to a music label +#: src/app/components/details/release_details.blp:41 +#, fuzzy +msgid "Label" +msgstr "লেবেল" + +#. This refers to a release date +#: src/app/components/details/release_details.blp:52 +#, fuzzy +msgid "Released" +msgstr "রিলিজ হয়েছে" + +#. This refers to a number of tracks +#: src/app/components/details/release_details.blp:63 +#, fuzzy +msgid "Tracks" +msgstr "ট্র্যাক গুলি" + +#. This refers to the duration of a release +#: src/app/components/details/release_details.ui:68 +#, fuzzy +msgid "Duration" +msgstr "সময়" + +#: src/app/components/details/release_details.blp:72 +#, fuzzy +msgid "Copyright" +msgstr "কপিরাইট" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +#, fuzzy +msgid "{} by {}" +msgstr "{} বাই {}" + +#: src/app/components/sidebar/sidebar.rs:49 +msgid "Unnamed playlist" +msgstr "" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "" + +#: src/main.rs:75 +msgid "Failed to open link!" +msgstr "" + +#. Header for a group of preference items regarding audio +#: src/app/components/settings/settings.blp:13 +msgid "Audio" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:18 +msgid "Audio Backend" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:30 +msgid "ALSA Device" +msgstr "" + +#. Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.blp:34 +msgid "Applied only if audio backend is ALSA" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:44 +msgid "Audio Quality" +msgstr "" + +#: src/app/components/settings/settings.blp:47 +msgid "Normal" +msgstr "" + +#: src/app/components/settings/settings.blp:48 +msgid "High" +msgstr "" + +#: src/app/components/settings/settings.blp:49 +msgid "Very high" +msgstr "" + +#. Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.blp:72 +msgid "Appearance" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:77 +msgid "Theme" +msgstr "" + +#: src/app/components/settings/settings.blp:80 +msgid "Light" +msgstr "" + +#: src/app/components/settings/settings.blp:81 +msgid "Dark" +msgstr "" + +#. Header for a group of preference items regarding network +#: src/app/components/settings/settings.blp:91 +msgid "Network" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:96 +msgid "Access Point Port" +msgstr "" + +#. Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.blp:100 +msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine." +msgstr "" + +#: src/app/components/selection/selection_toolbar.blp:78 +msgid "Save to library" +msgstr "" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "" + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:57 +msgid "Gapless playback" +msgstr "" + +#: src/app/components/settings/settings.blp:82 +msgid "System" +msgstr "" + +#. Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.blp:69 +msgid "Done" +msgstr "" + +#. label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.blp:10 +msgid "Name" +msgstr "" + +#. Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.blp:30 +msgid "Create" +msgstr "" + diff --git a/po/ca.po b/po/ca.po new file mode 100644 index 0000000..6566660 --- /dev/null +++ b/po/ca.po @@ -0,0 +1,391 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Spot\n" +"Language: ca\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +msgid "View album" +msgstr "Mostra l'àlbum" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +msgid "Copy link" +msgstr "Copia l'enllaç" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.ui:39 +msgid "Add to queue" +msgstr "Afegeix a la cua" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +msgid "Remove from queue" +msgstr "Suprimeix de la cua" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +msgid "Add to {}" +msgstr "Afegeix a {}" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:56 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "No s'ha pogut desar la sessió. Assegura't que l'anell de claus de la sessió està desbloquejat." + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:91 src/app/components/mod.rs:128 +msgid "An error occured. Check logs for details!" +msgstr "Ha ocorregut un error. Comprova els registres per més detalls!" + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +msgid "Library" +msgstr "Biblioteca" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +msgid "Playlists" +msgstr "Llistes de reproducció" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/now_playing/now_playing_model.rs:134 +#: src/app/components/sidebar/sidebar_item.rs:43 +msgid "Now playing" +msgstr "S'està reproduïnt" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.ui:32 +msgid "No song playing" +msgstr "Cap cançó en reproducció" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +msgid "About" +msgstr "Quant a" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +msgid "Quit" +msgstr "Surt" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +msgid "Log out" +msgstr "Tanca la sessió" + +#: src/app/state/login_state.rs:112 +msgid "Connection restored" +msgstr "Connexió restablerta" + +#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.ui:26 +msgid "Top tracks" +msgstr "Millors pistes" + +#. Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.ui:53 +msgid "Releases" +msgstr "Llançaments" + +#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). +#: src/app/components/login/login.ui:45 +msgid "Login to Spotify Premium" +msgstr "Inicia la sessió a Spotify Premium" + +#. Placeholder for the username field +#: src/app/components/login/login.ui:72 +msgid "Username" +msgstr "Nom d'usuari" + +#. Placeholder for the password field +#: src/app/components/login/login.ui:89 +msgid "Password" +msgstr "Contrasenya" + +#. This error is shown when authentication fails. +#: src/app/components/login/login.ui:114 +msgid "Authentication failed!" +msgstr "L'autenticació ha fallat!" + +#. Log in button label +#: src/app/components/login/login.ui:129 +msgid "Log in" +msgstr "Inicia sessió" + +#. This is the title of a section of the search results +#: src/app/components/search/search.ui:72 +msgid "Albums" +msgstr "Àlbums" + +#. This is the title of a section of the search results +#: src/app/components/search/search.ui:105 +msgid "Artists" +msgstr "Artistes" + +#: src/app/components/navigation/factory.rs:85 +#: src/app/components/sidebar/sidebar_item.rs:41 +msgid "Saved tracks" +msgstr "Pistes desades" + +#: src/app/components/playback/playback_controls.rs:64 +msgid "Pause" +msgstr "Pausa" + +#: src/app/components/playback/playback_controls.rs:66 +msgid "Play" +msgstr "Reprodueix" + +#: src/app/components/playback/playback_controls.ui:17 +msgid "Shuffle" +msgstr "Barreja" + +#: src/app/components/playback/playback_controls.ui:27 +msgid "Previous" +msgstr "Anterior" + +#: src/app/components/playback/playback_controls.ui:50 +msgid "Next" +msgstr "Següent" + +#: src/app/components/playback/playback_controls.ui:60 +msgid "Repeat" +msgstr "Repeteix" + +#. A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.ui:26 +msgid "You have no saved playlists." +msgstr "No teniu llistes de reproducció desades." + +#. A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.ui:27 +msgid "Your playlists will be shown here." +msgstr "Les vostres llistes de reproducció es mostraran aquí." + +#. A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.ui:26 +msgid "You have no saved albums." +msgstr "No teniu àlbums desats." + +#. A description of what happens when the user has saved albums. +#: src/app/components/library/library.ui:27 +msgid "Your library will be shown here." +msgstr "Aquí es mostrarà la vostra biblioteca." + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "{} cançó seleccionada" +msgstr[1] "{} cançons seleccionades" + +#. Title for the empty search page (initial state). +#: src/app/components/search/search.ui:116 +msgid "Search Spotify." +msgstr "Cerca a Spotify." + +#. Subtitle for the empty search page (initial state). +#: src/app/components/search/search.ui:117 +msgid "Type to search." +msgstr "Escriu per començar a buscar." + +#: src/app/components/selection/selection_toolbar.ui:69 +msgid "Remove" +msgstr "Elimina" + +#. playlist2-symbolic +#: src/app/components/selection/selection_toolbar.ui:56 +msgid "Add to playlist..." +msgstr "Afegeix a la llista..." + +#. Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.ui:50 +#: src/app/components/headerbar/headerbar.ui:47 +msgid "Cancel" +msgstr "Cancel·la" + +#. Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.ui:58 +msgid "Select all" +msgstr "Selecciona-ho tot" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +msgid "More from {}" +msgstr "Més de {}" + +#. This refers to a music label +#: src/app/components/details/release_details.ui:38 +msgid "Label" +msgstr "Etiqueta" + +#. This refers to a release date +#: src/app/components/details/release_details.ui:48 +msgid "Released" +msgstr "Llançaments" + +#. This refers to a number of tracks +#: src/app/components/details/release_details.ui:58 +msgid "Tracks" +msgstr "Millors pistes" + +#. This refers to the duration of a release +#: src/app/components/details/release_details.ui:68 +msgid "Duration" +msgstr "Duració" + +#: src/app/components/details/release_details.ui:68 +msgid "Copyright" +msgstr "Copyright" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +msgid "{} by {}" +msgstr "{} per {}" + +#: src/app/components/sidebar/sidebar.rs:48 +msgid "Unnamed playlist" +msgstr "Llista sense nom" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "Totes les llistes" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "Cançons desades!" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "Preferències" + +#: src/main.rs:80 +msgid "Failed to open link!" +msgstr "No s'ha pogut obrir l'enllaç!" + +#. Header for a group of preference items regarding audio +#: src/app/components/settings/settings.ui:13 +msgid "Audio" +msgstr "Àudio" + +#. Title for an item in preferences +#: src/app/components/settings/settings.ui:16 +msgid "Audio Backend" +msgstr "Sistema d'àudio" + +#. Title for an item in preferences +#: src/app/components/settings/settings.ui:29 +msgid "ALSA Device" +msgstr "Dispositiu ALSA" + +#. Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.ui:30 +msgid "Applied only if audio backend is ALSA" +msgstr "Usat només si el sistema d'àudio és l'ALSA" + +#. Title for an item in preferences +#: src/app/components/settings/settings.ui:40 +msgid "Audio Quality" +msgstr "Qualitat de l'àudio" + +#: src/app/components/settings/settings.ui:44 +msgid "Normal" +msgstr "Normal" + +#. Qualitat d'àudio -> femení, la qualitat +#: src/app/components/settings/settings.ui:45 +msgid "High" +msgstr "Alta" + +#. El mateix que amb "High". Cal usar el femení +#: src/app/components/settings/settings.ui:46 +msgid "Very high" +msgstr "Molt alta" + +#. Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.ui:70 +msgid "Appearance" +msgstr "Aparença" + +#. Title for an item in preferences +#: src/app/components/settings/settings.ui:73 +msgid "Theme" +msgstr "Tema" + +#: src/app/components/settings/settings.ui:77 +msgid "Light" +msgstr "Clar" + +#: src/app/components/settings/settings.ui:78 +msgid "Dark" +msgstr "Fosc" + +#. Header for a group of preference items regarding network +#: src/app/components/settings/settings.ui:89 +msgid "Network" +msgstr "Xarxa" + +#. Title for an item in preferences +#: src/app/components/settings/settings.ui:92 +msgid "Access Point Port" +msgstr "Port del punt d'accés" + +#. Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.ui:93 +msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine." +msgstr "El port usat per les connexions al punt d'accés de l'Spotify. Establiu-ho a 0 si qualsevol port és correcte." + +#: src/app/components/selection/selection_toolbar.ui:90 +msgid "Save to library" +msgstr "Desa-ho a la llibreria" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "" + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.ui:54 +msgid "Gapless playback" +msgstr "" + +#: src/app/components/settings/settings.ui:79 +msgid "System" +msgstr "" + +#. Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.ui:63 +msgid "Done" +msgstr "" + +#. label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.ui:10 +msgid "Name" +msgstr "" + +#. Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.ui:33 +msgid "Create" +msgstr "" + diff --git a/po/cs.po b/po/cs.po new file mode 100644 index 0000000..bbcfd43 --- /dev/null +++ b/po/cs.po @@ -0,0 +1,393 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Spot\n" +"Language: cs\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +msgid "View album" +msgstr "Zobrazit album" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +msgid "Copy link" +msgstr "Zkopírovat odkaz" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.blp:36 +msgid "Add to queue" +msgstr "Přidat do fronty" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +msgid "Remove from queue" +msgstr "Odebrat z fronty" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +msgid "Add to {}" +msgstr "Přidat do {}" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:68 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "Heslo nelze uložit. Zkontrolujte, že je klíčenka (session keyring) odemčená." + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129 +msgid "An error occured. Check logs for details!" +msgstr "Došlo k chybě. Podrobnosti najdete v protokolu." + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +msgid "Library" +msgstr "Knihovna" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +msgid "Playlists" +msgstr "Playlisty" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/now_playing/now_playing_model.rs:134 +#: src/app/components/sidebar/sidebar_item.rs:43 +msgid "Now playing" +msgstr "Právě hraje" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.blp:33 +msgid "No song playing" +msgstr "Nic nehraje" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +msgid "About" +msgstr "O aplikaci" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +msgid "Quit" +msgstr "Konec" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +msgid "Log out" +msgstr "Odhlásit se" + +#: src/app/state/login_state.rs:115 +msgid "Connection restored" +msgstr "Připojení obnoveno" + +#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.blp:26 +msgid "Top tracks" +msgstr "Populární" + +#. Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.blp:54 +msgid "Releases" +msgstr "Diskografie" + +#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). +#: src/app/components/login/login.blp:49 +msgid "Login to Spotify Premium" +msgstr "Přihlásit se ke Spotify Premium" + +#. Placeholder for the username field +#: src/app/components/login/login.blp:76 +msgid "Username" +msgstr "Uživatelské jméno" + +#. Placeholder for the password field +#: src/app/components/login/login.blp:94 +msgid "Password" +msgstr "Heslo" + +#. This error is shown when authentication fails. +#: src/app/components/login/login.blp:116 +msgid "Authentication failed!" +msgstr "Chyba autentizace!" + +#. Log in button label +#: src/app/components/login/login.blp:131 +msgid "Log in" +msgstr "Přihlásit se" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:69 +msgid "Albums" +msgstr "Alba" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:100 +msgid "Artists" +msgstr "Umělci" + +#. Where is this text displayed? +#. I see it is supposed to be in `src/app/components/navigation/home.rs:47`, but that does not seem to be the case: https://github.com/xou816/spot/blob/1b96cd07129f27c7f3f3a2e66299c44a52e489d4/src/app/components/navigation/home.rs +#. Maybe it is something that has not been commited yet (2021-10-02T10:42:52+02:00), but @xou816 has it locally and has generated pot / po files from it in https://github.com/xou816/spot/commit/86414c03d207cdf4e2c1cbddd47c7ca5629d1303 +#: src/app/components/navigation/factory.rs:85 +#: src/app/components/sidebar/sidebar_item.rs:41 +msgid "Saved tracks" +msgstr "Uložené skladby" + +#: src/app/components/playback/playback_controls.rs:64 +msgid "Pause" +msgstr "Pozastavit" + +#: src/app/components/playback/playback_controls.rs:66 +msgid "Play" +msgstr "Přehrát" + +#: src/app/components/playback/playback_controls.blp:15 +msgid "Shuffle" +msgstr "Náhodné přehrávání" + +#: src/app/components/playback/playback_controls.blp:24 +msgid "Previous" +msgstr "Předchozí" + +#: src/app/components/playback/playback_controls.blp:46 +msgid "Next" +msgstr "Následující" + +#: src/app/components/playback/playback_controls.blp:55 +msgid "Repeat" +msgstr "Opakovat" + +#. A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:26 +msgid "You have no saved playlists." +msgstr "Nemáte žádné uložené playlisty." + +#. A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:30 +msgid "Your playlists will be shown here." +msgstr "Tady budou zobrazeny vaše playlisty." + +#. A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.blp:25 +msgid "You have no saved albums." +msgstr "Nemáte žádná uložená alba." + +#. A description of what happens when the user has saved albums. +#: src/app/components/library/library.blp:29 +msgid "Your library will be shown here." +msgstr "Tady bude zobrazena vaše knihovna." + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "{} skladba vybrána" +msgstr[1] "{} skladby vybrány" +msgstr[2] "{} skladeb vybráno" + +#. Title for the empty search page (initial state). +#: src/app/components/search/search.blp:110 +msgid "Search Spotify." +msgstr "Hledat na Spotify." + +#. Subtitle for the empty search page (initial state). +#: src/app/components/search/search.blp:114 +msgid "Type to search." +msgstr "Zadejte a vyhledejte." + +#: src/app/components/selection/selection_toolbar.blp:61 +msgid "Remove" +msgstr "Odstranit" + +#. playlist2-symbolic +#: src/app/components/selection/selection_toolbar.blp:49 +msgid "Add to playlist..." +msgstr "Přidat do playlistu..." + +#. Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.blp:53 +#: src/app/components/headerbar/headerbar.blp:49 +msgid "Cancel" +msgstr "Zrušit" + +#. Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.blp:63 +msgid "Select all" +msgstr "Vybrat vše" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +msgid "More from {}" +msgstr "Více od {}" + +#. This refers to a music label +#: src/app/components/details/release_details.blp:41 +msgid "Label" +msgstr "Vydavatelství" + +#. This refers to a release date +#: src/app/components/details/release_details.blp:52 +msgid "Released" +msgstr "Vydáno" + +#. This refers to a number of tracks +#: src/app/components/details/release_details.blp:63 +msgid "Tracks" +msgstr "Počet skladeb" + +#. This refers to the duration of a release +#: src/app/components/details/release_details.ui:68 +msgid "Duration" +msgstr "Délka" + +#: src/app/components/details/release_details.blp:72 +msgid "Copyright" +msgstr "Copyright" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +msgid "{} by {}" +msgstr "{} od {}" + +#: src/app/components/sidebar/sidebar.rs:49 +msgid "Unnamed playlist" +msgstr "Playlist bez názvu" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "Všechny playlisty" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "Skladby uloženy!" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "Nastavení" + +#: src/main.rs:75 +msgid "Failed to open link!" +msgstr "Nepodařilo se otevřít odkaz!" + +#. Header for a group of preference items regarding audio +#: src/app/components/settings/settings.blp:13 +msgid "Audio" +msgstr "Zvuk" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:18 +msgid "Audio Backend" +msgstr "Zvukový backend" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:30 +msgid "ALSA Device" +msgstr "ALSA zařízení" + +#. Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.blp:34 +msgid "Applied only if audio backend is ALSA" +msgstr "Použito pouze pokud zvukový backend je ALSA" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:44 +msgid "Audio Quality" +msgstr "Kvalita zvuku" + +#: src/app/components/settings/settings.blp:47 +msgid "Normal" +msgstr "Normální" + +#: src/app/components/settings/settings.blp:48 +msgid "High" +msgstr "Vysoká" + +#: src/app/components/settings/settings.blp:49 +msgid "Very high" +msgstr "Velmi vysoká" + +#. Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.blp:72 +msgid "Appearance" +msgstr "Vzhled" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:77 +msgid "Theme" +msgstr "Téma" + +#: src/app/components/settings/settings.blp:80 +msgid "Light" +msgstr "Světlé" + +#: src/app/components/settings/settings.blp:81 +msgid "Dark" +msgstr "Tmavé" + +#. Header for a group of preference items regarding network +#: src/app/components/settings/settings.blp:91 +msgid "Network" +msgstr "Síť" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:96 +msgid "Access Point Port" +msgstr "Port Access Pointu" + +#. Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.blp:100 +msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine." +msgstr "Port používaný pro připojení ke Spotify Access Point. Nastavte na 0 pokud lze použít libovolný port." + +#: src/app/components/selection/selection_toolbar.blp:78 +msgid "Save to library" +msgstr "Uložit do knihovny" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "Playlist byl vytvořen." + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "Zobrazit" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "Přidat playlist" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:57 +msgid "Gapless playback" +msgstr "Přehrávání bez mezer" + +#: src/app/components/settings/settings.blp:82 +msgid "System" +msgstr "Systém" + +#. Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.blp:69 +msgid "Done" +msgstr "Hotovo" + +#. label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.blp:10 +msgid "Name" +msgstr "Název" + +#. Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.blp:30 +msgid "Create" +msgstr "Vytvořit" + diff --git a/po/de.po b/po/de.po new file mode 100644 index 0000000..2478b8f --- /dev/null +++ b/po/de.po @@ -0,0 +1,389 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Spot\n" +"Language: de\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +msgid "View album" +msgstr "Album ansehen" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +msgid "Copy link" +msgstr "Link kopieren" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.blp:36 +msgid "Add to queue" +msgstr "Zur Warteschlange hinzufügen" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +msgid "Remove from queue" +msgstr "Von Warteschlange entfernen" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +msgid "Add to {}" +msgstr "Zu {} hinzufügen" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:68 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "Das Passwort konnte nicht gespeichert werden. Stellen Sie sicher, dass der Sitzungs-Schlüsselbund entsperrt ist." + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129 +msgid "An error occured. Check logs for details!" +msgstr "Es ist ein Fehler aufgetreten. Überprüfen Sie die Protokolle für Details!" + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +msgid "Library" +msgstr "Bibliothek" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +msgid "Playlists" +msgstr "Playlists" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/now_playing/now_playing_model.rs:134 +#: src/app/components/sidebar/sidebar_item.rs:43 +msgid "Now playing" +msgstr "Wiedergabe" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.blp:33 +msgid "No song playing" +msgstr "Keine Wiedergabe" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +msgid "About" +msgstr "Info" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +msgid "Quit" +msgstr "Beenden" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +msgid "Log out" +msgstr "Abmelden" + +#: src/app/state/login_state.rs:115 +msgid "Connection restored" +msgstr "Verbindung wiederhergestellt" + +#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.blp:26 +msgid "Top tracks" +msgstr "Beliebte Titel" + +#. Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.blp:54 +msgid "Releases" +msgstr "Diskografie" + +#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). +#: src/app/components/login/login.blp:49 +msgid "Login to Spotify Premium" +msgstr "Bei Spotify Premium anmelden" + +#. Placeholder for the username field +#: src/app/components/login/login.blp:76 +msgid "Username" +msgstr "Benutzername" + +#. Placeholder for the password field +#: src/app/components/login/login.blp:94 +msgid "Password" +msgstr "Passwort" + +#. This error is shown when authentication fails. +#: src/app/components/login/login.blp:116 +msgid "Authentication failed!" +msgstr "Anmeldung fehlgeschlagen!" + +#. Log in button label +#: src/app/components/login/login.blp:131 +msgid "Log in" +msgstr "Anmelden" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:69 +msgid "Albums" +msgstr "Alben" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:100 +msgid "Artists" +msgstr "Künstler" + +#: src/app/components/navigation/factory.rs:85 +#: src/app/components/sidebar/sidebar_item.rs:41 +msgid "Saved tracks" +msgstr "Gespeicherte Titel" + +#: src/app/components/playback/playback_controls.rs:64 +msgid "Pause" +msgstr "Pause" + +#: src/app/components/playback/playback_controls.rs:66 +msgid "Play" +msgstr "Play" + +#: src/app/components/playback/playback_controls.blp:15 +msgid "Shuffle" +msgstr "Shuffle" + +#: src/app/components/playback/playback_controls.blp:24 +msgid "Previous" +msgstr "Zurück" + +#: src/app/components/playback/playback_controls.blp:46 +msgid "Next" +msgstr "Weiter" + +#: src/app/components/playback/playback_controls.blp:55 +msgid "Repeat" +msgstr "Wiederholen" + +#. A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:26 +msgid "You have no saved playlists." +msgstr "Keine gespeicherten Playlists vorhanden." + +#. A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:30 +msgid "Your playlists will be shown here." +msgstr "Playlists werden hier angezeigt." + +#. A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.blp:25 +msgid "You have no saved albums." +msgstr "Keine gespeicherten Alben vorhanden." + +#. A description of what happens when the user has saved albums. +#: src/app/components/library/library.blp:29 +msgid "Your library will be shown here." +msgstr "Die Bibliothek wird hier angezeigt." + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "{} Titel ausgewählt" +msgstr[1] "{} Titel ausgewählt" + +#. Title for the empty search page (initial state). +#: src/app/components/search/search.blp:110 +msgid "Search Spotify." +msgstr "Auf Spotify suchen." + +#. Subtitle for the empty search page (initial state). +#: src/app/components/search/search.blp:114 +msgid "Type to search." +msgstr "Tippe um zu suchen." + +#: src/app/components/selection/selection_toolbar.blp:61 +msgid "Remove" +msgstr "Entfernen" + +#. playlist2-symbolic +#: src/app/components/selection/selection_toolbar.blp:49 +msgid "Add to playlist..." +msgstr "Zur Playlist hinzufügen..." + +#. Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.blp:53 +#: src/app/components/headerbar/headerbar.blp:49 +msgid "Cancel" +msgstr "Abbrechen" + +#. Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.blp:63 +msgid "Select all" +msgstr "Alle auswählen" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +msgid "More from {}" +msgstr "Mehr von {}" + +#. This refers to a music label +#: src/app/components/details/release_details.blp:41 +msgid "Label" +msgstr "Label" + +#. This refers to a release date +#: src/app/components/details/release_details.blp:52 +msgid "Released" +msgstr "Veröffentlicht" + +#. This refers to a number of tracks +#: src/app/components/details/release_details.blp:63 +msgid "Tracks" +msgstr "Titel" + +#. This refers to the duration of a release +#: src/app/components/details/release_details.ui:68 +msgid "Duration" +msgstr "Dauer" + +#: src/app/components/details/release_details.blp:72 +msgid "Copyright" +msgstr "Copyright" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +msgid "{} by {}" +msgstr "{} von {}" + +#: src/app/components/sidebar/sidebar.rs:49 +msgid "Unnamed playlist" +msgstr "Unbenannte Playlist" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "Alle Playlists" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "Titel gespeichert!" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "Einstellungen" + +#: src/main.rs:75 +msgid "Failed to open link!" +msgstr "Öffnen des Links fehlgeschlagen!" + +#. Header for a group of preference items regarding audio +#: src/app/components/settings/settings.blp:13 +msgid "Audio" +msgstr "Audio" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:18 +msgid "Audio Backend" +msgstr "Audio Backend" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:30 +msgid "ALSA Device" +msgstr "ALSA Gerät" + +#. Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.blp:34 +msgid "Applied only if audio backend is ALSA" +msgstr "Wird nur angewendet, wenn das Audio-Backend ALSA ist" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:44 +msgid "Audio Quality" +msgstr "Audioqualität" + +#: src/app/components/settings/settings.blp:47 +msgid "Normal" +msgstr "Normal" + +#: src/app/components/settings/settings.blp:48 +msgid "High" +msgstr "Hoch" + +#: src/app/components/settings/settings.blp:49 +msgid "Very high" +msgstr "Sehr hoch" + +#. Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.blp:72 +msgid "Appearance" +msgstr "Aussehen" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:77 +msgid "Theme" +msgstr "Thema" + +#: src/app/components/settings/settings.blp:80 +msgid "Light" +msgstr "Hell" + +#: src/app/components/settings/settings.blp:81 +msgid "Dark" +msgstr "Dunkel" + +#. Header for a group of preference items regarding network +#: src/app/components/settings/settings.blp:91 +msgid "Network" +msgstr "Netzwerk" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:96 +msgid "Access Point Port" +msgstr "Access Point Port" + +#. Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.blp:100 +msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine." +msgstr "Port, der für Verbindungen mit dem Spotify Access Point verwendet wird. Setze den Wert auf 0 für einen beliebigen Port." + +#: src/app/components/selection/selection_toolbar.blp:78 +msgid "Save to library" +msgstr "In Bibliothek speichern" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "Neue Playlist erstellt." + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "Anzeigen" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "Neue Playlist" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:57 +msgid "Gapless playback" +msgstr "Lückenlose Wiedergabe" + +#: src/app/components/settings/settings.blp:82 +msgid "System" +msgstr "System" + +#. Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.blp:69 +msgid "Done" +msgstr "Fertig" + +#. label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.blp:10 +msgid "Name" +msgstr "Name" + +#. Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.blp:30 +msgid "Create" +msgstr "Erstellen" + diff --git a/po/en.po b/po/en.po new file mode 100644 index 0000000..f74849d --- /dev/null +++ b/po/en.po @@ -0,0 +1,389 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Spot\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +msgid "View album" +msgstr "View album" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +msgid "Copy link" +msgstr "Copy link" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.blp:36 +msgid "Add to queue" +msgstr "Add to queue" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +msgid "Remove from queue" +msgstr "Remove from queue" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +msgid "Add to {}" +msgstr "Add to {}" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:68 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "Could not save password. Make sure the session keyring is unlocked." + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129 +msgid "An error occured. Check logs for details!" +msgstr "An error occured. Check logs for details!" + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +msgid "Library" +msgstr "Library" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +msgid "Playlists" +msgstr "Playlists" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/now_playing/now_playing_model.rs:134 +#: src/app/components/sidebar/sidebar_item.rs:43 +msgid "Now playing" +msgstr "Now playing" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.blp:33 +msgid "No song playing" +msgstr "No song playing" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +msgid "About" +msgstr "About" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +msgid "Quit" +msgstr "Quit" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +msgid "Log out" +msgstr "Log out" + +#: src/app/state/login_state.rs:115 +msgid "Connection restored" +msgstr "Connection restored" + +#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.blp:26 +msgid "Top tracks" +msgstr "Top tracks" + +#. Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.blp:54 +msgid "Releases" +msgstr "Releases" + +#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). +#: src/app/components/login/login.blp:49 +msgid "Login to Spotify Premium" +msgstr "Login to Spotify Premium" + +#. Placeholder for the username field +#: src/app/components/login/login.blp:76 +msgid "Username" +msgstr "Username" + +#. Placeholder for the password field +#: src/app/components/login/login.blp:94 +msgid "Password" +msgstr "Password" + +#. This error is shown when authentication fails. +#: src/app/components/login/login.blp:116 +msgid "Authentication failed!" +msgstr "Authentication failed!" + +#. Log in button label +#: src/app/components/login/login.blp:131 +msgid "Log in" +msgstr "Log in" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:69 +msgid "Albums" +msgstr "Albums" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:100 +msgid "Artists" +msgstr "Artists" + +#: src/app/components/navigation/factory.rs:85 +#: src/app/components/sidebar/sidebar_item.rs:41 +msgid "Saved tracks" +msgstr "Saved Tracks" + +#: src/app/components/playback/playback_controls.rs:64 +msgid "Pause" +msgstr "Pause" + +#: src/app/components/playback/playback_controls.rs:66 +msgid "Play" +msgstr "Play" + +#: src/app/components/playback/playback_controls.blp:15 +msgid "Shuffle" +msgstr "Shuffle" + +#: src/app/components/playback/playback_controls.blp:24 +msgid "Previous" +msgstr "Previous" + +#: src/app/components/playback/playback_controls.blp:46 +msgid "Next" +msgstr "Next" + +#: src/app/components/playback/playback_controls.blp:55 +msgid "Repeat" +msgstr "Repeat" + +#. A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:26 +msgid "You have no saved playlists." +msgstr "You have no saved playlists." + +#. A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:30 +msgid "Your playlists will be shown here." +msgstr "Your playlists will be shown here." + +#. A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.blp:25 +msgid "You have no saved albums." +msgstr "You have no saved albums." + +#. A description of what happens when the user has saved albums. +#: src/app/components/library/library.blp:29 +msgid "Your library will be shown here." +msgstr "Your library will be shown here." + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "{} song selected" +msgstr[1] "{} songs selected" + +#. Title for the empty search page (initial state). +#: src/app/components/search/search.blp:110 +msgid "Search Spotify." +msgstr "Search Spotify." + +#. Subtitle for the empty search page (initial state). +#: src/app/components/search/search.blp:114 +msgid "Type to search." +msgstr "Type to search." + +#: src/app/components/selection/selection_toolbar.blp:61 +msgid "Remove" +msgstr "Remove" + +#. playlist2-symbolic +#: src/app/components/selection/selection_toolbar.blp:49 +msgid "Add to playlist..." +msgstr "Add to playlist..." + +#. Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.blp:53 +#: src/app/components/headerbar/headerbar.blp:49 +msgid "Cancel" +msgstr "Cancel" + +#. Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.blp:63 +msgid "Select all" +msgstr "Select all" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +msgid "More from {}" +msgstr "More from {}" + +#. This refers to a music label +#: src/app/components/details/release_details.blp:41 +msgid "Label" +msgstr "Label" + +#. This refers to a release date +#: src/app/components/details/release_details.blp:52 +msgid "Released" +msgstr "Released" + +#. This refers to a number of tracks +#: src/app/components/details/release_details.blp:63 +msgid "Tracks" +msgstr "Tracks" + +#. This refers to the duration of a release +#: src/app/components/details/release_details.ui:68 +msgid "Duration" +msgstr "Duration" + +#: src/app/components/details/release_details.blp:72 +msgid "Copyright" +msgstr "Copyright" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +msgid "{} by {}" +msgstr "{} by {}" + +#: src/app/components/sidebar/sidebar.rs:49 +msgid "Unnamed playlist" +msgstr "Unnamed playlist" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "" + +#: src/main.rs:75 +msgid "Failed to open link!" +msgstr "" + +#. Header for a group of preference items regarding audio +#: src/app/components/settings/settings.blp:13 +msgid "Audio" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:18 +msgid "Audio Backend" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:30 +msgid "ALSA Device" +msgstr "" + +#. Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.blp:34 +msgid "Applied only if audio backend is ALSA" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:44 +msgid "Audio Quality" +msgstr "" + +#: src/app/components/settings/settings.blp:47 +msgid "Normal" +msgstr "" + +#: src/app/components/settings/settings.blp:48 +msgid "High" +msgstr "" + +#: src/app/components/settings/settings.blp:49 +msgid "Very high" +msgstr "" + +#. Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.blp:72 +msgid "Appearance" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:77 +msgid "Theme" +msgstr "" + +#: src/app/components/settings/settings.blp:80 +msgid "Light" +msgstr "" + +#: src/app/components/settings/settings.blp:81 +msgid "Dark" +msgstr "" + +#. Header for a group of preference items regarding network +#: src/app/components/settings/settings.blp:91 +msgid "Network" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:96 +msgid "Access Point Port" +msgstr "" + +#. Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.blp:100 +msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine." +msgstr "" + +#: src/app/components/selection/selection_toolbar.blp:78 +msgid "Save to library" +msgstr "" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "" + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:57 +msgid "Gapless playback" +msgstr "" + +#: src/app/components/settings/settings.blp:82 +msgid "System" +msgstr "" + +#. Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.blp:69 +msgid "Done" +msgstr "" + +#. label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.blp:10 +msgid "Name" +msgstr "" + +#. Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.blp:30 +msgid "Create" +msgstr "" + diff --git a/po/es.po b/po/es.po new file mode 100644 index 0000000..8503368 --- /dev/null +++ b/po/es.po @@ -0,0 +1,389 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Spot\n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +msgid "View album" +msgstr "Ver álbum" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +msgid "Copy link" +msgstr "Copiar enlace" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.blp:36 +msgid "Add to queue" +msgstr "Añadir a la cola" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +msgid "Remove from queue" +msgstr "Borrar de la lista" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +msgid "Add to {}" +msgstr "Añadir a {}" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:68 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "No se pudo guardar la contraseña. Asegúrate de que el anillo de claves esta desbloqueado." + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129 +msgid "An error occured. Check logs for details!" +msgstr "Ha ocurrido un error. ¡Revisa los logs para más detalles!" + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +msgid "Library" +msgstr "Biblioteca" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +msgid "Playlists" +msgstr "Lista de reproducción" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/now_playing/now_playing_model.rs:134 +#: src/app/components/sidebar/sidebar_item.rs:43 +msgid "Now playing" +msgstr "Reproduciendo ahora" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.blp:33 +msgid "No song playing" +msgstr "Ninguna canción en reproducción" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +msgid "About" +msgstr "Acerca de" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +msgid "Quit" +msgstr "Salir" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +msgid "Log out" +msgstr "Cerrar sesión" + +#: src/app/state/login_state.rs:115 +msgid "Connection restored" +msgstr "Conexión restaurada" + +#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.blp:26 +msgid "Top tracks" +msgstr "Mejores canciones" + +#. Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.blp:54 +msgid "Releases" +msgstr "Lanzamientos" + +#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). +#: src/app/components/login/login.blp:49 +msgid "Login to Spotify Premium" +msgstr "Iniciar sesión en Spotify Premium" + +#. Placeholder for the username field +#: src/app/components/login/login.blp:76 +msgid "Username" +msgstr "Usuario" + +#. Placeholder for the password field +#: src/app/components/login/login.blp:94 +msgid "Password" +msgstr "Contraseña" + +#. This error is shown when authentication fails. +#: src/app/components/login/login.blp:116 +msgid "Authentication failed!" +msgstr "¡Autenticación fallida!" + +#. Log in button label +#: src/app/components/login/login.blp:131 +msgid "Log in" +msgstr "Iniciar sesión" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:69 +msgid "Albums" +msgstr "Álbumes" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:100 +msgid "Artists" +msgstr "Artistas" + +#: src/app/components/navigation/factory.rs:85 +#: src/app/components/sidebar/sidebar_item.rs:41 +msgid "Saved tracks" +msgstr "Pistas guardadas" + +#: src/app/components/playback/playback_controls.rs:64 +msgid "Pause" +msgstr "Pausa" + +#: src/app/components/playback/playback_controls.rs:66 +msgid "Play" +msgstr "Reproducir" + +#: src/app/components/playback/playback_controls.blp:15 +msgid "Shuffle" +msgstr "Activar aleatorio" + +#: src/app/components/playback/playback_controls.blp:24 +msgid "Previous" +msgstr "Anterior" + +#: src/app/components/playback/playback_controls.blp:46 +msgid "Next" +msgstr "Siguiente" + +#: src/app/components/playback/playback_controls.blp:55 +msgid "Repeat" +msgstr "Activar repetir" + +#. A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:26 +msgid "You have no saved playlists." +msgstr "No tienes listas de reproducción guardadas." + +#. A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:30 +msgid "Your playlists will be shown here." +msgstr "Tus listas de reproducción se mostrarán aquí." + +#. A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.blp:25 +msgid "You have no saved albums." +msgstr "No tienes álbumes guardados." + +#. A description of what happens when the user has saved albums. +#: src/app/components/library/library.blp:29 +msgid "Your library will be shown here." +msgstr "Tu biblioteca se mostrará aquí." + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "{} canción seleccionada" +msgstr[1] "{} canciones seleccionadas" + +#. Title for the empty search page (initial state). +#: src/app/components/search/search.blp:110 +msgid "Search Spotify." +msgstr "Buscar en Spotify." + +#. Subtitle for the empty search page (initial state). +#: src/app/components/search/search.blp:114 +msgid "Type to search." +msgstr "Escribe para buscar." + +#: src/app/components/selection/selection_toolbar.blp:61 +msgid "Remove" +msgstr "Retirar" + +#. playlist2-symbolic +#: src/app/components/selection/selection_toolbar.blp:49 +msgid "Add to playlist..." +msgstr "Añadir a la lista de reproducción..." + +#. Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.blp:53 +#: src/app/components/headerbar/headerbar.blp:49 +msgid "Cancel" +msgstr "Cancelar" + +#. Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.blp:63 +msgid "Select all" +msgstr "Seleccionar todo" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +msgid "More from {}" +msgstr "Más de {}" + +#. This refers to a music label +#: src/app/components/details/release_details.blp:41 +msgid "Label" +msgstr "Etiqueta" + +#. This refers to a release date +#: src/app/components/details/release_details.blp:52 +msgid "Released" +msgstr "Lanzado" + +#. This refers to a number of tracks +#: src/app/components/details/release_details.blp:63 +msgid "Tracks" +msgstr "Canciones" + +#. This refers to the duration of a release +#: src/app/components/details/release_details.ui:68 +msgid "Duration" +msgstr "Duración" + +#: src/app/components/details/release_details.blp:72 +msgid "Copyright" +msgstr "Copyright" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +msgid "{} by {}" +msgstr "{} de {}" + +#: src/app/components/sidebar/sidebar.rs:49 +msgid "Unnamed playlist" +msgstr "Lista de reproducción sin nombre" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "Todas las listas de reproducción" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "¡Pistas guardadas!" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "Preferencias" + +#: src/main.rs:75 +msgid "Failed to open link!" +msgstr "¡No se ha podido abrir el enlace!" + +#. Header for a group of preference items regarding audio +#: src/app/components/settings/settings.blp:13 +msgid "Audio" +msgstr "Sonido" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:18 +msgid "Audio Backend" +msgstr "Backend de audio" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:30 +msgid "ALSA Device" +msgstr "Dispositivo ALSA" + +#. Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.blp:34 +msgid "Applied only if audio backend is ALSA" +msgstr "Sólo se aplica si el backend de audio es ALSA" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:44 +msgid "Audio Quality" +msgstr "Calidad de sonido" + +#: src/app/components/settings/settings.blp:47 +msgid "Normal" +msgstr "Normal" + +#: src/app/components/settings/settings.blp:48 +msgid "High" +msgstr "Alta" + +#: src/app/components/settings/settings.blp:49 +msgid "Very high" +msgstr "Muy alta" + +#. Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.blp:72 +msgid "Appearance" +msgstr "Apariencia" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:77 +msgid "Theme" +msgstr "Tema" + +#: src/app/components/settings/settings.blp:80 +msgid "Light" +msgstr "Claro" + +#: src/app/components/settings/settings.blp:81 +msgid "Dark" +msgstr "Oscuro" + +#. Header for a group of preference items regarding network +#: src/app/components/settings/settings.blp:91 +msgid "Network" +msgstr "Red" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:96 +msgid "Access Point Port" +msgstr "Puerto del punto de acceso" + +#. Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.blp:100 +msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine." +msgstr "Puerto utilizado para las conexiones con el punto de acceso de Spotify. Poner a 0 si cualquier puerto está bien." + +#: src/app/components/selection/selection_toolbar.blp:78 +msgid "Save to library" +msgstr "Guardar en la biblioteca" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "" + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:57 +msgid "Gapless playback" +msgstr "" + +#: src/app/components/settings/settings.blp:82 +msgid "System" +msgstr "" + +#. Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.blp:69 +msgid "Done" +msgstr "" + +#. label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.blp:10 +msgid "Name" +msgstr "" + +#. Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.blp:30 +msgid "Create" +msgstr "" + diff --git a/po/et.po b/po/et.po new file mode 100644 index 0000000..7c128a7 --- /dev/null +++ b/po/et.po @@ -0,0 +1,389 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Spot\n" +"Language: et\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +msgid "View album" +msgstr "Vaata albumit" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +msgid "Copy link" +msgstr "Kopeeri link" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.blp:36 +msgid "Add to queue" +msgstr "Lisa järjekorda" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +msgid "Remove from queue" +msgstr "Eemalda järjekorrast" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +msgid "Add to {}" +msgstr "Lisa {}" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:68 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "Salasõna salvestamine ebaõnnestus. Kindel, et sessiooni võtmering on lukust lahti?" + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129 +msgid "An error occured. Check logs for details!" +msgstr "Tekkis viga. Rohkem infot logides." + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +msgid "Library" +msgstr "Teek" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +msgid "Playlists" +msgstr "Esitusloendid" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/now_playing/now_playing_model.rs:134 +#: src/app/components/sidebar/sidebar_item.rs:43 +msgid "Now playing" +msgstr "Praegu mängib" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.blp:33 +msgid "No song playing" +msgstr "Ükski laul ei mängi" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +msgid "About" +msgstr "Teave" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +msgid "Quit" +msgstr "Sulge" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +msgid "Log out" +msgstr "Logi välja" + +#: src/app/state/login_state.rs:115 +msgid "Connection restored" +msgstr "Ühendus taastatud" + +#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.blp:26 +msgid "Top tracks" +msgstr "Populaarsemad lood" + +#. Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.blp:54 +msgid "Releases" +msgstr "Diskograafia" + +#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). +#: src/app/components/login/login.blp:49 +msgid "Login to Spotify Premium" +msgstr "Logi sisse Spotify Premiumisse" + +#. Placeholder for the username field +#: src/app/components/login/login.blp:76 +msgid "Username" +msgstr "Kasutajanimi" + +#. Placeholder for the password field +#: src/app/components/login/login.blp:94 +msgid "Password" +msgstr "Salasõna" + +#. This error is shown when authentication fails. +#: src/app/components/login/login.blp:116 +msgid "Authentication failed!" +msgstr "Autentimine ebaõnnestus!" + +#. Log in button label +#: src/app/components/login/login.blp:131 +msgid "Log in" +msgstr "Logi sisse" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:69 +msgid "Albums" +msgstr "Albumid" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:100 +msgid "Artists" +msgstr "Esitajad" + +#: src/app/components/navigation/factory.rs:85 +#: src/app/components/sidebar/sidebar_item.rs:41 +msgid "Saved tracks" +msgstr "Salvestatud lood" + +#: src/app/components/playback/playback_controls.rs:64 +msgid "Pause" +msgstr "Peata" + +#: src/app/components/playback/playback_controls.rs:66 +msgid "Play" +msgstr "Esita" + +#: src/app/components/playback/playback_controls.blp:15 +msgid "Shuffle" +msgstr "Juhuesitus" + +#: src/app/components/playback/playback_controls.blp:24 +msgid "Previous" +msgstr "Eelmine" + +#: src/app/components/playback/playback_controls.blp:46 +msgid "Next" +msgstr "Järgmine" + +#: src/app/components/playback/playback_controls.blp:55 +msgid "Repeat" +msgstr "Korda" + +#. A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:26 +msgid "You have no saved playlists." +msgstr "Sul pole ühtegi salvestatud esitusloendit." + +#. A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:30 +msgid "Your playlists will be shown here." +msgstr "Sinu esitusloendid näidatakse siin." + +#. A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.blp:25 +msgid "You have no saved albums." +msgstr "Sul pole ühtegi salvestatud albumit." + +#. A description of what happens when the user has saved albums. +#: src/app/components/library/library.blp:29 +msgid "Your library will be shown here." +msgstr "Sinu teeki näidatakse siin." + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "{} lugu valitud" +msgstr[1] "{} lugu valitud" + +#. Title for the empty search page (initial state). +#: src/app/components/search/search.blp:110 +msgid "Search Spotify." +msgstr "Otsi Spotify'st." + +#. Subtitle for the empty search page (initial state). +#: src/app/components/search/search.blp:114 +msgid "Type to search." +msgstr "Tipi, et otsida." + +#: src/app/components/selection/selection_toolbar.blp:61 +msgid "Remove" +msgstr "Eemalda" + +#. playlist2-symbolic +#: src/app/components/selection/selection_toolbar.blp:49 +msgid "Add to playlist..." +msgstr "Lisa esitusloendisse..." + +#. Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.blp:53 +#: src/app/components/headerbar/headerbar.blp:49 +msgid "Cancel" +msgstr "Loobu" + +#. Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.blp:63 +msgid "Select all" +msgstr "Vali kõik" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +msgid "More from {}" +msgstr "Rohkem esitajast {}" + +#. This refers to a music label +#: src/app/components/details/release_details.blp:41 +msgid "Label" +msgstr "Väljaandja" + +#. This refers to a release date +#: src/app/components/details/release_details.blp:52 +msgid "Released" +msgstr "Välja antud" + +#. This refers to a number of tracks +#: src/app/components/details/release_details.blp:63 +msgid "Tracks" +msgstr "Lood" + +#. This refers to the duration of a release +#: src/app/components/details/release_details.ui:68 +msgid "Duration" +msgstr "Pikkus" + +#: src/app/components/details/release_details.blp:72 +msgid "Copyright" +msgstr "Autoriõigused" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +msgid "{} by {}" +msgstr "{} - {}" + +#: src/app/components/sidebar/sidebar.rs:49 +msgid "Unnamed playlist" +msgstr "Nimetamata esitusloend" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "Kõik esitusloendid" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "Lugu salvestatud!" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "Eelistused" + +#: src/main.rs:75 +msgid "Failed to open link!" +msgstr "Lingi avamine ebaõnnestus!" + +#. Header for a group of preference items regarding audio +#: src/app/components/settings/settings.blp:13 +msgid "Audio" +msgstr "Heli" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:18 +msgid "Audio Backend" +msgstr "Heli sisend" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:30 +msgid "ALSA Device" +msgstr "ALSA heliseade" + +#. Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.blp:34 +msgid "Applied only if audio backend is ALSA" +msgstr "Kehtib ainult, kui heli sisend on ALSA" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:44 +msgid "Audio Quality" +msgstr "Heli kvaliteet" + +#: src/app/components/settings/settings.blp:47 +msgid "Normal" +msgstr "Keskmine" + +#: src/app/components/settings/settings.blp:48 +msgid "High" +msgstr "Kõrge" + +#: src/app/components/settings/settings.blp:49 +msgid "Very high" +msgstr "Väga kõrge" + +#. Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.blp:72 +msgid "Appearance" +msgstr "Välimus" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:77 +msgid "Theme" +msgstr "Teema" + +#: src/app/components/settings/settings.blp:80 +msgid "Light" +msgstr "Hele teema" + +#: src/app/components/settings/settings.blp:81 +msgid "Dark" +msgstr "Tume teema" + +#. Header for a group of preference items regarding network +#: src/app/components/settings/settings.blp:91 +msgid "Network" +msgstr "Võrk" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:96 +msgid "Access Point Port" +msgstr "Sissepääsu port" + +#. Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.blp:100 +msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine." +msgstr "Port Spotify ühendamiseks. Säti 0, et lubada iga port." + +#: src/app/components/selection/selection_toolbar.blp:78 +msgid "Save to library" +msgstr "Salvesta teeki" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "" + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:57 +msgid "Gapless playback" +msgstr "" + +#: src/app/components/settings/settings.blp:82 +msgid "System" +msgstr "" + +#. Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.blp:69 +msgid "Done" +msgstr "" + +#. label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.blp:10 +msgid "Name" +msgstr "" + +#. Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.blp:30 +msgid "Create" +msgstr "" + diff --git a/po/eu.po b/po/eu.po new file mode 100644 index 0000000..ca66aa7 --- /dev/null +++ b/po/eu.po @@ -0,0 +1,389 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Spot\n" +"Language: eu\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +msgid "View album" +msgstr "Albuma ikusi" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +msgid "Copy link" +msgstr "Kopiatu esteka" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.blp:36 +msgid "Add to queue" +msgstr "Gehitu isatsari" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +msgid "Remove from queue" +msgstr "Ezabatu isatsetik" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +msgid "Add to {}" +msgstr "Gehitu {} (r)i" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:68 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "Ezin izan da pasahitza gorde. Ziurtatu saioko giltzatakoa desblokeatuta dagoela." + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129 +msgid "An error occured. Check logs for details!" +msgstr "Errore bat gertatu da. Egiaztatu xehetasunak erregistroetan!" + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +msgid "Library" +msgstr "Biblioteka" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +msgid "Playlists" +msgstr "Erreprodukzio-zerrendak" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/now_playing/now_playing_model.rs:134 +#: src/app/components/sidebar/sidebar_item.rs:43 +msgid "Now playing" +msgstr "Orain erreproduzitzen" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.blp:33 +msgid "No song playing" +msgstr "Ez da abestirik erreproduzitzen ari" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +msgid "About" +msgstr "Honi buruz" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +msgid "Quit" +msgstr "Utzi" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +msgid "Log out" +msgstr "Saioa itxi" + +#: src/app/state/login_state.rs:115 +msgid "Connection restored" +msgstr "Konexioa leheneratu da" + +#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.blp:26 +msgid "Top tracks" +msgstr "Pista nagusiak" + +#. Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.blp:54 +msgid "Releases" +msgstr "Jaurtiketak" + +#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). +#: src/app/components/login/login.blp:49 +msgid "Login to Spotify Premium" +msgstr "Saioa hasi Spotify Premiumen" + +#. Placeholder for the username field +#: src/app/components/login/login.blp:76 +msgid "Username" +msgstr "Erabiltzailearen izena" + +#. Placeholder for the password field +#: src/app/components/login/login.blp:94 +msgid "Password" +msgstr "Pasahitza" + +#. This error is shown when authentication fails. +#: src/app/components/login/login.blp:116 +msgid "Authentication failed!" +msgstr "Autentifikazioak huts egin du!" + +#. Log in button label +#: src/app/components/login/login.blp:131 +msgid "Log in" +msgstr "Hasi saioa" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:69 +msgid "Albums" +msgstr "Albumak" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:100 +msgid "Artists" +msgstr "Artistak" + +#: src/app/components/navigation/factory.rs:85 +#: src/app/components/sidebar/sidebar_item.rs:41 +msgid "Saved tracks" +msgstr "Gordetako pistak" + +#: src/app/components/playback/playback_controls.rs:64 +msgid "Pause" +msgstr "Gelditu" + +#: src/app/components/playback/playback_controls.rs:66 +msgid "Play" +msgstr "Erreproduzitu" + +#: src/app/components/playback/playback_controls.blp:15 +msgid "Shuffle" +msgstr "Nahasketa" + +#: src/app/components/playback/playback_controls.blp:24 +msgid "Previous" +msgstr "Aurrekoa" + +#: src/app/components/playback/playback_controls.blp:46 +msgid "Next" +msgstr "Hurrengoa" + +#: src/app/components/playback/playback_controls.blp:55 +msgid "Repeat" +msgstr "Errepikatu" + +#. A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:26 +msgid "You have no saved playlists." +msgstr "Ez duzu erreprodukzio zerrendarik gordeta." + +#. A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:30 +msgid "Your playlists will be shown here." +msgstr "Zure erreprodukzio zerrendak hemen agertuko dira." + +#. A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.blp:25 +msgid "You have no saved albums." +msgstr "Ez duzu albumik gordeta." + +#. A description of what happens when the user has saved albums. +#: src/app/components/library/library.blp:29 +msgid "Your library will be shown here." +msgstr "Zure liburutegia hemen agertuko da." + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "{} Aukeratutako abesti bat" +msgstr[1] "{} Aukeratutako abestiak" + +#. Title for the empty search page (initial state). +#: src/app/components/search/search.blp:110 +msgid "Search Spotify." +msgstr "Bilatu Spotifyn." + +#. Subtitle for the empty search page (initial state). +#: src/app/components/search/search.blp:114 +msgid "Type to search." +msgstr "Idatzi bilatzeko." + +#: src/app/components/selection/selection_toolbar.blp:61 +msgid "Remove" +msgstr "Ezabatu" + +#. playlist2-symbolic +#: src/app/components/selection/selection_toolbar.blp:49 +msgid "Add to playlist..." +msgstr "Erreprodukzio-zerrendara gehitu..." + +#. Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.blp:53 +#: src/app/components/headerbar/headerbar.blp:49 +msgid "Cancel" +msgstr "Ezeztatu" + +#. Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.blp:63 +msgid "Select all" +msgstr "Hautatu dena" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +msgid "More from {}" +msgstr "Gehiago honi buruz {}" + +#. This refers to a music label +#: src/app/components/details/release_details.blp:41 +msgid "Label" +msgstr "Etiketa" + +#. This refers to a release date +#: src/app/components/details/release_details.blp:52 +msgid "Released" +msgstr "Argitaratua" + +#. This refers to a number of tracks +#: src/app/components/details/release_details.blp:63 +msgid "Tracks" +msgstr "Pistak" + +#. This refers to the duration of a release +#: src/app/components/details/release_details.ui:68 +msgid "Duration" +msgstr "Iraupena" + +#: src/app/components/details/release_details.blp:72 +msgid "Copyright" +msgstr "Egile-eskubideak" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +msgid "{} by {}" +msgstr "{} {} -ek eginda" + +#: src/app/components/sidebar/sidebar.rs:49 +msgid "Unnamed playlist" +msgstr "" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "" + +#: src/main.rs:75 +msgid "Failed to open link!" +msgstr "" + +#. Header for a group of preference items regarding audio +#: src/app/components/settings/settings.blp:13 +msgid "Audio" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:18 +msgid "Audio Backend" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:30 +msgid "ALSA Device" +msgstr "" + +#. Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.blp:34 +msgid "Applied only if audio backend is ALSA" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:44 +msgid "Audio Quality" +msgstr "" + +#: src/app/components/settings/settings.blp:47 +msgid "Normal" +msgstr "" + +#: src/app/components/settings/settings.blp:48 +msgid "High" +msgstr "" + +#: src/app/components/settings/settings.blp:49 +msgid "Very high" +msgstr "" + +#. Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.blp:72 +msgid "Appearance" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:77 +msgid "Theme" +msgstr "" + +#: src/app/components/settings/settings.blp:80 +msgid "Light" +msgstr "" + +#: src/app/components/settings/settings.blp:81 +msgid "Dark" +msgstr "" + +#. Header for a group of preference items regarding network +#: src/app/components/settings/settings.blp:91 +msgid "Network" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:96 +msgid "Access Point Port" +msgstr "" + +#. Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.blp:100 +msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine." +msgstr "" + +#: src/app/components/selection/selection_toolbar.blp:78 +msgid "Save to library" +msgstr "" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "" + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:57 +msgid "Gapless playback" +msgstr "" + +#: src/app/components/settings/settings.blp:82 +msgid "System" +msgstr "" + +#. Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.blp:69 +msgid "Done" +msgstr "" + +#. label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.blp:10 +msgid "Name" +msgstr "" + +#. Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.blp:30 +msgid "Create" +msgstr "" + diff --git a/po/fi.po b/po/fi.po new file mode 100644 index 0000000..cbc9334 --- /dev/null +++ b/po/fi.po @@ -0,0 +1,389 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Spot\n" +"Language: fi\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +msgid "View album" +msgstr "Näytä albumi" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +msgid "Copy link" +msgstr "Kopioi linkki" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.blp:36 +msgid "Add to queue" +msgstr "Lisää jonoon" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +msgid "Remove from queue" +msgstr "Poista jonosta" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +msgid "Add to {}" +msgstr "Lisää kohteeseen {}" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:68 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "Salasanaa ei voitu tallentaa. Varmista, että avainnippua ei ole lukittu." + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129 +msgid "An error occured. Check logs for details!" +msgstr "Tapahtui virhe. Katso lisätiedot lokista!" + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +msgid "Library" +msgstr "Kirjasto" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +msgid "Playlists" +msgstr "Soittolistat" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/now_playing/now_playing_model.rs:134 +#: src/app/components/sidebar/sidebar_item.rs:43 +msgid "Now playing" +msgstr "Nyt toistetaan" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.blp:33 +msgid "No song playing" +msgstr "Ei toistettavaa kappaletta" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +msgid "About" +msgstr "Tietoja" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +msgid "Quit" +msgstr "Lopeta" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +msgid "Log out" +msgstr "Kirjaudu ulos" + +#: src/app/state/login_state.rs:115 +msgid "Connection restored" +msgstr "Yhteys palautunut" + +#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.blp:26 +msgid "Top tracks" +msgstr "Suosituimmat kappaleet" + +#. Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.blp:54 +msgid "Releases" +msgstr "Julkaisut" + +#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). +#: src/app/components/login/login.blp:49 +msgid "Login to Spotify Premium" +msgstr "Kirjaudu Spotify Premiumiin" + +#. Placeholder for the username field +#: src/app/components/login/login.blp:76 +msgid "Username" +msgstr "Käyttäjätunnus" + +#. Placeholder for the password field +#: src/app/components/login/login.blp:94 +msgid "Password" +msgstr "Salasana" + +#. This error is shown when authentication fails. +#: src/app/components/login/login.blp:116 +msgid "Authentication failed!" +msgstr "Tunnistautuminen epäonnistui!" + +#. Log in button label +#: src/app/components/login/login.blp:131 +msgid "Log in" +msgstr "Kirjaudu sisään" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:69 +msgid "Albums" +msgstr "Albumit" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:100 +msgid "Artists" +msgstr "Esittäjät" + +#: src/app/components/navigation/factory.rs:85 +#: src/app/components/sidebar/sidebar_item.rs:41 +msgid "Saved tracks" +msgstr "Tallennetut kappaleet" + +#: src/app/components/playback/playback_controls.rs:64 +msgid "Pause" +msgstr "Keskeytä" + +#: src/app/components/playback/playback_controls.rs:66 +msgid "Play" +msgstr "Toista" + +#: src/app/components/playback/playback_controls.blp:15 +msgid "Shuffle" +msgstr "Sekoita" + +#: src/app/components/playback/playback_controls.blp:24 +msgid "Previous" +msgstr "Edellinen" + +#: src/app/components/playback/playback_controls.blp:46 +msgid "Next" +msgstr "Seuraava" + +#: src/app/components/playback/playback_controls.blp:55 +msgid "Repeat" +msgstr "Kertaa" + +#. A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:26 +msgid "You have no saved playlists." +msgstr "Ei tallennettuja soittolistoja." + +#. A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:30 +msgid "Your playlists will be shown here." +msgstr "Soittolistasi näytetään täällä." + +#. A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.blp:25 +msgid "You have no saved albums." +msgstr "Ei tallennettuja albumeja." + +#. A description of what happens when the user has saved albums. +#: src/app/components/library/library.blp:29 +msgid "Your library will be shown here." +msgstr "Kirjastosi näytetään täällä." + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "{} kappale valittu" +msgstr[1] "{} kappaletta valittu" + +#. Title for the empty search page (initial state). +#: src/app/components/search/search.blp:110 +msgid "Search Spotify." +msgstr "Etsi Spotifysta." + +#. Subtitle for the empty search page (initial state). +#: src/app/components/search/search.blp:114 +msgid "Type to search." +msgstr "Kirjoita etsiäksesi." + +#: src/app/components/selection/selection_toolbar.blp:61 +msgid "Remove" +msgstr "Poista" + +#. playlist2-symbolic +#: src/app/components/selection/selection_toolbar.blp:49 +msgid "Add to playlist..." +msgstr "Lisää soittolistaan..." + +#. Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.blp:53 +#: src/app/components/headerbar/headerbar.blp:49 +msgid "Cancel" +msgstr "Peru" + +#. Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.blp:63 +msgid "Select all" +msgstr "Valitse kaikki" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +msgid "More from {}" +msgstr "Lisää esittäjältä {}" + +#. This refers to a music label +#: src/app/components/details/release_details.blp:41 +msgid "Label" +msgstr "Levy-yhtiö" + +#. This refers to a release date +#: src/app/components/details/release_details.blp:52 +msgid "Released" +msgstr "Julkaistu" + +#. This refers to a number of tracks +#: src/app/components/details/release_details.blp:63 +msgid "Tracks" +msgstr "Kappaleita" + +#. This refers to the duration of a release +#: src/app/components/details/release_details.ui:68 +msgid "Duration" +msgstr "Kesto" + +#: src/app/components/details/release_details.blp:72 +msgid "Copyright" +msgstr "Tekijänoikeus" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +msgid "{} by {}" +msgstr "{} tehnyt {}" + +#: src/app/components/sidebar/sidebar.rs:49 +msgid "Unnamed playlist" +msgstr "Nimetön soittolista" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "Kaikki soittolistat" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "Kappaleet tallennettu!" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "Asetukset" + +#: src/main.rs:75 +msgid "Failed to open link!" +msgstr "Linkin avaaminen epäonnistui!" + +#. Header for a group of preference items regarding audio +#: src/app/components/settings/settings.blp:13 +msgid "Audio" +msgstr "Ääni" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:18 +msgid "Audio Backend" +msgstr "Äänen taustaosa" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:30 +msgid "ALSA Device" +msgstr "ALSA-laite" + +#. Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.blp:34 +msgid "Applied only if audio backend is ALSA" +msgstr "Sovelletaan vain jos äänen taustaosa on ALSA" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:44 +msgid "Audio Quality" +msgstr "Äänenlaatu" + +#: src/app/components/settings/settings.blp:47 +msgid "Normal" +msgstr "Normaali" + +#: src/app/components/settings/settings.blp:48 +msgid "High" +msgstr "Korkea" + +#: src/app/components/settings/settings.blp:49 +msgid "Very high" +msgstr "Hyvin korkea" + +#. Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.blp:72 +msgid "Appearance" +msgstr "Ulkoasu" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:77 +msgid "Theme" +msgstr "Teema" + +#: src/app/components/settings/settings.blp:80 +msgid "Light" +msgstr "Vaalea" + +#: src/app/components/settings/settings.blp:81 +msgid "Dark" +msgstr "Tumma" + +#. Header for a group of preference items regarding network +#: src/app/components/settings/settings.blp:91 +msgid "Network" +msgstr "Verkko" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:96 +msgid "Access Point Port" +msgstr "Access Pointin portti" + +#. Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.blp:100 +msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine." +msgstr "Spotifyn Access Pointiin tuleviin yhteyksiin käytettävä portti. Aseta arvoksi 0, jos mikä tahansa portti kelpaa." + +#: src/app/components/selection/selection_toolbar.blp:78 +msgid "Save to library" +msgstr "Tallenna kirjastoon" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "Uusi soittolista luotu." + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "Näytä" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "Uusi soittolista" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:57 +msgid "Gapless playback" +msgstr "Keskeytyksetön toisto" + +#: src/app/components/settings/settings.blp:82 +msgid "System" +msgstr "Järjestelmä" + +#. Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.blp:69 +msgid "Done" +msgstr "Valmis" + +#. label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.blp:10 +msgid "Name" +msgstr "Nimi" + +#. Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.blp:30 +msgid "Create" +msgstr "Luo" + diff --git a/po/fr.po b/po/fr.po new file mode 100644 index 0000000..0b0c8a7 --- /dev/null +++ b/po/fr.po @@ -0,0 +1,389 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Spot\n" +"Language: fr\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +msgid "View album" +msgstr "Voir l'album" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +msgid "Copy link" +msgstr "Copier le lien" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.blp:36 +msgid "Add to queue" +msgstr "Ajouter à la file d'attente" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +msgid "Remove from queue" +msgstr "Retirer de la file d'attente" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +msgid "Add to {}" +msgstr "Ajouter à {}" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:68 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "Le mot de passe n'a pu être enregistré, assurez-vous que le Trousseau de session est déverouillé." + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129 +msgid "An error occured. Check logs for details!" +msgstr "Une erreur est survenue. Consultez les journaux de débogage pour plus d'information." + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +msgid "Library" +msgstr "Bibliothèque" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +msgid "Playlists" +msgstr "Listes de lecture" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/now_playing/now_playing_model.rs:134 +#: src/app/components/sidebar/sidebar_item.rs:43 +msgid "Now playing" +msgstr "En cours de lecture" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.blp:33 +msgid "No song playing" +msgstr "Aucune lecture en cours" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +msgid "About" +msgstr "À propos" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +msgid "Quit" +msgstr "Quitter" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +msgid "Log out" +msgstr "Déconnexion" + +#: src/app/state/login_state.rs:115 +msgid "Connection restored" +msgstr "Connexion rétablie" + +#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.blp:26 +msgid "Top tracks" +msgstr "Morceaux populaires" + +#. Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.blp:54 +msgid "Releases" +msgstr "Discographie" + +#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). +#: src/app/components/login/login.blp:49 +msgid "Login to Spotify Premium" +msgstr "Connexion à Spotify Premium" + +#. Placeholder for the username field +#: src/app/components/login/login.blp:76 +msgid "Username" +msgstr "Nom d'utilisateur" + +#. Placeholder for the password field +#: src/app/components/login/login.blp:94 +msgid "Password" +msgstr "Mot de passe" + +#. This error is shown when authentication fails. +#: src/app/components/login/login.blp:116 +msgid "Authentication failed!" +msgstr "L'authentification a échoué !" + +#. Log in button label +#: src/app/components/login/login.blp:131 +msgid "Log in" +msgstr "Connexion" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:69 +msgid "Albums" +msgstr "Albums" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:100 +msgid "Artists" +msgstr "Artistes" + +#: src/app/components/navigation/factory.rs:85 +#: src/app/components/sidebar/sidebar_item.rs:41 +msgid "Saved tracks" +msgstr "Titres aimés" + +#: src/app/components/playback/playback_controls.rs:64 +msgid "Pause" +msgstr "Pause" + +#: src/app/components/playback/playback_controls.rs:66 +msgid "Play" +msgstr "Lecture" + +#: src/app/components/playback/playback_controls.blp:15 +msgid "Shuffle" +msgstr "Aléatoire" + +#: src/app/components/playback/playback_controls.blp:24 +msgid "Previous" +msgstr "Précédent" + +#: src/app/components/playback/playback_controls.blp:46 +msgid "Next" +msgstr "Suivant" + +#: src/app/components/playback/playback_controls.blp:55 +msgid "Repeat" +msgstr "Répéter" + +#. A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:26 +msgid "You have no saved playlists." +msgstr "Aucune liste de lecture enregistrée." + +#. A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:30 +msgid "Your playlists will be shown here." +msgstr "Vos listes de lectures apparaîtrons ici." + +#. A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.blp:25 +msgid "You have no saved albums." +msgstr "Aucun album dans votre bibliothèque." + +#. A description of what happens when the user has saved albums. +#: src/app/components/library/library.blp:29 +msgid "Your library will be shown here." +msgstr "Vos albums apparaîtrons ici." + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "{} morceau sélectionné" +msgstr[1] "{} morceaux sélectionnés" + +#. Title for the empty search page (initial state). +#: src/app/components/search/search.blp:110 +msgid "Search Spotify." +msgstr "Rechercher dans Spotify." + +#. Subtitle for the empty search page (initial state). +#: src/app/components/search/search.blp:114 +msgid "Type to search." +msgstr "Saisissez un terme à rechercher." + +#: src/app/components/selection/selection_toolbar.blp:61 +msgid "Remove" +msgstr "Retirer" + +#. playlist2-symbolic +#: src/app/components/selection/selection_toolbar.blp:49 +msgid "Add to playlist..." +msgstr "Ajouter à..." + +#. Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.blp:53 +#: src/app/components/headerbar/headerbar.blp:49 +msgid "Cancel" +msgstr "Annuler" + +#. Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.blp:63 +msgid "Select all" +msgstr "Tout sélectionner" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +msgid "More from {}" +msgstr "Plus de {}" + +#. This refers to a music label +#: src/app/components/details/release_details.blp:41 +msgid "Label" +msgstr "Label" + +#. This refers to a release date +#: src/app/components/details/release_details.blp:52 +msgid "Released" +msgstr "Date de publication" + +#. This refers to a number of tracks +#: src/app/components/details/release_details.blp:63 +msgid "Tracks" +msgstr "Morceaux" + +#. This refers to the duration of a release +#: src/app/components/details/release_details.ui:68 +msgid "Duration" +msgstr "Durée" + +#: src/app/components/details/release_details.blp:72 +msgid "Copyright" +msgstr "Copyright" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +msgid "{} by {}" +msgstr "{} de {}" + +#: src/app/components/sidebar/sidebar.rs:49 +msgid "Unnamed playlist" +msgstr "Liste de lecture sans titre" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "Toutes les listes de lecture" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "Pistes enregistrées !" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "Préférences" + +#: src/main.rs:75 +msgid "Failed to open link!" +msgstr "Échec de l'ouverture du lien." + +#. Header for a group of preference items regarding audio +#: src/app/components/settings/settings.blp:13 +msgid "Audio" +msgstr "Audio" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:18 +msgid "Audio Backend" +msgstr "Gestionnaire de l'audio" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:30 +msgid "ALSA Device" +msgstr "Appareil ALSA" + +#. Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.blp:34 +msgid "Applied only if audio backend is ALSA" +msgstr "Ne s'applique que si ALSA est sélectionné" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:44 +msgid "Audio Quality" +msgstr "Qualité du flux audio" + +#: src/app/components/settings/settings.blp:47 +msgid "Normal" +msgstr "Normale" + +#: src/app/components/settings/settings.blp:48 +msgid "High" +msgstr "Élevée" + +#: src/app/components/settings/settings.blp:49 +msgid "Very high" +msgstr "Très élevée" + +#. Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.blp:72 +msgid "Appearance" +msgstr "Apparence" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:77 +msgid "Theme" +msgstr "Thème" + +#: src/app/components/settings/settings.blp:80 +msgid "Light" +msgstr "Clair" + +#: src/app/components/settings/settings.blp:81 +msgid "Dark" +msgstr "Sombre" + +#. Header for a group of preference items regarding network +#: src/app/components/settings/settings.blp:91 +msgid "Network" +msgstr "Réseau" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:96 +msgid "Access Point Port" +msgstr "Port du point d'accès" + +#. Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.blp:100 +msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine." +msgstr "Port utilisé pour se connecter au point d'accès de Spotify. Laisser la valeur à 0 pour gérer ce paramètre automatiquement. " + +#: src/app/components/selection/selection_toolbar.blp:78 +msgid "Save to library" +msgstr "Enregistrer dans la bibliothèque" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "Liste de lecture créée." + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "Voir" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "Nouvelle liste de lecture" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:57 +msgid "Gapless playback" +msgstr "Lecture continue" + +#: src/app/components/settings/settings.blp:82 +msgid "System" +msgstr "Valeur système" + +#. Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.blp:69 +msgid "Done" +msgstr "Terminé" + +#. label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.blp:10 +msgid "Name" +msgstr "Nom" + +#. Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.blp:30 +msgid "Create" +msgstr "Créer" + diff --git a/po/ia.po b/po/ia.po new file mode 100644 index 0000000..122f5ef --- /dev/null +++ b/po/ia.po @@ -0,0 +1,389 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Spot\n" +"Language: ia\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +msgid "View album" +msgstr "Vider album" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +msgid "Copy link" +msgstr "Copiar ligamine" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.blp:36 +msgid "Add to queue" +msgstr "Adder al cauda" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +msgid "Remove from queue" +msgstr "Remover ab cauda" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +msgid "Add to {}" +msgstr "Adder a {}" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:68 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "Non poteva salvar le contrasigno. Assecura te qui le magazin de claves es disblocate." + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129 +msgid "An error occured. Check logs for details!" +msgstr "Ocurreva un error. Verifica le registros pro detalios!" + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +msgid "Library" +msgstr "Bibliotheca" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +msgid "Playlists" +msgstr "Listas de reproduction" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/now_playing/now_playing_model.rs:134 +#: src/app/components/sidebar/sidebar_item.rs:43 +msgid "Now playing" +msgstr "Ora in reproduction" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.blp:33 +msgid "No song playing" +msgstr "Nulle canto in reproduction" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +msgid "About" +msgstr "A proposito de" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +msgid "Quit" +msgstr "Quitar" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +msgid "Log out" +msgstr "Clauder le session" + +#: src/app/state/login_state.rs:115 +msgid "Connection restored" +msgstr "Connexion restabilite" + +#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.blp:26 +msgid "Top tracks" +msgstr "Melior tracias" + +#. Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.blp:54 +msgid "Releases" +msgstr "Lanciamentos" + +#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). +#: src/app/components/login/login.blp:49 +msgid "Login to Spotify Premium" +msgstr "Initiar session in Spotify Premium" + +#. Placeholder for the username field +#: src/app/components/login/login.blp:76 +msgid "Username" +msgstr "Nomine de usator" + +#. Placeholder for the password field +#: src/app/components/login/login.blp:94 +msgid "Password" +msgstr "Contrasigno" + +#. This error is shown when authentication fails. +#: src/app/components/login/login.blp:116 +msgid "Authentication failed!" +msgstr "Falleva le authentication!" + +#. Log in button label +#: src/app/components/login/login.blp:131 +msgid "Log in" +msgstr "Initiar session" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:69 +msgid "Albums" +msgstr "Albumes" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:100 +msgid "Artists" +msgstr "Artistas" + +#: src/app/components/navigation/factory.rs:85 +#: src/app/components/sidebar/sidebar_item.rs:41 +msgid "Saved tracks" +msgstr "Tracias salvate" + +#: src/app/components/playback/playback_controls.rs:64 +msgid "Pause" +msgstr "Pausar" + +#: src/app/components/playback/playback_controls.rs:66 +msgid "Play" +msgstr "Reproducer" + +#: src/app/components/playback/playback_controls.blp:15 +msgid "Shuffle" +msgstr "Aleatori" + +#: src/app/components/playback/playback_controls.blp:24 +msgid "Previous" +msgstr "Previe" + +#: src/app/components/playback/playback_controls.blp:46 +msgid "Next" +msgstr "Sequente" + +#: src/app/components/playback/playback_controls.blp:55 +msgid "Repeat" +msgstr "Repeter" + +#. A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:26 +msgid "You have no saved playlists." +msgstr "Tu non ha listas de reproduction salvate." + +#. A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:30 +msgid "Your playlists will be shown here." +msgstr "Tu listas de reproduction essera monstrate hic." + +#. A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.blp:25 +msgid "You have no saved albums." +msgstr "Tu ha nulle albumes salvate." + +#. A description of what happens when the user has saved albums. +#: src/app/components/library/library.blp:29 +msgid "Your library will be shown here." +msgstr "Tu bibliotheca essera monstrate ci." + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "{} canto seligite" +msgstr[1] "{} cantos seligite" + +#. Title for the empty search page (initial state). +#: src/app/components/search/search.blp:110 +msgid "Search Spotify." +msgstr "Cercar Spotify." + +#. Subtitle for the empty search page (initial state). +#: src/app/components/search/search.blp:114 +msgid "Type to search." +msgstr "Scribe pro cercar." + +#: src/app/components/selection/selection_toolbar.blp:61 +msgid "Remove" +msgstr "Remover" + +#. playlist2-symbolic +#: src/app/components/selection/selection_toolbar.blp:49 +msgid "Add to playlist..." +msgstr "Adder al lista de reproduction..." + +#. Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.blp:53 +#: src/app/components/headerbar/headerbar.blp:49 +msgid "Cancel" +msgstr "Cancellar" + +#. Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.blp:63 +msgid "Select all" +msgstr "Seliger toto" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +msgid "More from {}" +msgstr "Plus ab {}" + +#. This refers to a music label +#: src/app/components/details/release_details.blp:41 +msgid "Label" +msgstr "Sigillo" + +#. This refers to a release date +#: src/app/components/details/release_details.blp:52 +msgid "Released" +msgstr "Lanciate" + +#. This refers to a number of tracks +#: src/app/components/details/release_details.blp:63 +msgid "Tracks" +msgstr "Tracias" + +#. This refers to the duration of a release +#: src/app/components/details/release_details.ui:68 +msgid "Duration" +msgstr "Duration" + +#: src/app/components/details/release_details.blp:72 +msgid "Copyright" +msgstr "Copyright" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +msgid "{} by {}" +msgstr "{} per {}" + +#: src/app/components/sidebar/sidebar.rs:49 +msgid "Unnamed playlist" +msgstr "Lista de reproduction sin nomine" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "Tote le listas de reproduction" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "Tracias salvate!" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "Preferentias" + +#: src/main.rs:75 +msgid "Failed to open link!" +msgstr "Falleva in aperir ligamine!" + +#. Header for a group of preference items regarding audio +#: src/app/components/settings/settings.blp:13 +msgid "Audio" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:18 +msgid "Audio Backend" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:30 +msgid "ALSA Device" +msgstr "" + +#. Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.blp:34 +msgid "Applied only if audio backend is ALSA" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:44 +msgid "Audio Quality" +msgstr "" + +#: src/app/components/settings/settings.blp:47 +msgid "Normal" +msgstr "" + +#: src/app/components/settings/settings.blp:48 +msgid "High" +msgstr "" + +#: src/app/components/settings/settings.blp:49 +msgid "Very high" +msgstr "" + +#. Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.blp:72 +msgid "Appearance" +msgstr "Apparentia" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:77 +msgid "Theme" +msgstr "Thema" + +#: src/app/components/settings/settings.blp:80 +msgid "Light" +msgstr "Clar" + +#: src/app/components/settings/settings.blp:81 +msgid "Dark" +msgstr "Obscur" + +#. Header for a group of preference items regarding network +#: src/app/components/settings/settings.blp:91 +msgid "Network" +msgstr "Rete" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:96 +msgid "Access Point Port" +msgstr "" + +#. Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.blp:100 +msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine." +msgstr "" + +#: src/app/components/selection/selection_toolbar.blp:78 +msgid "Save to library" +msgstr "" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "" + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:57 +msgid "Gapless playback" +msgstr "" + +#: src/app/components/settings/settings.blp:82 +msgid "System" +msgstr "" + +#. Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.blp:69 +msgid "Done" +msgstr "" + +#. label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.blp:10 +msgid "Name" +msgstr "" + +#. Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.blp:30 +msgid "Create" +msgstr "" + diff --git a/po/id.po b/po/id.po new file mode 100644 index 0000000..fc6c9bd --- /dev/null +++ b/po/id.po @@ -0,0 +1,388 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Spot\n" +"Language: id\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +msgid "View album" +msgstr "Lihat album" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +msgid "Copy link" +msgstr "Salin tautan" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.blp:36 +msgid "Add to queue" +msgstr "Tambah ke Antrian" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +msgid "Remove from queue" +msgstr "Hapus dari antrian" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +msgid "Add to {}" +msgstr "Tambah ke {}" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:68 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "Tak bisa menyimpan sandi. Pastikan bahwa kunci sesi tidak terkunci." + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129 +msgid "An error occured. Check logs for details!" +msgstr "Timbul galat. Periksa log untuk detailnya!" + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +msgid "Library" +msgstr "Pustaka" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +msgid "Playlists" +msgstr "Daftar putar" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/now_playing/now_playing_model.rs:134 +#: src/app/components/sidebar/sidebar_item.rs:43 +msgid "Now playing" +msgstr "Sedang memutar" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.blp:33 +msgid "No song playing" +msgstr "Tidak ada lagu yang diputar" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +msgid "About" +msgstr "Tentang" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +msgid "Quit" +msgstr "Keluar" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +msgid "Log out" +msgstr "Log keluar" + +#: src/app/state/login_state.rs:115 +msgid "Connection restored" +msgstr "Sambungan dipulihkan" + +#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.blp:26 +msgid "Top tracks" +msgstr "Trek teratas" + +#. Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.blp:54 +msgid "Releases" +msgstr "Rilis" + +#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). +#: src/app/components/login/login.blp:49 +msgid "Login to Spotify Premium" +msgstr "Masuk ke Spotify Premium" + +#. Placeholder for the username field +#: src/app/components/login/login.blp:76 +msgid "Username" +msgstr "Nama Pengguna" + +#. Placeholder for the password field +#: src/app/components/login/login.blp:94 +msgid "Password" +msgstr "Kata Sandi" + +#. This error is shown when authentication fails. +#: src/app/components/login/login.blp:116 +msgid "Authentication failed!" +msgstr "Autentikasi gagal!" + +#. Log in button label +#: src/app/components/login/login.blp:131 +msgid "Log in" +msgstr "Log masuk" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:69 +msgid "Albums" +msgstr "Album" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:100 +msgid "Artists" +msgstr "Artis" + +#: src/app/components/navigation/factory.rs:85 +#: src/app/components/sidebar/sidebar_item.rs:41 +msgid "Saved tracks" +msgstr "Trek yang disimpan" + +#: src/app/components/playback/playback_controls.rs:64 +msgid "Pause" +msgstr "Jeda" + +#: src/app/components/playback/playback_controls.rs:66 +msgid "Play" +msgstr "Putar" + +#: src/app/components/playback/playback_controls.blp:15 +msgid "Shuffle" +msgstr "Acak" + +#: src/app/components/playback/playback_controls.blp:24 +msgid "Previous" +msgstr "Sebelumnya" + +#: src/app/components/playback/playback_controls.blp:46 +msgid "Next" +msgstr "Selanjutnya" + +#: src/app/components/playback/playback_controls.blp:55 +msgid "Repeat" +msgstr "Ulangi" + +#. A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:26 +msgid "You have no saved playlists." +msgstr "Anda tidak memiliki daftar putar yang disimpan." + +#. A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:30 +msgid "Your playlists will be shown here." +msgstr "Daftar putar Anda akan ditampilkan di sini." + +#. A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.blp:25 +msgid "You have no saved albums." +msgstr "Anda tidak memiliki album yang disimpan." + +#. A description of what happens when the user has saved albums. +#: src/app/components/library/library.blp:29 +msgid "Your library will be shown here." +msgstr "Pustaka Anda akan ditampilkan di sini." + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "{} lagu dipilih" + +#. Title for the empty search page (initial state). +#: src/app/components/search/search.blp:110 +msgid "Search Spotify." +msgstr "Cari Spotify." + +#. Subtitle for the empty search page (initial state). +#: src/app/components/search/search.blp:114 +msgid "Type to search." +msgstr "Ketik untuk mencari." + +#: src/app/components/selection/selection_toolbar.blp:61 +msgid "Remove" +msgstr "Buang" + +#. playlist2-symbolic +#: src/app/components/selection/selection_toolbar.blp:49 +msgid "Add to playlist..." +msgstr "Tambah ke daftar putar…" + +#. Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.blp:53 +#: src/app/components/headerbar/headerbar.blp:49 +msgid "Cancel" +msgstr "Batal" + +#. Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.blp:63 +msgid "Select all" +msgstr "Pilih semua" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +msgid "More from {}" +msgstr "Lebih banyak dari {}" + +#. This refers to a music label +#: src/app/components/details/release_details.blp:41 +msgid "Label" +msgstr "Label" + +#. This refers to a release date +#: src/app/components/details/release_details.blp:52 +msgid "Released" +msgstr "Dirilis" + +#. This refers to a number of tracks +#: src/app/components/details/release_details.blp:63 +msgid "Tracks" +msgstr "Trek" + +#. This refers to the duration of a release +#: src/app/components/details/release_details.ui:68 +msgid "Duration" +msgstr "Durasi" + +#: src/app/components/details/release_details.blp:72 +msgid "Copyright" +msgstr "Hak Cipta" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +msgid "{} by {}" +msgstr "{} oleh {}" + +#: src/app/components/sidebar/sidebar.rs:49 +msgid "Unnamed playlist" +msgstr "Daftar putar tanpa nama" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "Semua Daftar Putar" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "Trek disimpan!" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "Preferensi" + +#: src/main.rs:75 +msgid "Failed to open link!" +msgstr "Gagal membuka tautan!" + +#. Header for a group of preference items regarding audio +#: src/app/components/settings/settings.blp:13 +msgid "Audio" +msgstr "Audio" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:18 +msgid "Audio Backend" +msgstr "Backend Audio" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:30 +msgid "ALSA Device" +msgstr "Perangkat ALSA" + +#. Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.blp:34 +msgid "Applied only if audio backend is ALSA" +msgstr "Diterapkan hanya jika backend audio adalah ALSA" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:44 +msgid "Audio Quality" +msgstr "Kualitas Audio" + +#: src/app/components/settings/settings.blp:47 +msgid "Normal" +msgstr "Normal" + +#: src/app/components/settings/settings.blp:48 +msgid "High" +msgstr "Tinggi" + +#: src/app/components/settings/settings.blp:49 +msgid "Very high" +msgstr "Sangat tinggi" + +#. Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.blp:72 +msgid "Appearance" +msgstr "Penampilan" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:77 +msgid "Theme" +msgstr "Tema" + +#: src/app/components/settings/settings.blp:80 +msgid "Light" +msgstr "Terang" + +#: src/app/components/settings/settings.blp:81 +msgid "Dark" +msgstr "Gelap" + +#. Header for a group of preference items regarding network +#: src/app/components/settings/settings.blp:91 +msgid "Network" +msgstr "Jaringan" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:96 +msgid "Access Point Port" +msgstr "Port Titik Akses" + +#. Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.blp:100 +msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine." +msgstr "Port yang digunakan untuk koneksi ke Titik Akses Spotify. Atur ke 0 jika ada port yang baik-baik saja." + +#: src/app/components/selection/selection_toolbar.blp:78 +msgid "Save to library" +msgstr "Simpan ke pustaka" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "Daftar putar baru dibuat." + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "Tilik" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "Daftar Putar Baru" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:57 +msgid "Gapless playback" +msgstr "Pemutaran tanpa celah" + +#: src/app/components/settings/settings.blp:82 +msgid "System" +msgstr "Sistem" + +#. Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.blp:69 +msgid "Done" +msgstr "Selesai" + +#. label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.blp:10 +msgid "Name" +msgstr "Nama" + +#. Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.blp:30 +msgid "Create" +msgstr "Mencipta" + diff --git a/po/it.po b/po/it.po new file mode 100644 index 0000000..e1acfef --- /dev/null +++ b/po/it.po @@ -0,0 +1,390 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Spot\n" +"Language: it\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +msgid "View album" +msgstr "Guarda l'album" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +msgid "Copy link" +msgstr "Copia link" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.blp:36 +msgid "Add to queue" +msgstr "Aggiungi alla coda di riproduzione" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +msgid "Remove from queue" +msgstr "Rimuovi dalla coda di riproduzione" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +msgid "Add to {}" +msgstr "Aggiungi a {}" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:68 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "Impossibile salvare la password. Controlla che la session keyring sia sbloccata." + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129 +msgid "An error occured. Check logs for details!" +msgstr "C'è stato un errore. Controlla i log per i dettagli!" + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +msgid "Library" +msgstr "Raccolta" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +msgid "Playlists" +msgstr "Playlist" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/now_playing/now_playing_model.rs:134 +#: src/app/components/sidebar/sidebar_item.rs:43 +msgid "Now playing" +msgstr "In riproduzione" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.blp:33 +msgid "No song playing" +msgstr "Nessun brano in riproduzione" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +msgid "About" +msgstr "Informazioni su" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +msgid "Quit" +msgstr "Esci" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +msgid "Log out" +msgstr "Log out" + +#: src/app/state/login_state.rs:115 +msgid "Connection restored" +msgstr "Connessione ripristinata" + +#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.blp:26 +msgid "Top tracks" +msgstr "Brani più popolari" + +#. Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.blp:54 +msgid "Releases" +msgstr "Releases" + +#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). +#: src/app/components/login/login.blp:49 +msgid "Login to Spotify Premium" +msgstr "Login su Spotify Premium" + +#. Placeholder for the username field +#: src/app/components/login/login.blp:76 +msgid "Username" +msgstr "Username" + +#. Placeholder for the password field +#: src/app/components/login/login.blp:94 +msgid "Password" +msgstr "Password" + +#. This error is shown when authentication fails. +#: src/app/components/login/login.blp:116 +msgid "Authentication failed!" +msgstr "Autenticazione fallita!" + +#. Log in button label +#: src/app/components/login/login.blp:131 +msgid "Log in" +msgstr "Log in" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:69 +msgid "Albums" +msgstr "Album" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:100 +msgid "Artists" +msgstr "Artisti" + +#: src/app/components/navigation/factory.rs:85 +#: src/app/components/sidebar/sidebar_item.rs:41 +msgid "Saved tracks" +msgstr "Tracce salvate" + +#: src/app/components/playback/playback_controls.rs:64 +msgid "Pause" +msgstr "Pausa" + +#: src/app/components/playback/playback_controls.rs:66 +msgid "Play" +msgstr "Riproduci" + +#: src/app/components/playback/playback_controls.blp:15 +msgid "Shuffle" +msgstr "Mescola" + +#: src/app/components/playback/playback_controls.blp:24 +msgid "Previous" +msgstr "Precedente" + +#: src/app/components/playback/playback_controls.blp:46 +msgid "Next" +msgstr "Successivo" + +#: src/app/components/playback/playback_controls.blp:55 +msgid "Repeat" +msgstr "Ripeti" + +#. A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:26 +msgid "You have no saved playlists." +msgstr "Non hai playlist salvate." + +#. A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:30 +msgid "Your playlists will be shown here." +msgstr "Le tue playlist verranno mostrate qui." + +#. A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.blp:25 +msgid "You have no saved albums." +msgstr "Non hai album salvati." + +#. A description of what happens when the user has saved albums. +#: src/app/components/library/library.blp:29 +msgid "Your library will be shown here." +msgstr "La tua libreria verrà mostrata qui." + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "{} brano selezionato" +msgstr[1] "{} brani selezionati" + +#. Title for the empty search page (initial state). +#: src/app/components/search/search.blp:110 +msgid "Search Spotify." +msgstr "Cerca Spotify." + +#. Subtitle for the empty search page (initial state). +#: src/app/components/search/search.blp:114 +msgid "Type to search." +msgstr "Digita per cercare." + +#: src/app/components/selection/selection_toolbar.blp:61 +msgid "Remove" +msgstr "Rimuovi" + +#. playlist2-symbolic +#: src/app/components/selection/selection_toolbar.blp:49 +msgid "Add to playlist..." +msgstr "Aggiungi alla playlist..." + +#. Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.blp:53 +#: src/app/components/headerbar/headerbar.blp:49 +msgid "Cancel" +msgstr "Annulla" + +#. Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.blp:63 +msgid "Select all" +msgstr "Seleziona tutto" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +msgid "More from {}" +msgstr "Di più da {}" + +#. This refers to a music label +#: src/app/components/details/release_details.blp:41 +msgid "Label" +msgstr "Etichetta discografica" + +#. This refers to a release date +#: src/app/components/details/release_details.blp:52 +msgid "Released" +msgstr "Data di uscita" + +#. This refers to a number of tracks +#: src/app/components/details/release_details.blp:63 +msgid "Tracks" +msgstr "Tracce" + +#. This refers to the duration of a release +#: src/app/components/details/release_details.ui:68 +msgid "Duration" +msgstr "Durata" + +#: src/app/components/details/release_details.blp:72 +msgid "Copyright" +msgstr "Copyright" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +msgid "{} by {}" +msgstr "{} di {}" + +#: src/app/components/sidebar/sidebar.rs:49 +msgid "Unnamed playlist" +msgstr "Playlist senza nome" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "Tutte le playlist" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "Tracce salvate!" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "Preferenze" + +#: src/main.rs:75 +msgid "Failed to open link!" +msgstr "Apertura del link fallita!" + +#. Header for a group of preference items regarding audio +#: src/app/components/settings/settings.blp:13 +msgid "Audio" +msgstr "Audio" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:18 +msgid "Audio Backend" +msgstr "Backend Audio" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:30 +msgid "ALSA Device" +msgstr "Device ALSA" + +#. Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.blp:34 +msgid "Applied only if audio backend is ALSA" +msgstr "Applicato solo se il backend audio è ALSA" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:44 +msgid "Audio Quality" +msgstr "Qualità Audio" + +#: src/app/components/settings/settings.blp:47 +msgid "Normal" +msgstr "Normale" + +#: src/app/components/settings/settings.blp:48 +#, fuzzy +msgid "High" +msgstr "Alta" + +#: src/app/components/settings/settings.blp:49 +msgid "Very high" +msgstr "Molto alta" + +#. Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.blp:72 +msgid "Appearance" +msgstr "Aspetto" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:77 +msgid "Theme" +msgstr "Tema" + +#: src/app/components/settings/settings.blp:80 +msgid "Light" +msgstr "Chiaro" + +#: src/app/components/settings/settings.blp:81 +msgid "Dark" +msgstr "Scuro" + +#. Header for a group of preference items regarding network +#: src/app/components/settings/settings.blp:91 +msgid "Network" +msgstr "Rete" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:96 +msgid "Access Point Port" +msgstr "Porta Access Point" + +#. Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.blp:100 +msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine." +msgstr "Porta usata per le connessioni all'Access Point Spotify. Impostala a 0 se qualsiasi valore va bene." + +#: src/app/components/selection/selection_toolbar.blp:78 +msgid "Save to library" +msgstr "Salva nella libreria" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "Creata nuova playlist." + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "Visualizza" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "Nuova Playlist" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:57 +msgid "Gapless playback" +msgstr "Riproduzione gapless" + +#: src/app/components/settings/settings.blp:82 +msgid "System" +msgstr "Sistema" + +#. Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.blp:69 +msgid "Done" +msgstr "Fatto" + +#. label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.blp:10 +msgid "Name" +msgstr "Nome" + +#. Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.blp:30 +msgid "Create" +msgstr "Crea" + diff --git a/po/ja.po b/po/ja.po new file mode 100644 index 0000000..ebc0f2a --- /dev/null +++ b/po/ja.po @@ -0,0 +1,389 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Spot\n" +"Language: ja\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +msgid "View album" +msgstr "アルバムを見る" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +msgid "Copy link" +msgstr "リンクをコピー" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.blp:36 +msgid "Add to queue" +msgstr "キューに追加" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +msgid "Remove from queue" +msgstr "キューから削除" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +msgid "Add to {}" +msgstr "{}に追加" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:68 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "パスワードを保存できませんでした。セッションキーリングのロックを解除してください。" + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129 +msgid "An error occured. Check logs for details!" +msgstr "エラーが発生しました。詳細はログを確認してください。" + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +msgid "Library" +msgstr "ライブラリ" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +msgid "Playlists" +msgstr "プレイリスト" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/now_playing/now_playing_model.rs:134 +#: src/app/components/sidebar/sidebar_item.rs:43 +msgid "Now playing" +msgstr "再生中" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.blp:33 +msgid "No song playing" +msgstr "再生中の曲はありません" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +msgid "About" +msgstr "このアプリについて" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +msgid "Quit" +msgstr "終了" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +msgid "Log out" +msgstr "ログアウト" + +#: src/app/state/login_state.rs:115 +msgid "Connection restored" +msgstr "再接続しました" + +#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.blp:26 +msgid "Top tracks" +msgstr "人気のトラック" + +#. Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.blp:54 +msgid "Releases" +msgstr "作品" + +#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). +#: src/app/components/login/login.blp:49 +msgid "Login to Spotify Premium" +msgstr "Spotifyプレミアムにログイン" + +#. Placeholder for the username field +#: src/app/components/login/login.blp:76 +msgid "Username" +msgstr "ユーザー名" + +#. Placeholder for the password field +#: src/app/components/login/login.blp:94 +msgid "Password" +msgstr "パスワード" + +#. This error is shown when authentication fails. +#: src/app/components/login/login.blp:116 +msgid "Authentication failed!" +msgstr "認証に失敗しました。" + +#. Log in button label +#: src/app/components/login/login.blp:131 +msgid "Log in" +msgstr "ログイン" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:69 +msgid "Albums" +msgstr "アルバム" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:100 +msgid "Artists" +msgstr "アーティスト" + +#: src/app/components/navigation/factory.rs:85 +#: src/app/components/sidebar/sidebar_item.rs:41 +msgid "Saved tracks" +msgstr "お気に入り" + +#: src/app/components/playback/playback_controls.rs:64 +msgid "Pause" +msgstr "一時停止" + +#: src/app/components/playback/playback_controls.rs:66 +msgid "Play" +msgstr "再生" + +#: src/app/components/playback/playback_controls.blp:15 +msgid "Shuffle" +msgstr "ランダム" + +#: src/app/components/playback/playback_controls.blp:24 +msgid "Previous" +msgstr "前" + +#: src/app/components/playback/playback_controls.blp:46 +msgid "Next" +msgstr "次" + +#: src/app/components/playback/playback_controls.blp:55 +msgid "Repeat" +msgstr "繰り返し" + +#. A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:26 +msgid "You have no saved playlists." +msgstr "保存したプレイリストはありません。" + +#. A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:30 +msgid "Your playlists will be shown here." +msgstr "プレイリストがここに表示されます。" + +#. A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.blp:25 +msgid "You have no saved albums." +msgstr "保存したアルバムがありません。" + +#. A description of what happens when the user has saved albums. +#: src/app/components/library/library.blp:29 +msgid "Your library will be shown here." +msgstr "ライブラリはここに表示されます。" + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "{}曲選択中" + +#. Title for the empty search page (initial state). +#: src/app/components/search/search.blp:110 +msgid "Search Spotify." +msgstr "検索する。" + +#. Subtitle for the empty search page (initial state). +#: src/app/components/search/search.blp:114 +msgid "Type to search." +msgstr "検索ワードを入力してください。" + +#: src/app/components/selection/selection_toolbar.blp:61 +msgid "Remove" +msgstr "削除" + +#. playlist2-symbolic +#: src/app/components/selection/selection_toolbar.blp:49 +msgid "Add to playlist..." +msgstr "プレイリストに追加…" + +#. Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.blp:53 +#: src/app/components/headerbar/headerbar.blp:49 +msgid "Cancel" +msgstr "選択解除" + +#. Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.blp:63 +msgid "Select all" +msgstr "すべて選択" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +#, fuzzy +msgid "More from {}" +msgstr "" + +#. This refers to a music label +#: src/app/components/details/release_details.blp:41 +msgid "Label" +msgstr "レーベル" + +#. This refers to a release date +#: src/app/components/details/release_details.blp:52 +msgid "Released" +msgstr "リリース" + +#. This refers to a number of tracks +#: src/app/components/details/release_details.blp:63 +msgid "Tracks" +msgstr "曲数" + +#. This refers to the duration of a release +#: src/app/components/details/release_details.ui:68 +msgid "Duration" +msgstr "再生時間" + +#: src/app/components/details/release_details.blp:72 +msgid "Copyright" +msgstr "著作権表示" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +msgid "{} by {}" +msgstr "{} から {}" + +#: src/app/components/sidebar/sidebar.rs:49 +msgid "Unnamed playlist" +msgstr "" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "" + +#: src/main.rs:75 +msgid "Failed to open link!" +msgstr "" + +#. Header for a group of preference items regarding audio +#: src/app/components/settings/settings.blp:13 +msgid "Audio" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:18 +msgid "Audio Backend" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:30 +msgid "ALSA Device" +msgstr "" + +#. Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.blp:34 +msgid "Applied only if audio backend is ALSA" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:44 +msgid "Audio Quality" +msgstr "" + +#: src/app/components/settings/settings.blp:47 +msgid "Normal" +msgstr "" + +#: src/app/components/settings/settings.blp:48 +msgid "High" +msgstr "" + +#: src/app/components/settings/settings.blp:49 +msgid "Very high" +msgstr "" + +#. Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.blp:72 +msgid "Appearance" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:77 +msgid "Theme" +msgstr "" + +#: src/app/components/settings/settings.blp:80 +msgid "Light" +msgstr "" + +#: src/app/components/settings/settings.blp:81 +msgid "Dark" +msgstr "" + +#. Header for a group of preference items regarding network +#: src/app/components/settings/settings.blp:91 +msgid "Network" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:96 +msgid "Access Point Port" +msgstr "" + +#. Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.blp:100 +msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine." +msgstr "" + +#: src/app/components/selection/selection_toolbar.blp:78 +msgid "Save to library" +msgstr "" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "" + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:57 +msgid "Gapless playback" +msgstr "" + +#: src/app/components/settings/settings.blp:82 +msgid "System" +msgstr "" + +#. Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.blp:69 +msgid "Done" +msgstr "" + +#. label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.blp:10 +msgid "Name" +msgstr "" + +#. Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.blp:30 +msgid "Create" +msgstr "" + diff --git a/po/meson.build b/po/meson.build new file mode 100644 index 0000000..b75aa95 --- /dev/null +++ b/po/meson.build @@ -0,0 +1,2 @@ +i18n = import('i18n') +i18n.gettext('spot', args: ['--from-code=UTF-8', '--add-comments'], preset: 'glib') diff --git a/po/nb.po b/po/nb.po new file mode 100644 index 0000000..9815f6e --- /dev/null +++ b/po/nb.po @@ -0,0 +1,390 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Spot\n" +"Language: nb\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +msgid "View album" +msgstr "Vis album" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +msgid "Copy link" +msgstr "Kopier lenke" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.blp:36 +msgid "Add to queue" +msgstr "Legg til i køen" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +msgid "Remove from queue" +msgstr "Fjern fra køen" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +msgid "Add to {}" +msgstr "Legg i {}" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:68 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "Kunne ikke lagre passordet. Sørg for at nøkkelringen til sesjonen er låst opp." + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129 +msgid "An error occured. Check logs for details!" +msgstr "En feil oppstod. Sjekk logger for detaljer!" + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +msgid "Library" +msgstr "Bibliotek" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +msgid "Playlists" +msgstr "Spillelister" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/now_playing/now_playing_model.rs:134 +#: src/app/components/sidebar/sidebar_item.rs:43 +msgid "Now playing" +msgstr "Nå spiller" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.blp:33 +msgid "No song playing" +msgstr "Ingen sang spilles" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +msgid "About" +msgstr "Om" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +msgid "Quit" +msgstr "Avslutt" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +msgid "Log out" +msgstr "Logg ut" + +#: src/app/state/login_state.rs:115 +msgid "Connection restored" +msgstr "Tilkoblingen er gjenopprettet" + +#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.blp:26 +msgid "Top tracks" +msgstr "Topp spor" + +#. Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.blp:54 +msgid "Releases" +msgstr "Utgivelser" + +#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). +#: src/app/components/login/login.blp:49 +msgid "Login to Spotify Premium" +msgstr "Logg på Spotify Premium" + +#. Placeholder for the username field +#: src/app/components/login/login.blp:76 +msgid "Username" +msgstr "Brukernavn" + +#. Placeholder for the password field +#: src/app/components/login/login.blp:94 +msgid "Password" +msgstr "Passord" + +#. This error is shown when authentication fails. +#: src/app/components/login/login.blp:116 +msgid "Authentication failed!" +msgstr "Autentiseringen mislyktes!" + +#. Log in button label +#: src/app/components/login/login.blp:131 +msgid "Log in" +msgstr "Logg inn" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:69 +msgid "Albums" +msgstr "Album" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:100 +msgid "Artists" +msgstr "Artister" + +#: src/app/components/navigation/factory.rs:85 +#: src/app/components/sidebar/sidebar_item.rs:41 +msgid "Saved tracks" +msgstr "Lagrede spor" + +#: src/app/components/playback/playback_controls.rs:64 +msgid "Pause" +msgstr "Pause" + +#: src/app/components/playback/playback_controls.rs:66 +msgid "Play" +msgstr "Spill av" + +#: src/app/components/playback/playback_controls.blp:15 +msgid "Shuffle" +msgstr "Tilfeldig" + +#: src/app/components/playback/playback_controls.blp:24 +msgid "Previous" +msgstr "Forrige" + +#: src/app/components/playback/playback_controls.blp:46 +msgid "Next" +msgstr "Neste" + +#: src/app/components/playback/playback_controls.blp:55 +msgid "Repeat" +msgstr "Gjenta" + +#. A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:26 +msgid "You have no saved playlists." +msgstr "Du har ingen lagrede spillelister." + +#. A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:30 +msgid "Your playlists will be shown here." +msgstr "Spillelistene dine vises her." + +#. A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.blp:25 +msgid "You have no saved albums." +msgstr "Du har ingen lagrede album." + +#. A description of what happens when the user has saved albums. +#: src/app/components/library/library.blp:29 +msgid "Your library will be shown here." +msgstr "Biblioteket ditt vises her." + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +#, fuzzy +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "{} sang valgt" +msgstr[1] "{} sanger valgt" + +#. Title for the empty search page (initial state). +#: src/app/components/search/search.blp:110 +msgid "Search Spotify." +msgstr "Søk Spotify." + +#. Subtitle for the empty search page (initial state). +#: src/app/components/search/search.blp:114 +msgid "Type to search." +msgstr "Skriv for å søke." + +#: src/app/components/selection/selection_toolbar.blp:61 +msgid "Remove" +msgstr "Fjern" + +#. playlist2-symbolic +#: src/app/components/selection/selection_toolbar.blp:49 +msgid "Add to playlist..." +msgstr "Legg til spilleliste..." + +#. Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.blp:53 +#: src/app/components/headerbar/headerbar.blp:49 +msgid "Cancel" +msgstr "Avbryt" + +#. Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.blp:63 +msgid "Select all" +msgstr "Velg alle" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +msgid "More from {}" +msgstr "Mer fra {}" + +#. This refers to a music label +#: src/app/components/details/release_details.blp:41 +msgid "Label" +msgstr "Utgiver" + +#. This refers to a release date +#: src/app/components/details/release_details.blp:52 +msgid "Released" +msgstr "Utgitt" + +#. This refers to a number of tracks +#: src/app/components/details/release_details.blp:63 +msgid "Tracks" +msgstr "Spor" + +#. This refers to the duration of a release +#: src/app/components/details/release_details.ui:68 +msgid "Duration" +msgstr "Varighet" + +#: src/app/components/details/release_details.blp:72 +msgid "Copyright" +msgstr "Opphavsrett" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +msgid "{} by {}" +msgstr "{} av {}" + +#: src/app/components/sidebar/sidebar.rs:49 +msgid "Unnamed playlist" +msgstr "Navnløs spilleliste" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "Alle Spillelister" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "Spor lagret!" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "Innstillinger" + +#: src/main.rs:75 +msgid "Failed to open link!" +msgstr "Kunne ikke åpne linken!" + +#. Header for a group of preference items regarding audio +#: src/app/components/settings/settings.blp:13 +msgid "Audio" +msgstr "Lyd" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:18 +msgid "Audio Backend" +msgstr "Lyd Backend" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:30 +msgid "ALSA Device" +msgstr "ALSA Enhet" + +#. Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.blp:34 +msgid "Applied only if audio backend is ALSA" +msgstr "Brukes bare hvis lyd backend er ALSA" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:44 +msgid "Audio Quality" +msgstr "Lyd Kvalitet" + +#: src/app/components/settings/settings.blp:47 +msgid "Normal" +msgstr "Normal" + +#: src/app/components/settings/settings.blp:48 +msgid "High" +msgstr "Høy" + +#: src/app/components/settings/settings.blp:49 +msgid "Very high" +msgstr "Veldig høy" + +#. Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.blp:72 +msgid "Appearance" +msgstr "Utseende" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:77 +msgid "Theme" +msgstr "Tema" + +#: src/app/components/settings/settings.blp:80 +msgid "Light" +msgstr "Lys" + +#: src/app/components/settings/settings.blp:81 +msgid "Dark" +msgstr "Mørk" + +#. Header for a group of preference items regarding network +#: src/app/components/settings/settings.blp:91 +msgid "Network" +msgstr "Nettverk" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:96 +msgid "Access Point Port" +msgstr "Aksesspunkt Port" + +#. Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.blp:100 +msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine." +msgstr "Port som brukes for tilkoblinger til Spotify sitt aksesspunkt. Sett til 0 hvis tilfeldig port er greit." + +#: src/app/components/selection/selection_toolbar.blp:78 +msgid "Save to library" +msgstr "Lagre til bibliotek" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "" + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:57 +msgid "Gapless playback" +msgstr "" + +#: src/app/components/settings/settings.blp:82 +msgid "System" +msgstr "" + +#. Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.blp:69 +msgid "Done" +msgstr "" + +#. label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.blp:10 +msgid "Name" +msgstr "" + +#. Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.blp:30 +msgid "Create" +msgstr "" + diff --git a/po/nl.po b/po/nl.po new file mode 100644 index 0000000..1a1e8cd --- /dev/null +++ b/po/nl.po @@ -0,0 +1,389 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Spot\n" +"Language: nl\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +msgid "View album" +msgstr "Album bekijken" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +msgid "Copy link" +msgstr "Link kopiëren" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.blp:36 +msgid "Add to queue" +msgstr "Toevoegen aan wachtrij" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +msgid "Remove from queue" +msgstr "Verwijderen uit wachtrij" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +msgid "Add to {}" +msgstr "Toevoegen aan {}" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:68 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "Het wachtwoord kan niet worden opgeslagen - zorg dat de sessiesleutelhanger ontgrendeld is." + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129 +msgid "An error occured. Check logs for details!" +msgstr "Er is een fout opgetreden - bekijk het logboek!" + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +msgid "Library" +msgstr "Verzameling" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +msgid "Playlists" +msgstr "Afspeellijsten" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/now_playing/now_playing_model.rs:134 +#: src/app/components/sidebar/sidebar_item.rs:43 +msgid "Now playing" +msgstr "Je luistert naar" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.blp:33 +msgid "No song playing" +msgstr "Er wordt niks afgespeeld" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +msgid "About" +msgstr "Over" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +msgid "Quit" +msgstr "Afsluiten" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +msgid "Log out" +msgstr "Uitloggen" + +#: src/app/state/login_state.rs:115 +msgid "Connection restored" +msgstr "De verbinding is hersteld" + +#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.blp:26 +msgid "Top tracks" +msgstr "Topnummers" + +#. Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.blp:54 +msgid "Releases" +msgstr "Uitgaven" + +#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). +#: src/app/components/login/login.blp:49 +msgid "Login to Spotify Premium" +msgstr "Inloggen op Spotify Premium" + +#. Placeholder for the username field +#: src/app/components/login/login.blp:76 +msgid "Username" +msgstr "Gebruikersnaam" + +#. Placeholder for the password field +#: src/app/components/login/login.blp:94 +msgid "Password" +msgstr "Wachtwoord" + +#. This error is shown when authentication fails. +#: src/app/components/login/login.blp:116 +msgid "Authentication failed!" +msgstr "Het inloggen is mislukt." + +#. Log in button label +#: src/app/components/login/login.blp:131 +msgid "Log in" +msgstr "Inloggen" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:69 +msgid "Albums" +msgstr "Albums" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:100 +msgid "Artists" +msgstr "Artiesten" + +#: src/app/components/navigation/factory.rs:85 +#: src/app/components/sidebar/sidebar_item.rs:41 +msgid "Saved tracks" +msgstr "Opgeslagen nummers" + +#: src/app/components/playback/playback_controls.rs:64 +msgid "Pause" +msgstr "Pauzeren" + +#: src/app/components/playback/playback_controls.rs:66 +msgid "Play" +msgstr "Afspelen" + +#: src/app/components/playback/playback_controls.blp:15 +msgid "Shuffle" +msgstr "Willekeurig" + +#: src/app/components/playback/playback_controls.blp:24 +msgid "Previous" +msgstr "Vorige" + +#: src/app/components/playback/playback_controls.blp:46 +msgid "Next" +msgstr "Volgende" + +#: src/app/components/playback/playback_controls.blp:55 +msgid "Repeat" +msgstr "Herhalen" + +#. A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:26 +msgid "You have no saved playlists." +msgstr "Je hebt nog geen opgeslagen afspeellijsten." + +#. A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:30 +msgid "Your playlists will be shown here." +msgstr "Je afspeellijsten worden hier getoond." + +#. A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.blp:25 +msgid "You have no saved albums." +msgstr "Je hebt nog geen opgeslagen albums." + +#. A description of what happens when the user has saved albums. +#: src/app/components/library/library.blp:29 +msgid "Your library will be shown here." +msgstr "Je verzameling wordt hier getoond." + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "{} nummer geselecteerd" +msgstr[1] "{} nummers geselecteerd" + +#. Title for the empty search page (initial state). +#: src/app/components/search/search.blp:110 +msgid "Search Spotify." +msgstr "Doorzoek Spotify." + +#. Subtitle for the empty search page (initial state). +#: src/app/components/search/search.blp:114 +msgid "Type to search." +msgstr "Typ om te zoeken" + +#: src/app/components/selection/selection_toolbar.blp:61 +msgid "Remove" +msgstr "Verwijderen" + +#. playlist2-symbolic +#: src/app/components/selection/selection_toolbar.blp:49 +msgid "Add to playlist..." +msgstr "Toevoegen aan afspeellijst…" + +#. Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.blp:53 +#: src/app/components/headerbar/headerbar.blp:49 +msgid "Cancel" +msgstr "Annuleren" + +#. Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.blp:63 +msgid "Select all" +msgstr "Alles selecteren" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +msgid "More from {}" +msgstr "Meer van {}" + +#. This refers to a music label +#: src/app/components/details/release_details.blp:41 +msgid "Label" +msgstr "Label" + +#. This refers to a release date +#: src/app/components/details/release_details.blp:52 +msgid "Released" +msgstr "Uitgebracht" + +#. This refers to a number of tracks +#: src/app/components/details/release_details.blp:63 +msgid "Tracks" +msgstr "Nummers" + +#. This refers to the duration of a release +#: src/app/components/details/release_details.ui:68 +msgid "Duration" +msgstr "Duur" + +#: src/app/components/details/release_details.blp:72 +msgid "Copyright" +msgstr "Copyright" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +msgid "{} by {}" +msgstr "{} van {}" + +#: src/app/components/sidebar/sidebar.rs:49 +msgid "Unnamed playlist" +msgstr "Naamloze afspeellijst" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "Alle afspeellijsten" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "De nummers zijn opgeslagen!" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "Voorkeuren" + +#: src/main.rs:75 +msgid "Failed to open link!" +msgstr "De link kan niet worden geopend!" + +#. Header for a group of preference items regarding audio +#: src/app/components/settings/settings.blp:13 +msgid "Audio" +msgstr "Audio" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:18 +msgid "Audio Backend" +msgstr "Audiobackend" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:30 +msgid "ALSA Device" +msgstr "ALSA-apparaat" + +#. Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.blp:34 +msgid "Applied only if audio backend is ALSA" +msgstr "Alleen van toepassing als het audiobackend is ingesteld op ALSA" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:44 +msgid "Audio Quality" +msgstr "Audiokwaliteit" + +#: src/app/components/settings/settings.blp:47 +msgid "Normal" +msgstr "Normaal" + +#: src/app/components/settings/settings.blp:48 +msgid "High" +msgstr "Hoog" + +#: src/app/components/settings/settings.blp:49 +msgid "Very high" +msgstr "Hoogst" + +#. Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.blp:72 +msgid "Appearance" +msgstr "Vormgeving" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:77 +msgid "Theme" +msgstr "Thema" + +#: src/app/components/settings/settings.blp:80 +msgid "Light" +msgstr "Licht" + +#: src/app/components/settings/settings.blp:81 +msgid "Dark" +msgstr "Donker" + +#. Header for a group of preference items regarding network +#: src/app/components/settings/settings.blp:91 +msgid "Network" +msgstr "Netwerk" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:96 +msgid "Access Point Port" +msgstr "Poort van toegangspunt" + +#. Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.blp:100 +msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine." +msgstr "De poort die wordt gebruikt om verbinding te maken met Spotify's toegangspunt. 0 = elke poort." + +#: src/app/components/selection/selection_toolbar.blp:78 +msgid "Save to library" +msgstr "Opslaan in verzameling" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "De afspeellijst is aangemaakt." + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "Bekijken" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "Nieuwe afspeellijst" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:57 +msgid "Gapless playback" +msgstr "Afspelen zonder pauzes" + +#: src/app/components/settings/settings.blp:82 +msgid "System" +msgstr "Systeem" + +#. Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.blp:69 +msgid "Done" +msgstr "Klaar" + +#. label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.blp:10 +msgid "Name" +msgstr "Naam" + +#. Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.blp:30 +msgid "Create" +msgstr "Aanmaken" + diff --git a/po/pl.po b/po/pl.po new file mode 100644 index 0000000..d9985c1 --- /dev/null +++ b/po/pl.po @@ -0,0 +1,391 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Spot\n" +"Language: pl\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +msgid "View album" +msgstr "Zobacz album" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +msgid "Copy link" +msgstr "Skopiuj link" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.blp:36 +msgid "Add to queue" +msgstr "Dodaj do kolejki" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +msgid "Remove from queue" +msgstr "Usuń z kolejki" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +msgid "Add to {}" +msgstr "Dodaj do {}" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:68 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "Nie można zapisać hasła. Upewnij się że narzędzie zarządzające kluczami jest odblokowane." + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129 +msgid "An error occured. Check logs for details!" +msgstr "Wystąpił błąd. Sprawdź logi po więcej informacji!" + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +msgid "Library" +msgstr "Biblioteka" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +msgid "Playlists" +msgstr "Playlisty" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/now_playing/now_playing_model.rs:134 +#: src/app/components/sidebar/sidebar_item.rs:43 +msgid "Now playing" +msgstr "Aktualnie odtwarzane" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.blp:33 +msgid "No song playing" +msgstr "Aktualnie nie gra żadna piosenka" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +msgid "About" +msgstr "O programie" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +msgid "Quit" +msgstr "Wyjdź" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +msgid "Log out" +msgstr "Wyloguj się" + +#: src/app/state/login_state.rs:115 +msgid "Connection restored" +msgstr "Odzyskano połączenie" + +#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.blp:26 +msgid "Top tracks" +msgstr "Najlepsze piosenki" + +#. Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.blp:54 +msgid "Releases" +msgstr "Premiery" + +#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). +#: src/app/components/login/login.blp:49 +msgid "Login to Spotify Premium" +msgstr "Zaloguj się do Spotify Premium" + +#. Placeholder for the username field +#: src/app/components/login/login.blp:76 +msgid "Username" +msgstr "Nazwa użytkownika" + +#. Placeholder for the password field +#: src/app/components/login/login.blp:94 +msgid "Password" +msgstr "Hasło" + +#. This error is shown when authentication fails. +#: src/app/components/login/login.blp:116 +msgid "Authentication failed!" +msgstr "Błąd autoryzacji!" + +#. Log in button label +#: src/app/components/login/login.blp:131 +msgid "Log in" +msgstr "Zaloguj" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:69 +msgid "Albums" +msgstr "Albumy" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:100 +msgid "Artists" +msgstr "Artyści" + +#: src/app/components/navigation/factory.rs:85 +#: src/app/components/sidebar/sidebar_item.rs:41 +msgid "Saved tracks" +msgstr "Zachowane piosenki" + +#: src/app/components/playback/playback_controls.rs:64 +msgid "Pause" +msgstr "" + +#: src/app/components/playback/playback_controls.rs:66 +msgid "Play" +msgstr "" + +#: src/app/components/playback/playback_controls.blp:15 +msgid "Shuffle" +msgstr "" + +#: src/app/components/playback/playback_controls.blp:24 +msgid "Previous" +msgstr "" + +#: src/app/components/playback/playback_controls.blp:46 +msgid "Next" +msgstr "" + +#: src/app/components/playback/playback_controls.blp:55 +msgid "Repeat" +msgstr "" + +#. A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:26 +msgid "You have no saved playlists." +msgstr "Nie masz zapisanych playlist." + +#. A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:30 +msgid "Your playlists will be shown here." +msgstr "Twoje playlisty będą tu pokazane." + +#. A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.blp:25 +msgid "You have no saved albums." +msgstr "Nie masz zapisanych albumów." + +#. A description of what happens when the user has saved albums. +#: src/app/components/library/library.blp:29 +msgid "Your library will be shown here." +msgstr "Twoja biblioteka będzie tu pokazana." + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "{} wybrana piosenka" +msgstr[1] "{} wybrane piosenki" +msgstr[2] "{} wybranych piosenek" +msgstr[3] "" + +#. Title for the empty search page (initial state). +#: src/app/components/search/search.blp:110 +msgid "Search Spotify." +msgstr "" + +#. Subtitle for the empty search page (initial state). +#: src/app/components/search/search.blp:114 +msgid "Type to search." +msgstr "" + +#: src/app/components/selection/selection_toolbar.blp:61 +msgid "Remove" +msgstr "" + +#. playlist2-symbolic +#: src/app/components/selection/selection_toolbar.blp:49 +msgid "Add to playlist..." +msgstr "" + +#. Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.blp:53 +#: src/app/components/headerbar/headerbar.blp:49 +msgid "Cancel" +msgstr "" + +#. Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.blp:63 +msgid "Select all" +msgstr "" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +msgid "More from {}" +msgstr "Więcej od {}" + +#. This refers to a music label +#: src/app/components/details/release_details.blp:41 +msgid "Label" +msgstr "Wydawca" + +#. This refers to a release date +#: src/app/components/details/release_details.blp:52 +msgid "Released" +msgstr "Premiery" + +#. This refers to a number of tracks +#: src/app/components/details/release_details.blp:63 +msgid "Tracks" +msgstr "Najlepsze piosenki" + +#. This refers to the duration of a release +#: src/app/components/details/release_details.ui:68 +msgid "Duration" +msgstr "Długość" + +#: src/app/components/details/release_details.blp:72 +msgid "Copyright" +msgstr "Prawa autorskie" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +msgid "{} by {}" +msgstr "" + +#: src/app/components/sidebar/sidebar.rs:49 +msgid "Unnamed playlist" +msgstr "" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "" + +#: src/main.rs:75 +msgid "Failed to open link!" +msgstr "" + +#. Header for a group of preference items regarding audio +#: src/app/components/settings/settings.blp:13 +msgid "Audio" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:18 +msgid "Audio Backend" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:30 +msgid "ALSA Device" +msgstr "" + +#. Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.blp:34 +msgid "Applied only if audio backend is ALSA" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:44 +msgid "Audio Quality" +msgstr "" + +#: src/app/components/settings/settings.blp:47 +msgid "Normal" +msgstr "" + +#: src/app/components/settings/settings.blp:48 +msgid "High" +msgstr "" + +#: src/app/components/settings/settings.blp:49 +msgid "Very high" +msgstr "" + +#. Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.blp:72 +msgid "Appearance" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:77 +msgid "Theme" +msgstr "" + +#: src/app/components/settings/settings.blp:80 +msgid "Light" +msgstr "" + +#: src/app/components/settings/settings.blp:81 +msgid "Dark" +msgstr "" + +#. Header for a group of preference items regarding network +#: src/app/components/settings/settings.blp:91 +msgid "Network" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:96 +msgid "Access Point Port" +msgstr "" + +#. Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.blp:100 +msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine." +msgstr "" + +#: src/app/components/selection/selection_toolbar.blp:78 +msgid "Save to library" +msgstr "" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "" + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:57 +msgid "Gapless playback" +msgstr "" + +#: src/app/components/settings/settings.blp:82 +msgid "System" +msgstr "" + +#. Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.blp:69 +msgid "Done" +msgstr "" + +#. label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.blp:10 +msgid "Name" +msgstr "" + +#. Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.blp:30 +msgid "Create" +msgstr "" + diff --git a/po/poeditor.yml b/po/poeditor.yml new file mode 100644 index 0000000..33a5442 --- /dev/null +++ b/po/poeditor.yml @@ -0,0 +1,5 @@ +api_token: 7ac85acabcb7842c23f5d4060485f25e # read only token +projects: +- format: po + id: 469205 + terms_path: '{language_code}.po' diff --git a/po/pt-br.po b/po/pt-br.po new file mode 100644 index 0000000..9fd65f4 --- /dev/null +++ b/po/pt-br.po @@ -0,0 +1,389 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Spot\n" +"Language: pt-br\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +msgid "View album" +msgstr "Ver álbum" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +msgid "Copy link" +msgstr "Copiar link" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.blp:36 +msgid "Add to queue" +msgstr "Adicionar a fila" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +msgid "Remove from queue" +msgstr "Remover da fila" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +msgid "Add to {}" +msgstr "Adicionar a {}" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:68 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "Não foi possível salvar a senha. Verifique se o seu chaveiro de sessão está desbloqueado." + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129 +msgid "An error occured. Check logs for details!" +msgstr "Ocorreu um erro. Verifique os logs para mais detalhes!" + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +msgid "Library" +msgstr "Biblioteca" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +msgid "Playlists" +msgstr "Listas de reprodução" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/now_playing/now_playing_model.rs:134 +#: src/app/components/sidebar/sidebar_item.rs:43 +msgid "Now playing" +msgstr "Tocando agora" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.blp:33 +msgid "No song playing" +msgstr "Nenhum música sendo executada" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +msgid "About" +msgstr "Sobre" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +msgid "Quit" +msgstr "Fechar" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +msgid "Log out" +msgstr "Sair" + +#: src/app/state/login_state.rs:115 +msgid "Connection restored" +msgstr "Conexão reestabelecida" + +#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.blp:26 +msgid "Top tracks" +msgstr "Melhores faixas" + +#. Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.blp:54 +msgid "Releases" +msgstr "Lançamentos" + +#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). +#: src/app/components/login/login.blp:49 +msgid "Login to Spotify Premium" +msgstr "Entrar no Spotify Premium" + +#. Placeholder for the username field +#: src/app/components/login/login.blp:76 +msgid "Username" +msgstr "Nome de usuário" + +#. Placeholder for the password field +#: src/app/components/login/login.blp:94 +msgid "Password" +msgstr "Senha" + +#. This error is shown when authentication fails. +#: src/app/components/login/login.blp:116 +msgid "Authentication failed!" +msgstr "A autenticação falhou!" + +#. Log in button label +#: src/app/components/login/login.blp:131 +msgid "Log in" +msgstr "Entrar" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:69 +msgid "Albums" +msgstr "Álbuns" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:100 +msgid "Artists" +msgstr "Artistas" + +#: src/app/components/navigation/factory.rs:85 +#: src/app/components/sidebar/sidebar_item.rs:41 +msgid "Saved tracks" +msgstr "" + +#: src/app/components/playback/playback_controls.rs:64 +msgid "Pause" +msgstr "" + +#: src/app/components/playback/playback_controls.rs:66 +msgid "Play" +msgstr "" + +#: src/app/components/playback/playback_controls.blp:15 +msgid "Shuffle" +msgstr "" + +#: src/app/components/playback/playback_controls.blp:24 +msgid "Previous" +msgstr "" + +#: src/app/components/playback/playback_controls.blp:46 +msgid "Next" +msgstr "" + +#: src/app/components/playback/playback_controls.blp:55 +msgid "Repeat" +msgstr "" + +#. A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:26 +msgid "You have no saved playlists." +msgstr "" + +#. A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:30 +msgid "Your playlists will be shown here." +msgstr "" + +#. A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.blp:25 +msgid "You have no saved albums." +msgstr "" + +#. A description of what happens when the user has saved albums. +#: src/app/components/library/library.blp:29 +msgid "Your library will be shown here." +msgstr "" + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "{} música selecionada" +msgstr[1] "{} músicas selecionadas" + +#. Title for the empty search page (initial state). +#: src/app/components/search/search.blp:110 +msgid "Search Spotify." +msgstr "" + +#. Subtitle for the empty search page (initial state). +#: src/app/components/search/search.blp:114 +msgid "Type to search." +msgstr "" + +#: src/app/components/selection/selection_toolbar.blp:61 +msgid "Remove" +msgstr "" + +#. playlist2-symbolic +#: src/app/components/selection/selection_toolbar.blp:49 +msgid "Add to playlist..." +msgstr "" + +#. Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.blp:53 +#: src/app/components/headerbar/headerbar.blp:49 +msgid "Cancel" +msgstr "" + +#. Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.blp:63 +msgid "Select all" +msgstr "" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +msgid "More from {}" +msgstr "Mais de {}" + +#. This refers to a music label +#: src/app/components/details/release_details.blp:41 +msgid "Label" +msgstr "Editora" + +#. This refers to a release date +#: src/app/components/details/release_details.blp:52 +msgid "Released" +msgstr "Lançamento" + +#. This refers to a number of tracks +#: src/app/components/details/release_details.blp:63 +msgid "Tracks" +msgstr "Faixas" + +#. This refers to the duration of a release +#: src/app/components/details/release_details.ui:68 +msgid "Duration" +msgstr "Duração" + +#: src/app/components/details/release_details.blp:72 +msgid "Copyright" +msgstr "Copyright" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +msgid "{} by {}" +msgstr "" + +#: src/app/components/sidebar/sidebar.rs:49 +msgid "Unnamed playlist" +msgstr "" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "" + +#: src/main.rs:75 +msgid "Failed to open link!" +msgstr "" + +#. Header for a group of preference items regarding audio +#: src/app/components/settings/settings.blp:13 +msgid "Audio" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:18 +msgid "Audio Backend" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:30 +msgid "ALSA Device" +msgstr "" + +#. Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.blp:34 +msgid "Applied only if audio backend is ALSA" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:44 +msgid "Audio Quality" +msgstr "" + +#: src/app/components/settings/settings.blp:47 +msgid "Normal" +msgstr "" + +#: src/app/components/settings/settings.blp:48 +msgid "High" +msgstr "" + +#: src/app/components/settings/settings.blp:49 +msgid "Very high" +msgstr "" + +#. Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.blp:72 +msgid "Appearance" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:77 +msgid "Theme" +msgstr "" + +#: src/app/components/settings/settings.blp:80 +msgid "Light" +msgstr "" + +#: src/app/components/settings/settings.blp:81 +msgid "Dark" +msgstr "" + +#. Header for a group of preference items regarding network +#: src/app/components/settings/settings.blp:91 +msgid "Network" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:96 +msgid "Access Point Port" +msgstr "" + +#. Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.blp:100 +msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine." +msgstr "" + +#: src/app/components/selection/selection_toolbar.blp:78 +msgid "Save to library" +msgstr "" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "" + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:57 +msgid "Gapless playback" +msgstr "" + +#: src/app/components/settings/settings.blp:82 +msgid "System" +msgstr "" + +#. Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.blp:69 +msgid "Done" +msgstr "" + +#. label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.blp:10 +msgid "Name" +msgstr "" + +#. Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.blp:30 +msgid "Create" +msgstr "" + diff --git a/po/pt.po b/po/pt.po new file mode 100644 index 0000000..7a81fe8 --- /dev/null +++ b/po/pt.po @@ -0,0 +1,389 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Spot\n" +"Language: pt\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +msgid "View album" +msgstr "Ver álbum" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +msgid "Copy link" +msgstr "Copiar ligação" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.blp:36 +msgid "Add to queue" +msgstr "Adicionar à fila" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +msgid "Remove from queue" +msgstr "Remover da fila" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +msgid "Add to {}" +msgstr "Adicionar a {}" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:68 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "Não foi possível guardar a palavra-passe. Garanta que o chaveiro da sessão está desbloqueado." + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129 +msgid "An error occured. Check logs for details!" +msgstr "Ocorreu um erro! Verifique os logs para mais detalhes!" + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +msgid "Library" +msgstr "Biblioteca" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +msgid "Playlists" +msgstr "Listas de reprodução" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/now_playing/now_playing_model.rs:134 +#: src/app/components/sidebar/sidebar_item.rs:43 +msgid "Now playing" +msgstr "A reproduzir" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.blp:33 +msgid "No song playing" +msgstr "Nenhuma canção a ser reproduzida" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +msgid "About" +msgstr "Sobre" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +msgid "Quit" +msgstr "Sair" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +msgid "Log out" +msgstr "Terminar sessão" + +#: src/app/state/login_state.rs:115 +msgid "Connection restored" +msgstr "Conexão restaurada" + +#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.blp:26 +msgid "Top tracks" +msgstr "Melhores faixas" + +#. Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.blp:54 +msgid "Releases" +msgstr "Lançamentos" + +#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). +#: src/app/components/login/login.blp:49 +msgid "Login to Spotify Premium" +msgstr "Autenticar-se no Spotify Premium" + +#. Placeholder for the username field +#: src/app/components/login/login.blp:76 +msgid "Username" +msgstr "Utilizador" + +#. Placeholder for the password field +#: src/app/components/login/login.blp:94 +msgid "Password" +msgstr "Palavra-passe" + +#. This error is shown when authentication fails. +#: src/app/components/login/login.blp:116 +msgid "Authentication failed!" +msgstr "Autenticação falhou!" + +#. Log in button label +#: src/app/components/login/login.blp:131 +msgid "Log in" +msgstr "Autenticar" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:69 +msgid "Albums" +msgstr "Álbums" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:100 +msgid "Artists" +msgstr "Artistas" + +#: src/app/components/navigation/factory.rs:85 +#: src/app/components/sidebar/sidebar_item.rs:41 +msgid "Saved tracks" +msgstr "Músicas salvas" + +#: src/app/components/playback/playback_controls.rs:64 +msgid "Pause" +msgstr "Pausar" + +#: src/app/components/playback/playback_controls.rs:66 +msgid "Play" +msgstr "Tocar" + +#: src/app/components/playback/playback_controls.blp:15 +msgid "Shuffle" +msgstr "Misturar" + +#: src/app/components/playback/playback_controls.blp:24 +msgid "Previous" +msgstr "Anterior" + +#: src/app/components/playback/playback_controls.blp:46 +msgid "Next" +msgstr "Próximo" + +#: src/app/components/playback/playback_controls.blp:55 +msgid "Repeat" +msgstr "Repetir" + +#. A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:26 +msgid "You have no saved playlists." +msgstr "Não tem listas de reprodução salvas." + +#. A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:30 +msgid "Your playlists will be shown here." +msgstr "As tuas listas de reprodução aparecerão aqui." + +#. A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.blp:25 +msgid "You have no saved albums." +msgstr "Não tens albúns salvos." + +#. A description of what happens when the user has saved albums. +#: src/app/components/library/library.blp:29 +msgid "Your library will be shown here." +msgstr "A tua biblioteca vai aparecer aqui." + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "{} canção selecionada" +msgstr[1] "{} canções selecionadas" + +#. Title for the empty search page (initial state). +#: src/app/components/search/search.blp:110 +msgid "Search Spotify." +msgstr "Procurar no Spotify." + +#. Subtitle for the empty search page (initial state). +#: src/app/components/search/search.blp:114 +msgid "Type to search." +msgstr "Escrever para pesquisar." + +#: src/app/components/selection/selection_toolbar.blp:61 +msgid "Remove" +msgstr "Remover" + +#. playlist2-symbolic +#: src/app/components/selection/selection_toolbar.blp:49 +msgid "Add to playlist..." +msgstr "Adicionar à lista de reprodução..." + +#. Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.blp:53 +#: src/app/components/headerbar/headerbar.blp:49 +msgid "Cancel" +msgstr "Cancelar" + +#. Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.blp:63 +msgid "Select all" +msgstr "Selecionar todos" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +msgid "More from {}" +msgstr "Mais de {}" + +#. This refers to a music label +#: src/app/components/details/release_details.blp:41 +msgid "Label" +msgstr "Editora" + +#. This refers to a release date +#: src/app/components/details/release_details.blp:52 +msgid "Released" +msgstr "Lançamento" + +#. This refers to a number of tracks +#: src/app/components/details/release_details.blp:63 +msgid "Tracks" +msgstr "Faixas" + +#. This refers to the duration of a release +#: src/app/components/details/release_details.ui:68 +msgid "Duration" +msgstr "Duração" + +#: src/app/components/details/release_details.blp:72 +msgid "Copyright" +msgstr "Copyright" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +msgid "{} by {}" +msgstr "{} por {}" + +#: src/app/components/sidebar/sidebar.rs:49 +msgid "Unnamed playlist" +msgstr "Lista de reprodução sem nome" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "Todas as listas de reprodução" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "Músicas salvas!" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "Preferências" + +#: src/main.rs:75 +msgid "Failed to open link!" +msgstr "Falha ao abrir a ligação!" + +#. Header for a group of preference items regarding audio +#: src/app/components/settings/settings.blp:13 +msgid "Audio" +msgstr "Áudio" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:18 +msgid "Audio Backend" +msgstr "Back-end de Áudio" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:30 +msgid "ALSA Device" +msgstr "Dispositivo ALSA" + +#. Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.blp:34 +msgid "Applied only if audio backend is ALSA" +msgstr "Aplicado apenas se back-end do áudio é ALSA" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:44 +msgid "Audio Quality" +msgstr "Qualidade do Áudio" + +#: src/app/components/settings/settings.blp:47 +msgid "Normal" +msgstr "Normal" + +#: src/app/components/settings/settings.blp:48 +msgid "High" +msgstr "Alta" + +#: src/app/components/settings/settings.blp:49 +msgid "Very high" +msgstr "Muito Alta" + +#. Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.blp:72 +msgid "Appearance" +msgstr "Aparência" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:77 +msgid "Theme" +msgstr "Tema" + +#: src/app/components/settings/settings.blp:80 +msgid "Light" +msgstr "Claro" + +#: src/app/components/settings/settings.blp:81 +msgid "Dark" +msgstr "Escuro" + +#. Header for a group of preference items regarding network +#: src/app/components/settings/settings.blp:91 +msgid "Network" +msgstr "Rede" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:96 +msgid "Access Point Port" +msgstr "Porta do Ponto de Acesso" + +#. Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.blp:100 +msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine." +msgstr "Porta usada para conexões ao Ponto de Acesso do Spotify. Definir para 0 se qualquer porta servir." + +#: src/app/components/selection/selection_toolbar.blp:78 +msgid "Save to library" +msgstr "Salvar na biblioteca" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "Nova lista de reprodução criada." + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "Ver" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "Nova lista de reprodução" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:57 +msgid "Gapless playback" +msgstr "Reprodução sem intervalos" + +#: src/app/components/settings/settings.blp:82 +msgid "System" +msgstr "Sistema" + +#. Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.blp:69 +msgid "Done" +msgstr "Feito" + +#. label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.blp:10 +msgid "Name" +msgstr "Nome" + +#. Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.blp:30 +msgid "Create" +msgstr "Criar" + diff --git a/po/ru.po b/po/ru.po new file mode 100644 index 0000000..2b4e150 --- /dev/null +++ b/po/ru.po @@ -0,0 +1,391 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Spot\n" +"Language: ru\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +msgid "View album" +msgstr "Посмотреть альбом" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +msgid "Copy link" +msgstr "Скопировать ссылку" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.blp:36 +msgid "Add to queue" +msgstr "Добавить в очередь" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +msgid "Remove from queue" +msgstr "Удалить из очереди" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +msgid "Add to {}" +msgstr "Добавить в {}" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:68 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "Не удалось сохранить пароль. Убедитесь, что связка ключей сеанса разблокирована." + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129 +msgid "An error occured. Check logs for details!" +msgstr "Произошла ошибка. Смотрите подробности в логах!" + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +msgid "Library" +msgstr "Фонотека" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +msgid "Playlists" +msgstr "Плейлисты" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/now_playing/now_playing_model.rs:134 +#: src/app/components/sidebar/sidebar_item.rs:43 +msgid "Now playing" +msgstr "Воспроизводится сейчас" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.blp:33 +msgid "No song playing" +msgstr "Песня сейчас не играет" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +msgid "About" +msgstr "О Spot" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +msgid "Quit" +msgstr "Выйти из программы" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +msgid "Log out" +msgstr "Выйти из аккаунта" + +#: src/app/state/login_state.rs:115 +msgid "Connection restored" +msgstr "Подключение восстановлено" + +#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.blp:26 +msgid "Top tracks" +msgstr "Топ треков" + +#. Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.blp:54 +msgid "Releases" +msgstr "Релизы" + +#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). +#: src/app/components/login/login.blp:49 +msgid "Login to Spotify Premium" +msgstr "Войти в Spotify Premium" + +#. Placeholder for the username field +#: src/app/components/login/login.blp:76 +msgid "Username" +msgstr "Имя аккаунта" + +#. Placeholder for the password field +#: src/app/components/login/login.blp:94 +msgid "Password" +msgstr "Пароль" + +#. This error is shown when authentication fails. +#: src/app/components/login/login.blp:116 +msgid "Authentication failed!" +msgstr "Вход не удался!" + +#. Log in button label +#: src/app/components/login/login.blp:131 +msgid "Log in" +msgstr "Войти" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:69 +msgid "Albums" +msgstr "Альбомы" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:100 +msgid "Artists" +msgstr "Исполнители" + +#: src/app/components/navigation/factory.rs:85 +#: src/app/components/sidebar/sidebar_item.rs:41 +msgid "Saved tracks" +msgstr "Сохраненные треки" + +#: src/app/components/playback/playback_controls.rs:64 +msgid "Pause" +msgstr "Пауза" + +#: src/app/components/playback/playback_controls.rs:66 +msgid "Play" +msgstr "Воспроизведение" + +#: src/app/components/playback/playback_controls.blp:15 +msgid "Shuffle" +msgstr "Перемешать" + +#: src/app/components/playback/playback_controls.blp:24 +msgid "Previous" +msgstr "Предыдущий" + +#: src/app/components/playback/playback_controls.blp:46 +msgid "Next" +msgstr "Следующий" + +#: src/app/components/playback/playback_controls.blp:55 +msgid "Repeat" +msgstr "Повторить" + +#. A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:26 +msgid "You have no saved playlists." +msgstr "У вас нет сохраненных плейлистов." + +#. A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:30 +msgid "Your playlists will be shown here." +msgstr "Здесь будут показаны ваши плейлисты." + +#. A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.blp:25 +msgid "You have no saved albums." +msgstr "У вас нет сохраненных альбомов." + +#. A description of what happens when the user has saved albums. +#: src/app/components/library/library.blp:29 +msgid "Your library will be shown here." +msgstr "Здесь будет показана ваша фонотека." + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "{} композиция выбрана" +msgstr[1] "{} композиции выбраны" +msgstr[2] "{} композиций выбрано" +msgstr[3] "" + +#. Title for the empty search page (initial state). +#: src/app/components/search/search.blp:110 +msgid "Search Spotify." +msgstr "Поиск в Spotify." + +#. Subtitle for the empty search page (initial state). +#: src/app/components/search/search.blp:114 +msgid "Type to search." +msgstr "Введите для поиска." + +#: src/app/components/selection/selection_toolbar.blp:61 +msgid "Remove" +msgstr "Удалить" + +#. playlist2-symbolic +#: src/app/components/selection/selection_toolbar.blp:49 +msgid "Add to playlist..." +msgstr "Добавить в плейлист..." + +#. Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.blp:53 +#: src/app/components/headerbar/headerbar.blp:49 +msgid "Cancel" +msgstr "Отменить" + +#. Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.blp:63 +msgid "Select all" +msgstr "Выбрать все" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +msgid "More from {}" +msgstr "Больше от {}" + +#. This refers to a music label +#: src/app/components/details/release_details.blp:41 +msgid "Label" +msgstr "Лейбл" + +#. This refers to a release date +#: src/app/components/details/release_details.blp:52 +msgid "Released" +msgstr "Релизы" + +#. This refers to a number of tracks +#: src/app/components/details/release_details.blp:63 +msgid "Tracks" +msgstr "Треки" + +#. This refers to the duration of a release +#: src/app/components/details/release_details.ui:68 +msgid "Duration" +msgstr "Продолжительность" + +#: src/app/components/details/release_details.blp:72 +msgid "Copyright" +msgstr "Авторское право" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +msgid "{} by {}" +msgstr "{} в исполнении {}" + +#: src/app/components/sidebar/sidebar.rs:49 +msgid "Unnamed playlist" +msgstr "Безымянный плейлист" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "Все плейлисты" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "Треки сохранены!" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "Настройки" + +#: src/main.rs:75 +msgid "Failed to open link!" +msgstr "Не удалось открыть ссылку!" + +#. Header for a group of preference items regarding audio +#: src/app/components/settings/settings.blp:13 +msgid "Audio" +msgstr "Аудио" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:18 +msgid "Audio Backend" +msgstr "Аудио Бэкэнд" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:30 +msgid "ALSA Device" +msgstr "ALSA Device" + +#. Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.blp:34 +msgid "Applied only if audio backend is ALSA" +msgstr "Применяется, только если звуковой бэкэнд ALSA" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:44 +msgid "Audio Quality" +msgstr "Качество звука" + +#: src/app/components/settings/settings.blp:47 +msgid "Normal" +msgstr "Нормальное" + +#: src/app/components/settings/settings.blp:48 +msgid "High" +msgstr "Высокое" + +#: src/app/components/settings/settings.blp:49 +msgid "Very high" +msgstr "Очень высокое" + +#. Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.blp:72 +msgid "Appearance" +msgstr "Внешний вид" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:77 +msgid "Theme" +msgstr "Тема" + +#: src/app/components/settings/settings.blp:80 +msgid "Light" +msgstr "Светлая" + +#: src/app/components/settings/settings.blp:81 +msgid "Dark" +msgstr "Темная" + +#. Header for a group of preference items regarding network +#: src/app/components/settings/settings.blp:91 +msgid "Network" +msgstr "Сеть" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:96 +msgid "Access Point Port" +msgstr "Порт точки доступа" + +#. Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.blp:100 +msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine." +msgstr "Порт, используемый для подключения к точке доступа Spotify. Установите на 0, если все порты в порядке." + +#: src/app/components/selection/selection_toolbar.blp:78 +msgid "Save to library" +msgstr "Сохранить в библиотеку" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "Создан новый плейлист." + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "Вид" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "Новый плейлист" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:57 +msgid "Gapless playback" +msgstr "Непрерывное воспроизведение" + +#: src/app/components/settings/settings.blp:82 +msgid "System" +msgstr "Система" + +#. Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.blp:69 +msgid "Done" +msgstr "Сделано" + +#. label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.blp:10 +msgid "Name" +msgstr "Имя" + +#. Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.blp:30 +msgid "Create" +msgstr "Создать" + diff --git a/po/sl.po b/po/sl.po new file mode 100644 index 0000000..a82075f --- /dev/null +++ b/po/sl.po @@ -0,0 +1,391 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Spot\n" +"Language: sl\n" +"Plural-Forms: nplurals=4; plural=(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n%100==4 ? 3 : 0);\n" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +msgid "View album" +msgstr "Oglej si album" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +msgid "Copy link" +msgstr "Kopiraj povezavo" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.blp:36 +msgid "Add to queue" +msgstr "Dodaj v vrsto" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +msgid "Remove from queue" +msgstr "Odstrani iz vrste" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +msgid "Add to {}" +msgstr "Dodaj na {}" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:68 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "Gesla ni bilo mogoče shraniti. Preveri, da je session keyring odklenjen." + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129 +msgid "An error occured. Check logs for details!" +msgstr "Napaka! Poglej v dnevnik od programa za podrobnosti." + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +msgid "Library" +msgstr "Moja zbirka" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +msgid "Playlists" +msgstr "Seznami predvajanja" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/now_playing/now_playing_model.rs:134 +#: src/app/components/sidebar/sidebar_item.rs:43 +msgid "Now playing" +msgstr "Se predvaja" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.blp:33 +msgid "No song playing" +msgstr "Ni skladb za predvajanje" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +msgid "About" +msgstr "O programu" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +msgid "Quit" +msgstr "Izhod" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +msgid "Log out" +msgstr "Odjava" + +#: src/app/state/login_state.rs:115 +msgid "Connection restored" +msgstr "Povezava ponovno vzpovstavljena" + +#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.blp:26 +msgid "Top tracks" +msgstr "Priljubljene skladbe" + +#. Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.blp:54 +msgid "Releases" +msgstr "Izdaje" + +#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). +#: src/app/components/login/login.blp:49 +msgid "Login to Spotify Premium" +msgstr "Prijava v Spotify Premium" + +#. Placeholder for the username field +#: src/app/components/login/login.blp:76 +msgid "Username" +msgstr "Uporabniško ime" + +#. Placeholder for the password field +#: src/app/components/login/login.blp:94 +msgid "Password" +msgstr "Geslo" + +#. This error is shown when authentication fails. +#: src/app/components/login/login.blp:116 +msgid "Authentication failed!" +msgstr "Preverjanje pristnosti ni bilo uspešno!" + +#. Log in button label +#: src/app/components/login/login.blp:131 +msgid "Log in" +msgstr "Prijava" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:69 +msgid "Albums" +msgstr "Albumi" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:100 +msgid "Artists" +msgstr "Izvajalci" + +#: src/app/components/navigation/factory.rs:85 +#: src/app/components/sidebar/sidebar_item.rs:41 +msgid "Saved tracks" +msgstr "Shranjene skladbe" + +#: src/app/components/playback/playback_controls.rs:64 +msgid "Pause" +msgstr "Premor" + +#: src/app/components/playback/playback_controls.rs:66 +msgid "Play" +msgstr "Predvajaj" + +#: src/app/components/playback/playback_controls.blp:15 +msgid "Shuffle" +msgstr "Naključno predvajanje" + +#: src/app/components/playback/playback_controls.blp:24 +msgid "Previous" +msgstr "Prejšna skladba" + +#: src/app/components/playback/playback_controls.blp:46 +msgid "Next" +msgstr "Naslednja skladba" + +#: src/app/components/playback/playback_controls.blp:55 +msgid "Repeat" +msgstr "Ponavljajoče predvajanje" + +#. A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:26 +msgid "You have no saved playlists." +msgstr "Nimaš shranjenih seznamov predvajanja." + +#. A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:30 +msgid "Your playlists will be shown here." +msgstr "Tvoji seznami predvajanja bodo prikazani tukaj." + +#. A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.blp:25 +msgid "You have no saved albums." +msgstr "Nimaš shranjenih albumov." + +#. A description of what happens when the user has saved albums. +#: src/app/components/library/library.blp:29 +msgid "Your library will be shown here." +msgstr "Tvoja zbirka bo prikazana tukaj." + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "{} izbrana skladba" +msgstr[1] "{} izbrani skladbi" +msgstr[2] "{} izbrane skladbe" +msgstr[3] "{} izbranih skladb" + +#. Title for the empty search page (initial state). +#: src/app/components/search/search.blp:110 +msgid "Search Spotify." +msgstr "Razišči Spotify." + +#. Subtitle for the empty search page (initial state). +#: src/app/components/search/search.blp:114 +msgid "Type to search." +msgstr "Napiši za iskanje." + +#: src/app/components/selection/selection_toolbar.blp:61 +msgid "Remove" +msgstr "Odstrani" + +#. playlist2-symbolic +#: src/app/components/selection/selection_toolbar.blp:49 +msgid "Add to playlist..." +msgstr "Dodaj na seznam predvajanja..." + +#. Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.blp:53 +#: src/app/components/headerbar/headerbar.blp:49 +msgid "Cancel" +msgstr "Prekliči" + +#. Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.blp:63 +msgid "Select all" +msgstr "Izberi vse" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +msgid "More from {}" +msgstr "Več od {}" + +#. This refers to a music label +#: src/app/components/details/release_details.blp:41 +msgid "Label" +msgstr "Založba" + +#. This refers to a release date +#: src/app/components/details/release_details.blp:52 +msgid "Released" +msgstr "Izdano" + +#. This refers to a number of tracks +#: src/app/components/details/release_details.blp:63 +msgid "Tracks" +msgstr "Skladbe" + +#. This refers to the duration of a release +#: src/app/components/details/release_details.ui:68 +msgid "Duration" +msgstr "Trajanje" + +#: src/app/components/details/release_details.blp:72 +msgid "Copyright" +msgstr "Avtorska pravica" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +msgid "{} by {}" +msgstr "{} od {}" + +#: src/app/components/sidebar/sidebar.rs:49 +msgid "Unnamed playlist" +msgstr "" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "" + +#: src/main.rs:75 +msgid "Failed to open link!" +msgstr "" + +#. Header for a group of preference items regarding audio +#: src/app/components/settings/settings.blp:13 +msgid "Audio" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:18 +msgid "Audio Backend" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:30 +msgid "ALSA Device" +msgstr "" + +#. Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.blp:34 +msgid "Applied only if audio backend is ALSA" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:44 +msgid "Audio Quality" +msgstr "" + +#: src/app/components/settings/settings.blp:47 +msgid "Normal" +msgstr "" + +#: src/app/components/settings/settings.blp:48 +msgid "High" +msgstr "" + +#: src/app/components/settings/settings.blp:49 +msgid "Very high" +msgstr "" + +#. Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.blp:72 +msgid "Appearance" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:77 +msgid "Theme" +msgstr "" + +#: src/app/components/settings/settings.blp:80 +msgid "Light" +msgstr "" + +#: src/app/components/settings/settings.blp:81 +msgid "Dark" +msgstr "" + +#. Header for a group of preference items regarding network +#: src/app/components/settings/settings.blp:91 +msgid "Network" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:96 +msgid "Access Point Port" +msgstr "" + +#. Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.blp:100 +msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine." +msgstr "" + +#: src/app/components/selection/selection_toolbar.blp:78 +msgid "Save to library" +msgstr "" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "" + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:57 +msgid "Gapless playback" +msgstr "" + +#: src/app/components/settings/settings.blp:82 +msgid "System" +msgstr "" + +#. Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.blp:69 +msgid "Done" +msgstr "" + +#. label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.blp:10 +msgid "Name" +msgstr "" + +#. Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.blp:30 +msgid "Create" +msgstr "" + diff --git a/po/spot.pot b/po/spot.pot new file mode 100644 index 0000000..da2113f --- /dev/null +++ b/po/spot.pot @@ -0,0 +1,418 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the spot package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: spot\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-03-18 19:15-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:106 src/app/components/mod.rs:133 +msgid "An error occured. Check logs for details!" +msgstr "" + +#: src/app/components/device_selector/widget.rs:135 +#: src/app/components/device_selector/device_selector.blp:9 +#: src/app/components/device_selector/device_selector.blp:37 +msgid "This device" +msgstr "" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +msgid "View album" +msgstr "" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +msgid "Copy link" +msgstr "" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.blp:36 +msgid "Add to queue" +msgstr "" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +msgid "Remove from queue" +msgstr "" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +msgid "Add to {}" +msgstr "" + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "" +msgstr[1] "" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +msgid "More from {}" +msgstr "" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +msgid "{} by {}" +msgstr "" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:68 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "" + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +msgid "Library" +msgstr "" + +#. translators: This is a sidebar entry that marks that the entries below are playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +msgid "Playlists" +msgstr "" + +#. translators: This is a sidebar entry to browse to saved tracks. +#: src/app/components/navigation/factory.rs:76 +#: src/app/components/sidebar/sidebar_item.rs:41 +msgid "Saved tracks" +msgstr "" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "" + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "" + +#: src/app/components/playback/playback_controls.rs:64 +msgid "Pause" +msgstr "" + +#: src/app/components/playback/playback_controls.rs:66 +msgid "Play" +msgstr "" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Translators: Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.blp:33 +msgid "No song playing" +msgstr "" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:43 +msgid "Now playing" +msgstr "" + +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "" + +#: src/app/components/sidebar/sidebar.rs:49 +msgid "Unnamed playlist" +msgstr "" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +msgid "About" +msgstr "" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +msgid "Quit" +msgstr "" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +msgid "Log out" +msgstr "" + +#: src/app/state/login_state.rs:115 +msgid "Connection restored" +msgstr "" + +#: src/connect/player.rs:65 +msgid "Connection to device lost!" +msgstr "" + +#: src/main.rs:76 +msgid "Failed to open link!" +msgstr "" + +#. Translators: A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:26 +msgid "You have no saved playlists." +msgstr "" + +#. Translators: A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:30 +msgid "Your playlists will be shown here." +msgstr "" + +#. Translators: Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.blp:26 +msgid "Top tracks" +msgstr "" + +#. Translators: Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.blp:54 +msgid "Releases" +msgstr "" + +#. Translators: This is the title of a section of the search results +#: src/app/components/search/search.blp:69 +msgid "Albums" +msgstr "" + +#. Translators: This is the title of a section of the search results +#: src/app/components/search/search.blp:100 +msgid "Artists" +msgstr "" + +#. Translators: Title for the empty search page (initial state). +#: src/app/components/search/search.blp:110 +msgid "Search Spotify." +msgstr "" + +#. Translators: Subtitle for the empty search page (initial state). +#: src/app/components/search/search.blp:114 +msgid "Type to search." +msgstr "" + +#. Translators: Header for a group of preference items regarding audio +#: src/app/components/settings/settings.blp:13 +msgid "Audio" +msgstr "" + +#. Translators: Title for an item in preferences +#: src/app/components/settings/settings.blp:18 +msgid "Audio Backend" +msgstr "" + +#. Translators: Title for an item in preferences +#: src/app/components/settings/settings.blp:30 +msgid "ALSA Device" +msgstr "" + +#. Translators: Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.blp:34 +msgid "Applied only if audio backend is ALSA" +msgstr "" + +#. Translators: Title for an item in preferences +#: src/app/components/settings/settings.blp:44 +msgid "Audio Quality" +msgstr "" + +#: src/app/components/settings/settings.blp:47 +msgid "Normal" +msgstr "" + +#: src/app/components/settings/settings.blp:48 +msgid "High" +msgstr "" + +#: src/app/components/settings/settings.blp:49 +msgid "Very high" +msgstr "" + +#. Translators: Title for an item in preferences +#: src/app/components/settings/settings.blp:57 +msgid "Gapless playback" +msgstr "" + +#. Translators: Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.blp:72 +msgid "Appearance" +msgstr "" + +#. Translators: Title for an item in preferences +#: src/app/components/settings/settings.blp:77 +msgid "Theme" +msgstr "" + +#: src/app/components/settings/settings.blp:80 +msgid "Light" +msgstr "" + +#: src/app/components/settings/settings.blp:81 +msgid "Dark" +msgstr "" + +#: src/app/components/settings/settings.blp:82 +msgid "System" +msgstr "" + +#. Translators: Header for a group of preference items regarding network +#: src/app/components/settings/settings.blp:91 +msgid "Network" +msgstr "" + +#. Translators: Title for an item in preferences +#: src/app/components/settings/settings.blp:96 +msgid "Access Point Port" +msgstr "" + +#. Translators: Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.blp:100 +msgid "" +"Port used for connections to Spotify's Access Point. Set to 0 if any port is " +"fine." +msgstr "" + +#: src/app/components/selection/selection_toolbar.blp:49 +msgid "Add to playlist..." +msgstr "" + +#: src/app/components/selection/selection_toolbar.blp:61 +msgid "Remove" +msgstr "" + +#: src/app/components/selection/selection_toolbar.blp:78 +msgid "Save to library" +msgstr "" + +#. Translators: This refers to a music label +#: src/app/components/details/release_details.blp:41 +msgid "Label" +msgstr "" + +#. Translators: This refers to a release date +#: src/app/components/details/release_details.blp:52 +msgid "Released" +msgstr "" + +#. Translators: This refers to a number of tracks +#: src/app/components/details/release_details.blp:63 +msgid "Tracks" +msgstr "" + +#: src/app/components/details/release_details.blp:72 +msgid "Copyright" +msgstr "" + +#: src/app/components/login/login.blp:46 +msgid "Welcome to Spot" +msgstr "" + +#. Translators: Login window title, must mention Premium (a premium account is required). +#: src/app/components/login/login.blp:53 +msgid "" +"Log in with your Spotify Account. A Spotify Premium subscription is required " +"to use the app." +msgstr "" + +#. Translators: Placeholder for the username field +#: src/app/components/login/login.blp:66 +msgid "Username or Email" +msgstr "" + +#. Translators: Placeholder for the password field +#: src/app/components/login/login.blp:71 +msgid "Password" +msgstr "" + +#. Translators: This error is shown when authentication fails. +#: src/app/components/login/login.blp:81 +msgid "Incorrect login credentials." +msgstr "" + +#. Translators: Log in button label +#: src/app/components/login/login.blp:92 +msgid "Log in" +msgstr "" + +#. Translators: Exit playlist edition +#. Translators: Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.blp:53 +#: src/app/components/headerbar/headerbar.blp:49 +msgid "Cancel" +msgstr "" + +#. Translators: Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.blp:69 +msgid "Done" +msgstr "" + +#. Translators: Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.blp:63 +msgid "Select all" +msgstr "" + +#: src/app/components/device_selector/device_selector.blp:15 +msgid "Playing on" +msgstr "" + +#: src/app/components/device_selector/device_selector.blp:24 +msgid "Refresh devices" +msgstr "" + +#. Translators: label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.blp:10 +msgid "Name" +msgstr "" + +#. Translators: Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.blp:30 +msgid "Create" +msgstr "" + +#: src/app/components/playback/playback_controls.blp:15 +msgid "Shuffle" +msgstr "" + +#: src/app/components/playback/playback_controls.blp:24 +msgid "Previous" +msgstr "" + +#: src/app/components/playback/playback_controls.blp:46 +msgid "Next" +msgstr "" + +#: src/app/components/playback/playback_controls.blp:55 +msgid "Repeat" +msgstr "" + +#. Translators: A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.blp:25 +msgid "You have no saved albums." +msgstr "" + +#. Translators: A description of what happens when the user has saved albums. +#: src/app/components/library/library.blp:29 +msgid "Your library will be shown here." +msgstr "" diff --git a/po/tr.po b/po/tr.po new file mode 100644 index 0000000..4011113 --- /dev/null +++ b/po/tr.po @@ -0,0 +1,389 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Spot\n" +"Language: tr\n" +"Plural-Forms: nplurals=2; plural=(n>1);\n" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +msgid "View album" +msgstr "Albümü görüntüle" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +msgid "Copy link" +msgstr "Bağlantıyı kopyala" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.blp:36 +msgid "Add to queue" +msgstr "Sıraya ekle" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +msgid "Remove from queue" +msgstr "Sıradan çıkar" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +msgid "Add to {}" +msgstr "{} listesine ekle" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:68 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "Parola kaydedilemedi. Oturum anahtarlığınızın kilidinin açık olduğundan emin olun." + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129 +msgid "An error occured. Check logs for details!" +msgstr "Bir hata meydana geldi. Günlükleri kontrol edin." + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +msgid "Library" +msgstr "Kütüphane" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +msgid "Playlists" +msgstr "Oynatma listesi" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/now_playing/now_playing_model.rs:134 +#: src/app/components/sidebar/sidebar_item.rs:43 +msgid "Now playing" +msgstr "Şu an oynatılıyor" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.blp:33 +msgid "No song playing" +msgstr "Herhangi bir şarkı oynatılmıyor" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +msgid "About" +msgstr "Hakkında" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +msgid "Quit" +msgstr "Çık" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +msgid "Log out" +msgstr "Oturumdan çık" + +#: src/app/state/login_state.rs:115 +msgid "Connection restored" +msgstr "Bağlantı geri yüklendi." + +#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.blp:26 +msgid "Top tracks" +msgstr "En çok dinlenenler" + +#. Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.blp:54 +msgid "Releases" +msgstr "Parçalar" + +#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). +#: src/app/components/login/login.blp:49 +msgid "Login to Spotify Premium" +msgstr "Spotify Premium'a giriş yap" + +#. Placeholder for the username field +#: src/app/components/login/login.blp:76 +msgid "Username" +msgstr "Kullanıcı adı" + +#. Placeholder for the password field +#: src/app/components/login/login.blp:94 +msgid "Password" +msgstr "Parola" + +#. This error is shown when authentication fails. +#: src/app/components/login/login.blp:116 +msgid "Authentication failed!" +msgstr "Kimlik doğrulama başarısız." + +#. Log in button label +#: src/app/components/login/login.blp:131 +msgid "Log in" +msgstr "Giriş yap" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:69 +msgid "Albums" +msgstr "Albümler" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:100 +msgid "Artists" +msgstr "Sanatçılar" + +#: src/app/components/navigation/factory.rs:85 +#: src/app/components/sidebar/sidebar_item.rs:41 +msgid "Saved tracks" +msgstr "Kaydedilen parçalar" + +#: src/app/components/playback/playback_controls.rs:64 +msgid "Pause" +msgstr "Durdur" + +#: src/app/components/playback/playback_controls.rs:66 +msgid "Play" +msgstr "Oynat" + +#: src/app/components/playback/playback_controls.blp:15 +msgid "Shuffle" +msgstr "Karıştır" + +#: src/app/components/playback/playback_controls.blp:24 +msgid "Previous" +msgstr "Önceki" + +#: src/app/components/playback/playback_controls.blp:46 +msgid "Next" +msgstr "Sonraki" + +#: src/app/components/playback/playback_controls.blp:55 +msgid "Repeat" +msgstr "Yinele" + +#. A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:26 +msgid "You have no saved playlists." +msgstr "Kaydedilmiş oynatma listeniz yok." + +#. A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:30 +msgid "Your playlists will be shown here." +msgstr "Oynatma listeniz burada gösterilir." + +#. A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.blp:25 +msgid "You have no saved albums." +msgstr "Kaydedilmiş ablümünüz yok." + +#. A description of what happens when the user has saved albums. +#: src/app/components/library/library.blp:29 +msgid "Your library will be shown here." +msgstr "Kütüphaneniz burada gösterilir." + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "{} seçildi" +msgstr[1] "{} seçildi" + +#. Title for the empty search page (initial state). +#: src/app/components/search/search.blp:110 +msgid "Search Spotify." +msgstr "Spotify'da Ara." + +#. Subtitle for the empty search page (initial state). +#: src/app/components/search/search.blp:114 +msgid "Type to search." +msgstr "Aramak için yazın." + +#: src/app/components/selection/selection_toolbar.blp:61 +msgid "Remove" +msgstr "Kaldır" + +#. playlist2-symbolic +#: src/app/components/selection/selection_toolbar.blp:49 +msgid "Add to playlist..." +msgstr "Oynatma listesine ekle..." + +#. Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.blp:53 +#: src/app/components/headerbar/headerbar.blp:49 +msgid "Cancel" +msgstr "İptal Et" + +#. Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.blp:63 +msgid "Select all" +msgstr "Tümünü seç" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +msgid "More from {}" +msgstr "Daha fazla {}" + +#. This refers to a music label +#: src/app/components/details/release_details.blp:41 +msgid "Label" +msgstr "Yayıncı" + +#. This refers to a release date +#: src/app/components/details/release_details.blp:52 +msgid "Released" +msgstr "Yayın tarihi" + +#. This refers to a number of tracks +#: src/app/components/details/release_details.blp:63 +msgid "Tracks" +msgstr "Parça" + +#. This refers to the duration of a release +#: src/app/components/details/release_details.ui:68 +msgid "Duration" +msgstr "Süre" + +#: src/app/components/details/release_details.blp:72 +msgid "Copyright" +msgstr "Telif hakkı" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +msgid "{} by {}" +msgstr "{}, {}" + +#: src/app/components/sidebar/sidebar.rs:49 +msgid "Unnamed playlist" +msgstr "Adsız oynatma listesi" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "Tüm Oynatma Listeleri" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "Parçalar kaydedildi!" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "Tercihler" + +#: src/main.rs:75 +msgid "Failed to open link!" +msgstr "Bağlantı açılamadı!" + +#. Header for a group of preference items regarding audio +#: src/app/components/settings/settings.blp:13 +msgid "Audio" +msgstr "Ses" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:18 +msgid "Audio Backend" +msgstr "Ses Arka Ucu" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:30 +msgid "ALSA Device" +msgstr "ALSA Aygıtı" + +#. Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.blp:34 +msgid "Applied only if audio backend is ALSA" +msgstr "Sadece, ses arka ucu ALSA ise uygulanır" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:44 +msgid "Audio Quality" +msgstr "Ses Kalitesi" + +#: src/app/components/settings/settings.blp:47 +msgid "Normal" +msgstr "Normal" + +#: src/app/components/settings/settings.blp:48 +msgid "High" +msgstr "Yüksek" + +#: src/app/components/settings/settings.blp:49 +msgid "Very high" +msgstr "Çok yüksek" + +#. Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.blp:72 +msgid "Appearance" +msgstr "Görünüm" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:77 +msgid "Theme" +msgstr "Tema" + +#: src/app/components/settings/settings.blp:80 +msgid "Light" +msgstr "Açık" + +#: src/app/components/settings/settings.blp:81 +msgid "Dark" +msgstr "Koyu" + +#. Header for a group of preference items regarding network +#: src/app/components/settings/settings.blp:91 +msgid "Network" +msgstr "Ağ" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:96 +msgid "Access Point Port" +msgstr "Erişim Noktası Bağlantı Noktası" + +#. Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.blp:100 +msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine." +msgstr "Spotify Erişim Noktasına bağlanmak için kullanılan bağlantı noktası. Herhangi bir bağlantı noktası iyiyse 0'a ayarlayın." + +#: src/app/components/selection/selection_toolbar.blp:78 +msgid "Save to library" +msgstr "Kütüphaneye kaydet" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "Yeni çalma listesi oluşturuldu." + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "Görünüm" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "Yeni Çalma Listesi" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:57 +msgid "Gapless playback" +msgstr "Kesintisiz çalma" + +#: src/app/components/settings/settings.blp:82 +msgid "System" +msgstr "Sistem" + +#. Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.blp:69 +msgid "Done" +msgstr "Bitti" + +#. label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.blp:10 +msgid "Name" +msgstr "Ad" + +#. Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.blp:30 +msgid "Create" +msgstr "Oluştur" + diff --git a/po/uk.po b/po/uk.po new file mode 100644 index 0000000..1cf9170 --- /dev/null +++ b/po/uk.po @@ -0,0 +1,391 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: Spot\n" +"Language: uk\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#. translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. +#: src/app/components/labels.rs:5 +msgid "View album" +msgstr "Подивитися альбом" + +#. translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. +#: src/app/components/labels.rs:8 +msgid "Copy link" +msgstr "Копіювати посилання" + +#. translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. +#: src/app/components/labels.rs:11 +#: src/app/components/selection/selection_toolbar.blp:36 +msgid "Add to queue" +msgstr "Додати в чергу" + +#. translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. +#: src/app/components/labels.rs:14 +msgid "Remove from queue" +msgstr "Видалити з черги" + +#. translators: This is part of a larger text that says "Add to ". This text should be as short as possible. +#: src/app/components/labels.rs:21 +msgid "Add to {}" +msgstr "Додати в {}" + +#. translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). +#: src/app/components/login/login_model.rs:68 +msgid "Could not save password. Make sure the session keyring is unlocked." +msgstr "Не вдалося зберегти пароль. Переконайтесь, що зв'язка ключів сеансу розблокована." + +#. translators: This notification is the default message for unhandled errors. Logs refer to console output. +#: src/app/batch_loader.rs:91 src/app/components/mod.rs:129 +msgid "An error occured. Check logs for details!" +msgstr "Сталася помилка. Перевірте журнал для детальної інформації!" + +#. translators: This is a sidebar entry to browse to saved albums. +#: src/app/components/navigation/factory.rs:33 +#: src/app/components/sidebar/sidebar_item.rs:39 +msgid "Library" +msgstr "Бібліотека" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/navigation/factory.rs:54 +#: src/app/components/sidebar/sidebar_item.rs:45 +msgid "Playlists" +msgstr "Плейлисти" + +#. This is the visible name for the play queue. It appears in the sidebar as well. +#: src/app/components/now_playing/now_playing_model.rs:134 +#: src/app/components/sidebar/sidebar_item.rs:43 +msgid "Now playing" +msgstr "Відтворюється зараз" + +#. translators: Short text displayed instead of a song title when nothing plays +#. Short text displayed instead of a song title when nothing plays +#: src/app/components/playback/playback_info.rs:58 +#: src/app/components/playback/playback_info.blp:33 +msgid "No song playing" +msgstr "Пісня зараз не грає" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:67 +msgid "About" +msgstr "Про Spot" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:69 +msgid "Quit" +msgstr "Вийти з програми" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:74 +msgid "Log out" +msgstr "Вийти з аккаунту" + +#: src/app/state/login_state.rs:115 +msgid "Connection restored" +msgstr "З'єднання відновлено" + +#. Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. +#: src/app/components/artist_details/artist_details.blp:26 +msgid "Top tracks" +msgstr "Топ треків" + +#. Title of the sections that contains all releases from an artist (both singles and albums). +#: src/app/components/artist_details/artist_details.blp:54 +msgid "Releases" +msgstr "Релізи" + +#. Login window title -- shouldn't be too long, but must mention Premium (a premium account is required). +#: src/app/components/login/login.blp:49 +msgid "Login to Spotify Premium" +msgstr "Увійти в Spotify Premium" + +#. Placeholder for the username field +#: src/app/components/login/login.blp:76 +msgid "Username" +msgstr "Логін" + +#. Placeholder for the password field +#: src/app/components/login/login.blp:94 +msgid "Password" +msgstr "Пароль" + +#. This error is shown when authentication fails. +#: src/app/components/login/login.blp:116 +msgid "Authentication failed!" +msgstr "Помилка входу!" + +#. Log in button label +#: src/app/components/login/login.blp:131 +msgid "Log in" +msgstr "Увійти" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:69 +msgid "Albums" +msgstr "Альбоми" + +#. This is the title of a section of the search results +#: src/app/components/search/search.blp:100 +msgid "Artists" +msgstr "Виконавці" + +#: src/app/components/navigation/factory.rs:85 +#: src/app/components/sidebar/sidebar_item.rs:41 +msgid "Saved tracks" +msgstr "" + +#: src/app/components/playback/playback_controls.rs:64 +msgid "Pause" +msgstr "" + +#: src/app/components/playback/playback_controls.rs:66 +msgid "Play" +msgstr "" + +#: src/app/components/playback/playback_controls.blp:15 +msgid "Shuffle" +msgstr "" + +#: src/app/components/playback/playback_controls.blp:24 +msgid "Previous" +msgstr "" + +#: src/app/components/playback/playback_controls.blp:46 +msgid "Next" +msgstr "" + +#: src/app/components/playback/playback_controls.blp:55 +msgid "Repeat" +msgstr "" + +#. A title that is shown when the user has not saved any playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:26 +msgid "You have no saved playlists." +msgstr "" + +#. A description of what happens when the user has saved playlists. +#: src/app/components/saved_playlists/saved_playlists.blp:30 +msgid "Your playlists will be shown here." +msgstr "" + +#. A title that is shown when the user has not saved any albums. +#: src/app/components/library/library.blp:25 +msgid "You have no saved albums." +msgstr "" + +#. A description of what happens when the user has saved albums. +#: src/app/components/library/library.blp:29 +msgid "Your library will be shown here." +msgstr "" + +#. translators: This shows up when in selection mode. This text should be as short as possible. +#: src/app/components/labels.rs:30 +msgid "{} song selected" +msgid_plural "{} songs selected" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" + +#. Title for the empty search page (initial state). +#: src/app/components/search/search.blp:110 +msgid "Search Spotify." +msgstr "" + +#. Subtitle for the empty search page (initial state). +#: src/app/components/search/search.blp:114 +msgid "Type to search." +msgstr "" + +#: src/app/components/selection/selection_toolbar.blp:61 +msgid "Remove" +msgstr "" + +#. playlist2-symbolic +#: src/app/components/selection/selection_toolbar.blp:49 +msgid "Add to playlist..." +msgstr "" + +#. Button label. Exits selection mode. +#: src/app/components/playlist_details/playlist_headerbar.blp:53 +#: src/app/components/headerbar/headerbar.blp:49 +msgid "Cancel" +msgstr "" + +#. Button label. Selects all visible songs. +#: src/app/components/headerbar/headerbar.blp:63 +msgid "Select all" +msgstr "" + +#. translators: This is part of a contextual menu attached to a single track; the full text is "More from ". +#: src/app/components/labels.rs:39 +msgid "More from {}" +msgstr "" + +#. This refers to a music label +#: src/app/components/details/release_details.blp:41 +msgid "Label" +msgstr "" + +#. This refers to a release date +#: src/app/components/details/release_details.blp:52 +msgid "Released" +msgstr "" + +#. This refers to a number of tracks +#: src/app/components/details/release_details.blp:63 +msgid "Tracks" +msgstr "" + +#. This refers to the duration of a release +#: src/app/components/details/release_details.ui:68 +msgid "Duration" +msgstr "" + +#: src/app/components/details/release_details.blp:72 +msgid "Copyright" +msgstr "" + +#. translators: This is part of a larger label that reads " by " +#: src/app/components/labels.rs:48 +msgid "{} by {}" +msgstr "" + +#: src/app/components/sidebar/sidebar.rs:49 +msgid "Unnamed playlist" +msgstr "" + +#. translators: This is a sidebar entry to browse to saved playlists. +#: src/app/components/sidebar/sidebar_item.rs:81 +msgid "All Playlists" +msgstr "" + +#: src/app/components/selection/component.rs:66 +msgid "Tracks saved!" +msgstr "" + +#. translators: This is a menu entry. +#: src/app/components/user_menu/user_menu.rs:65 +msgid "Preferences" +msgstr "" + +#: src/main.rs:75 +msgid "Failed to open link!" +msgstr "" + +#. Header for a group of preference items regarding audio +#: src/app/components/settings/settings.blp:13 +msgid "Audio" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:18 +msgid "Audio Backend" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:30 +msgid "ALSA Device" +msgstr "" + +#. Description for the item (ALSA Device) in preferences +#: src/app/components/settings/settings.blp:34 +msgid "Applied only if audio backend is ALSA" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:44 +msgid "Audio Quality" +msgstr "" + +#: src/app/components/settings/settings.blp:47 +msgid "Normal" +msgstr "" + +#: src/app/components/settings/settings.blp:48 +msgid "High" +msgstr "" + +#: src/app/components/settings/settings.blp:49 +msgid "Very high" +msgstr "" + +#. Header for a group of preference items regarding the application's appearance +#: src/app/components/settings/settings.blp:72 +msgid "Appearance" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:77 +msgid "Theme" +msgstr "" + +#: src/app/components/settings/settings.blp:80 +msgid "Light" +msgstr "" + +#: src/app/components/settings/settings.blp:81 +msgid "Dark" +msgstr "" + +#. Header for a group of preference items regarding network +#: src/app/components/settings/settings.blp:91 +msgid "Network" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:96 +msgid "Access Point Port" +msgstr "" + +#. Longer description for an item (Access Point Port) in preferences +#: src/app/components/settings/settings.blp:100 +msgid "Port used for connections to Spotify's Access Point. Set to 0 if any port is fine." +msgstr "" + +#: src/app/components/selection/selection_toolbar.blp:78 +msgid "Save to library" +msgstr "" + +#. translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. +#: src/app/components/notification/mod.rs:25 +msgid "New playlist created." +msgstr "" + +#. translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. +#: src/app/components/notification/mod.rs:27 +msgid "View" +msgstr "" + +#: src/app/components/sidebar/sidebar_item.rs:90 +msgid "New Playlist" +msgstr "" + +#. Title for an item in preferences +#: src/app/components/settings/settings.blp:57 +msgid "Gapless playback" +msgstr "" + +#: src/app/components/settings/settings.blp:82 +msgid "System" +msgstr "" + +#. Finish playlist edition +#: src/app/components/playlist_details/playlist_headerbar.blp:69 +msgid "Done" +msgstr "" + +#. label for the entry containing the name of a new playlist +#: src/app/components/sidebar/create_playlist.blp:10 +msgid "Name" +msgstr "" + +#. Button that creates a new playlist +#: src/app/components/sidebar/create_playlist.blp:30 +msgid "Create" +msgstr "" + diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..cf71118 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,3 @@ +#use_small_heuristics = "Max" +newline_style = "Unix" +use_field_init_shorthand = true diff --git a/spot.nix b/spot.nix new file mode 100644 index 0000000..a613875 --- /dev/null +++ b/spot.nix @@ -0,0 +1,73 @@ +{ lib +, stdenv +, fetchgit +, nix-update-script +, meson +, ninja +, gettext +, desktop-file-utils +, cargo +, rustPlatform +, rustc +, pkg-config +, glib +, libadwaita +, libhandy +, gtk4 +, openssl +, alsa-lib +, libpulseaudio +, wrapGAppsHook4 +, blueprint-compiler +, gst_all_1 +}: + +rustPlatform.buildRustPackage rec { + pname = "spot"; + version = "0.6.0"; + + src = fetchgit { + url = "https://github.com/stevenleadbeater/spot.git"; + rev = "b2291e0834d9a1a6e7ce1020fc88c483dfbc0b24"; + hash = "sha256-JFBwTAYKBwmjUI3B67zB60dLM+DjIzzbO/1iLsn+5TE="; + }; + cargoHash = "sha256-HIjA+oDFegsnnV3EViaWfsNhmqPB1yl8rIVS5LbTu/A="; + + nativeBuildInputs = [ + gettext + meson + ninja + pkg-config + gtk4 # for gtk-update-icon-cache + glib # for glib-compile-schemas + desktop-file-utils + cargo + rustPlatform.cargoSetupHook + rustc + wrapGAppsHook4 + blueprint-compiler + ]; + + buildInputs = [ + glib + gtk4 + libadwaita + libhandy + openssl + alsa-lib + libpulseaudio + gst_all_1.gst-plugins-base + gst_all_1.gstreamer + ]; + + # https://github.com/xou816/spot/issues/313 + mesonBuildType = "release"; + + meta = { + description = "Native Spotify client for the GNOME desktop"; + mainProgram = "spot"; + homepage = "https://github.com/stevenleadbeater/spot"; + maintainers = []; + }; +} + diff --git a/src/api/api_models.rs b/src/api/api_models.rs new file mode 100644 index 0000000..bc1089b --- /dev/null +++ b/src/api/api_models.rs @@ -0,0 +1,689 @@ +use form_urlencoded::Serializer; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashSet, + convert::{Into, TryFrom, TryInto}, + vec::IntoIter, +}; + +use crate::app::{models::*, SongsSource}; + +#[derive(Serialize)] +pub struct PlaylistDetails { + pub name: String, +} + +#[derive(Serialize)] +pub struct Uris { + pub uris: Vec, +} + +#[derive(Serialize)] +pub struct PlayOffset { + pub position: u32, +} + +#[derive(Serialize)] +#[serde(untagged)] +pub enum PlayRequest { + Contextual { + context_uri: String, + offset: PlayOffset, + }, + Uris { + uris: Vec, + offset: PlayOffset, + }, +} + +#[derive(Serialize)] +pub struct Ids { + pub ids: Vec, +} + +#[derive(Serialize)] +pub struct Name<'a> { + pub name: &'a str, +} + +pub enum SearchType { + Artist, + Album, +} + +impl SearchType { + fn into_string(self) -> &'static str { + match self { + Self::Artist => "artist", + Self::Album => "album", + } + } +} + +pub struct SearchQuery { + pub query: String, + pub types: Vec, + pub limit: usize, + pub offset: usize, +} + +impl SearchQuery { + pub fn into_query_string(self) -> String { + let mut types = self + .types + .into_iter() + .fold(String::new(), |acc, t| acc + t.into_string() + ","); + types.pop(); + + let re = Regex::new(r"(\W|\s)+").unwrap(); + let query = re.replace_all(&self.query[..], " "); + + let serialized = Serializer::new(String::new()) + .append_pair("q", query.as_ref()) + .append_pair("offset", &self.offset.to_string()[..]) + .append_pair("limit", &self.limit.to_string()[..]) + .append_pair("market", "from_token") + .finish(); + + format!("type={types}&{serialized}") + } +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Page { + items: Option>, + offset: Option, + limit: Option, + total: usize, +} + +impl Page { + fn new(items: Vec) -> Self { + let l = items.len(); + Self { + total: l, + items: Some(items), + offset: Some(0), + limit: Some(l), + } + } + + fn map(self, mapper: Mapper) -> Page + where + Mapper: Fn(T) -> U, + { + let Page { + items, + offset, + limit, + total, + } = self; + Page { + items: items.map(|item| item.into_iter().map(mapper).collect()), + offset, + limit, + total, + } + } + + pub fn limit(&self) -> usize { + self.limit + .or_else(|| Some(self.items.as_ref()?.len())) + .filter(|limit| *limit > 0) + .unwrap_or(50) + } + + pub fn total(&self) -> usize { + self.total + } + + pub fn offset(&self) -> usize { + self.offset.unwrap_or(0) + } +} + +impl IntoIterator for Page { + type Item = T; + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.items.unwrap_or_default().into_iter() + } +} + +impl Default for Page { + fn default() -> Self { + Self { + items: None, + total: 0, + offset: Some(0), + limit: Some(0), + } + } +} + +trait WithImages { + fn images(&self) -> &[Image]; + + fn best_image T>(&self, criterion: F) -> Option<&Image> { + let mut ords = self + .images() + .iter() + .map(|image| (criterion(image), image)) + .collect::>(); + + ords.sort_by(|a, b| (a.0).partial_cmp(&b.0).unwrap()); + Some(ords.first()?.1) + } + + fn best_image_for_width(&self, width: i32) -> Option<&Image> { + self.best_image(|i| (width - i.width.unwrap_or(0) as i32).abs()) + } +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Playlist { + pub id: String, + pub name: String, + pub images: Vec, + pub tracks: Page, + pub owner: PlaylistOwner, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct PlaylistOwner { + pub id: String, + pub display_name: String, +} + +impl WithImages for Playlist { + fn images(&self) -> &[Image] { + &self.images[..] + } +} + +#[derive(Deserialize, Debug, Clone)] +pub struct PlaylistTrack { + pub is_local: bool, + pub track: Option, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct SavedTrack { + pub added_at: String, + pub track: TrackItem, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct SavedAlbum { + pub album: Album, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct FullAlbum { + #[serde(flatten)] + pub album: Album, + #[serde(flatten)] + pub album_info: AlbumInfo, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Album { + pub id: String, + pub tracks: Option>, + pub artists: Vec, + pub release_date: Option, + pub name: String, + pub images: Vec, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct AlbumInfo { + pub label: String, + pub copyrights: Vec, + pub total_tracks: u32, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Copyright { + pub text: String, + #[serde(alias = "type")] + pub type_: char, +} + +impl WithImages for Album { + fn images(&self) -> &[Image] { + &self.images[..] + } +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Image { + pub url: String, + pub height: Option, + pub width: Option, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Artist { + pub id: String, + pub name: String, + pub images: Option>, +} + +impl WithImages for Artist { + fn images(&self) -> &[Image] { + #[allow(clippy::manual_unwrap_or_default)] + if let Some(ref images) = self.images { + images + } else { + &[] + } + } +} + +#[derive(Deserialize, Debug, Clone)] +pub struct User { + pub id: String, + pub display_name: String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Device { + #[serde(alias = "type")] + pub type_: String, + pub name: String, + pub id: String, + pub is_active: bool, + pub is_restricted: bool, + pub volume_percent: u32, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Devices { + pub devices: Vec, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct PlayerQueue { + pub currently_playing: TrackItem, + pub queue: Vec, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct PlayerContext { + #[serde(alias = "type")] + pub type_: String, + pub uri: String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct PlayerState { + pub progress_ms: u32, + pub is_playing: bool, + pub repeat_state: String, + pub shuffle_state: bool, + pub item: FailibleTrackItem, + pub context: Option, +} + +impl From for ConnectPlayerState { + fn from( + PlayerState { + progress_ms, + is_playing, + repeat_state, + shuffle_state, + item, + context, + }: PlayerState, + ) -> Self { + let repeat = match &repeat_state[..] { + "track" => RepeatMode::Song, + "context" => RepeatMode::Playlist, + _ => RepeatMode::None, + }; + let source = context.and_then(|PlayerContext { type_, uri }| match type_.as_str() { + "album" => { + let id = uri.split(':').last().unwrap_or_default(); + Some(SongsSource::Album(id.to_string())) + } + _ => None, + }); + let shuffle = shuffle_state; + let current_song_id = item.get().map(|i| i.track.id); + Self { + is_playing, + progress_ms, + repeat, + shuffle, + source, + current_song_id, + } + } +} + +#[derive(Deserialize, Debug, Clone)] +pub struct TopTracks { + pub tracks: Vec, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct AlbumTrackItem { + pub id: String, + pub track_number: Option, + pub uri: String, + pub name: String, + pub duration_ms: i64, + pub artists: Vec, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct TrackItem { + #[serde(flatten)] + pub track: AlbumTrackItem, + pub album: Album, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct BadTrackItem {} + +#[derive(Deserialize, Debug, Clone)] +#[serde(untagged)] +pub enum FailibleTrackItem { + Ok(Box), + Failing(BadTrackItem), +} + +impl FailibleTrackItem { + fn get(self) -> Option { + match self { + Self::Ok(track) => Some(*track), + Self::Failing(_) => None, + } + } +} + +#[derive(Deserialize, Debug, Clone)] +pub struct RawSearchResults { + pub albums: Option>, + pub artists: Option>, +} + +impl From for ArtistSummary { + fn from(artist: Artist) -> Self { + let photo = artist.best_image_for_width(200).map(|i| &i.url).cloned(); + let Artist { id, name, .. } = artist; + Self { id, name, photo } + } +} + +impl TryFrom for TrackItem { + type Error = (); + + fn try_from(PlaylistTrack { is_local, track }: PlaylistTrack) -> Result { + track.ok_or(())?.get().filter(|_| !is_local).ok_or(()) + } +} + +impl From for TrackItem { + fn from(track: SavedTrack) -> Self { + track.track + } +} + +impl From for Vec { + fn from( + PlayerQueue { + mut queue, + currently_playing, + }: PlayerQueue, + ) -> Self { + let mut ids = HashSet::::new(); + queue.insert(0, currently_playing); + let queue: Vec = queue + .into_iter() + .take_while(|e| { + if ids.contains(&e.track.id) { + false + } else { + ids.insert(e.track.id.clone()); + true + } + }) + .collect(); + Page::new(queue).into() + } +} + +impl From for Vec { + fn from(top_tracks: TopTracks) -> Self { + Page::new(top_tracks.tracks).into() + } +} + +impl From> for Vec +where + T: TryInto, +{ + fn from(page: Page) -> Self { + SongBatch::from(page).songs + } +} + +impl From<(Page, &Album)> for SongBatch { + fn from(page_and_album: (Page, &Album)) -> Self { + let (page, album) = page_and_album; + Self::from(page.map(|track| TrackItem { + track, + album: album.clone(), + })) + } +} + +impl From> for SongBatch +where + T: TryInto, +{ + fn from(page: Page) -> Self { + let batch = Batch { + offset: page.offset(), + batch_size: page.limit(), + total: page.total(), + }; + let songs = page + .into_iter() + .filter_map(|t| { + let TrackItem { track, album } = t.try_into().ok()?; + let AlbumTrackItem { + artists, + id, + uri, + name, + duration_ms, + track_number, + } = track; + let artists = artists + .into_iter() + .map(|a| ArtistRef { + id: a.id, + name: a.name, + }) + .collect::>(); + + let art = album.best_image_for_width(200).map(|i| &i.url).cloned(); + let Album { + id: album_id, + name: album_name, + .. + } = album; + + let album_ref = AlbumRef { + id: album_id, + name: album_name, + }; + + Some(SongDescription { + id, + track_number: track_number.map(|u| u as u32), + uri, + title: name, + artists, + album: album_ref, + duration: duration_ms as u32, + art, + }) + }) + .collect(); + SongBatch { songs, batch } + } +} + +impl TryFrom for SongBatch { + type Error = (); + + fn try_from(mut album: Album) -> Result { + let tracks = album.tracks.take().ok_or(())?; + Ok((tracks, &album).into()) + } +} + +impl From for AlbumFullDescription { + fn from(full_album: FullAlbum) -> Self { + let description = full_album.album.into(); + let release_details = full_album.album_info.into(); + Self { + description, + release_details, + } + } +} + +impl From for AlbumDescription { + fn from(album: Album) -> Self { + let artists = album + .artists + .iter() + .map(|a| ArtistRef { + id: a.id.clone(), + name: a.name.clone(), + }) + .collect::>(); + let songs = album + .clone() + .try_into() + .unwrap_or_else(|_| SongBatch::empty()); + let art = album.best_image_for_width(200).map(|i| i.url.clone()); + + Self { + id: album.id, + title: album.name, + artists, + release_date: album.release_date, + art, + songs, + is_liked: false, + } + } +} + +impl From for AlbumReleaseDetails { + fn from( + AlbumInfo { + label, + copyrights, + total_tracks, + }: AlbumInfo, + ) -> Self { + let copyright_text = copyrights + .iter() + .map(|Copyright { type_, text }| format!("[{type_}] {text}")) + .collect::>() + .join(",\n "); + + Self { + label, + copyright_text, + total_tracks: total_tracks as usize, + } + } +} + +impl From for PlaylistDescription { + fn from(playlist: Playlist) -> Self { + let art = playlist.best_image_for_width(200).map(|i| i.url.clone()); + let Playlist { + id, + name, + tracks, + owner, + .. + } = playlist; + let PlaylistOwner { + id: owner_id, + display_name, + } = owner; + let song_batch = tracks.into(); + PlaylistDescription { + id, + title: name, + art, + songs: song_batch, + owner: UserRef { + id: owner_id, + display_name, + }, + } + } +} + +impl From for ConnectDevice { + fn from( + Device { + id, name, type_, .. + }: Device, + ) -> Self { + let kind = match type_.to_lowercase().as_str() { + "smartphone" => ConnectDeviceKind::Phone, + "computer" => ConnectDeviceKind::Computer, + "speaker" => ConnectDeviceKind::Speaker, + _ => ConnectDeviceKind::Other, + }; + Self { + id, + label: name, + kind, + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_playlist_track_null() { + let track = r#"{"is_local": false, "track": null}"#; + let deserialized: PlaylistTrack = serde_json::from_str(track).unwrap(); + let track_item: Option = deserialized.try_into().ok(); + assert!(track_item.is_none()); + } + + #[test] + fn test_playlist_track_local() { + let track = r#"{"is_local": true, "track": {"name": ""}}"#; + let deserialized: PlaylistTrack = serde_json::from_str(track).unwrap(); + let track_item: Option = deserialized.try_into().ok(); + assert!(track_item.is_none()); + } + + #[test] + fn test_playlist_track_ok() { + let track = r#"{"is_local":false,"track":{"album":{"artists":[{"external_urls":{"spotify":""},"href":"","id":"","name":"","type":"artist","uri":""}],"id":"","images":[{"height":64,"url":"","width":64}],"name":""},"artists":[{"id":"","name":""}],"duration_ms":1,"id":"","name":"","uri":""}}"#; + let deserialized: PlaylistTrack = serde_json::from_str(track).unwrap(); + let track_item: Option = deserialized.try_into().ok(); + assert!(track_item.is_some()); + } +} diff --git a/src/api/cache.rs b/src/api/cache.rs new file mode 100644 index 0000000..b7e38d5 --- /dev/null +++ b/src/api/cache.rs @@ -0,0 +1,289 @@ +use async_std::fs; +use async_std::io; +use async_std::path::Path; +use async_std::path::PathBuf; +use async_std::prelude::*; +use core::mem::size_of; +use futures::join; +use regex::Regex; +use std::convert::From; +use std::future::Future; +use std::time::{Duration, SystemTime}; +use thiserror::Error; + +const EXPIRY_FILE_EXT: &str = ".expiry"; + +#[derive(Error, Debug)] +pub enum CacheError { + #[error("No content available")] + NoContent, + #[error("File could not be saved to cache: {0}")] + WriteError(std::io::Error), + #[error("File could not be read from cache: {0}")] + ReadError(std::io::Error), + #[error("File could not be removed from cache: {0}")] + RemoveError(std::io::Error), + #[error(transparent)] + ConversionError(#[from] std::string::FromUtf8Error), +} + +pub type ETag = String; + +pub enum CacheFile { + Fresh(Vec), + Expired(Vec, Option), + None, +} + +#[derive(PartialEq, Clone, Copy, Debug)] +pub enum CachePolicy { + Default, // query remote cache when stale + IgnoreExpiry, // always use cached value + Revalidate, // always query remote cache + IgnoreCached, // ignore cache alltogether +} + +#[derive(PartialEq, Clone, Debug)] +pub enum CacheExpiry { + Never, + AtUnixTimestamp(Duration, Option), +} + +impl CacheExpiry { + pub fn expire_in_seconds(seconds: u64, etag: Option) -> Self { + let timestamp = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + Self::AtUnixTimestamp(timestamp + Duration::new(seconds, 0), etag) + } + + fn is_expired(&self) -> bool { + match self { + Self::Never => false, + Self::AtUnixTimestamp(ref duration, _) => { + let now = &SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + now > duration + } + } + } + + fn etag(&self) -> Option<&String> { + match self { + Self::Never => None, + Self::AtUnixTimestamp(_, ref etag) => etag.as_ref(), + } + } +} + +#[derive(Clone)] +pub struct CacheManager { + root: PathBuf, +} + +impl CacheManager { + pub fn for_dir(dir: &str) -> Option { + let root: PathBuf = glib::user_cache_dir().into(); + let root = root.join(dir); + let mask = 0o744; + + glib::mkdir_with_parents(&root, mask); + + Some(Self { root }) + } + + fn cache_path(&self, resource: &str) -> PathBuf { + self.root.join(resource) + } + + fn cache_meta_path(&self, resource: &str) -> PathBuf { + let full = resource.to_string() + EXPIRY_FILE_EXT; + self.root.join(full) + } +} + +impl CacheManager { + async fn read_expiry_file(&self, resource: &str) -> Result { + let expiry_file = self.cache_meta_path(resource); + match fs::read(&expiry_file).await { + Err(e) => match e.kind() { + io::ErrorKind::NotFound => Ok(CacheExpiry::Never), + _ => Err(CacheError::ReadError(e)), + }, + Ok(buffer) => { + const OFFSET: usize = size_of::(); + + let mut duration: [u8; OFFSET] = Default::default(); + duration.copy_from_slice(&buffer[..OFFSET]); + let duration = Duration::from_secs(u64::from_be_bytes(duration)); + + let etag = String::from_utf8(buffer[OFFSET..].to_vec()).ok(); + + Ok(CacheExpiry::AtUnixTimestamp(duration, etag)) + } + } + } + + pub async fn read_cache_file( + &self, + resource: &str, + policy: CachePolicy, + ) -> Result { + if matches!(policy, CachePolicy::IgnoreCached) { + return Ok(CacheFile::None); + } + + let path = self.cache_path(resource); + let (file, expiry) = join!(fs::read(&path), self.read_expiry_file(resource)); + + match (file, policy) { + (Ok(buf), CachePolicy::IgnoreExpiry) => Ok(CacheFile::Fresh(buf)), + (Ok(buf), CachePolicy::Revalidate) => { + let expiry = expiry.unwrap_or(CacheExpiry::Never); + let etag = expiry.etag().cloned(); + Ok(CacheFile::Expired(buf, etag)) + } + (Ok(buf), CachePolicy::Default) => { + let expiry = expiry?; + let etag = expiry.etag().cloned(); + Ok(if expiry.is_expired() { + CacheFile::Expired(buf, etag) + } else { + CacheFile::Fresh(buf) + }) + } + (_, CachePolicy::IgnoreCached) => Ok(CacheFile::None), + (Err(e), _) => match e.kind() { + io::ErrorKind::NotFound => Ok(CacheFile::None), + _ => Err(CacheError::ReadError(e)), + }, + } + } +} + +impl CacheManager { + async fn set_expiry_for_path( + &self, + path: &PathBuf, + expiry: CacheExpiry, + ) -> Result<(), CacheError> { + if let CacheExpiry::AtUnixTimestamp(duration, etag) = expiry { + let mut content = duration.as_secs().to_be_bytes().to_vec(); + if let Some(etag) = etag { + content.append(&mut etag.into_bytes()); + } + fs::write(path, content) + .await + .map_err(CacheError::WriteError)?; + } + Ok(()) + } + + pub async fn clear_cache_pattern(&self, regex: &Regex) -> Result<(), CacheError> { + let mut entries = fs::read_dir(&self.root) + .await + .map_err(CacheError::ReadError)?; + + while let Some(Ok(entry)) = entries.next().await { + let matches = entry + .file_name() + .to_str() + .map(|s| regex.is_match(s)) + .unwrap_or(false); + if matches { + info!("Removing {}...", entry.file_name().to_str().unwrap_or("")); + fs::remove_file(entry.path()) + .await + .map_err(CacheError::RemoveError)?; + if let Some(expiry_file_path) = entry + .path() + .to_str() + .map(|path| path.to_string() + EXPIRY_FILE_EXT) + { + let _ = fs::remove_file(Path::new(&expiry_file_path)).await; + } + } + } + + Ok(()) + } + + pub async fn set_expired_pattern(&self, regex: &Regex) -> Result<(), CacheError> { + let mut entries = fs::read_dir(&self.root) + .await + .map_err(CacheError::ReadError)?; + + while let Some(Ok(entry)) = entries.next().await { + let matches = entry + .file_name() + .to_str() + .and_then(|s| s.strip_suffix(EXPIRY_FILE_EXT)) + .map(|s| regex.is_match(s)) + .unwrap_or(false); + if matches { + self.set_expiry_for_path(&entry.path(), CacheExpiry::expire_in_seconds(0, None)) + .await?; + } + } + + Ok(()) + } + + pub async fn write_cache_file( + &self, + resource: &str, + content: &[u8], + expiry: CacheExpiry, + ) -> Result<(), CacheError> { + let file = self.cache_path(resource); + let meta = self.cache_meta_path(resource); + let (r1, r2) = join!( + fs::write(&file, content), + self.set_expiry_for_path(&meta, expiry) + ); + r1.map_err(CacheError::WriteError)?; + r2?; + Ok(()) + } + + pub async fn get_or_write( + &self, + resource: &str, + policy: CachePolicy, + fetch: F, + ) -> Result, E> + where + O: Future>, + F: FnOnce(Option) -> O, + E: From, + { + let file = self.read_cache_file(resource, policy).await?; + match file { + CacheFile::Fresh(buf) => Ok(buf), + CacheFile::Expired(buf, etag) => match fetch(etag).await? { + FetchResult::NotModified(expiry) => { + let meta = self.cache_meta_path(resource); + self.set_expiry_for_path(&meta, expiry).await?; + Ok(buf) + } + FetchResult::Modified(fresh, expiry) => { + self.write_cache_file(resource, &fresh, expiry).await?; + Ok(fresh) + } + }, + CacheFile::None => match fetch(None).await? { + FetchResult::NotModified(_) => Err(E::from(CacheError::NoContent)), + FetchResult::Modified(fresh, expiry) => { + self.write_cache_file(resource, &fresh, expiry).await?; + Ok(fresh) + } + }, + } + } +} + +pub enum FetchResult { + NotModified(CacheExpiry), + Modified(Vec, CacheExpiry), +} diff --git a/src/api/cached_client.rs b/src/api/cached_client.rs new file mode 100644 index 0000000..16b87ff --- /dev/null +++ b/src/api/cached_client.rs @@ -0,0 +1,876 @@ +use futures::future::BoxFuture; +use futures::{join, FutureExt}; +use regex::Regex; +use serde::de::DeserializeOwned; +use serde_json::from_slice; +use std::convert::Into; +use std::future::Future; + +use super::cache::{CacheExpiry, CacheManager, CachePolicy, FetchResult}; +use super::client::*; +use crate::app::models::*; + +pub type SpotifyResult = Result; + +pub trait SpotifyApiClient { + fn get_artist(&self, id: &str) -> BoxFuture>; + + fn get_album(&self, id: &str) -> BoxFuture>; + + fn get_album_tracks( + &self, + id: &str, + offset: usize, + limit: usize, + ) -> BoxFuture>; + + fn get_playlist(&self, id: &str) -> BoxFuture>; + + fn get_playlist_tracks( + &self, + id: &str, + offset: usize, + limit: usize, + ) -> BoxFuture>; + + fn get_saved_albums( + &self, + offset: usize, + limit: usize, + ) -> BoxFuture>>; + + fn get_saved_tracks(&self, offset: usize, limit: usize) -> BoxFuture>; + + fn save_album(&self, id: &str) -> BoxFuture>; + + fn save_tracks(&self, ids: Vec) -> BoxFuture>; + + fn remove_saved_album(&self, id: &str) -> BoxFuture>; + + fn remove_saved_tracks(&self, ids: Vec) -> BoxFuture>; + + fn get_saved_playlists( + &self, + offset: usize, + limit: usize, + ) -> BoxFuture>>; + + fn add_to_playlist(&self, id: &str, uris: Vec) -> BoxFuture>; + + fn create_new_playlist( + &self, + name: &str, + user_id: &str, + ) -> BoxFuture>; + + fn remove_from_playlist(&self, id: &str, uris: Vec) -> BoxFuture>; + + fn update_playlist_details(&self, id: &str, name: String) -> BoxFuture>; + + fn search( + &self, + query: &str, + offset: usize, + limit: usize, + ) -> BoxFuture>; + + fn get_artist_albums( + &self, + id: &str, + offset: usize, + limit: usize, + ) -> BoxFuture>>; + + fn get_user(&self, id: &str) -> BoxFuture>; + + fn get_user_playlists( + &self, + id: &str, + offset: usize, + limit: usize, + ) -> BoxFuture>>; + + fn list_available_devices(&self) -> BoxFuture>>; + + fn get_player_queue(&self) -> BoxFuture>>; + + fn update_token(&self, token: String); + + fn player_pause(&self, device_id: String) -> BoxFuture>; + + fn player_resume(&self, device_id: String) -> BoxFuture>; + + #[allow(dead_code)] + fn player_next(&self, device_id: String) -> BoxFuture>; + + fn player_seek(&self, device_id: String, pos: usize) -> BoxFuture>; + + fn player_repeat(&self, device_id: String, mode: RepeatMode) -> BoxFuture>; + + fn player_shuffle(&self, device_id: String, shuffle: bool) -> BoxFuture>; + + fn player_volume(&self, device_id: String, volume: u8) -> BoxFuture>; + + fn player_play_in_context( + &self, + device_id: String, + context: String, + offset: usize, + ) -> BoxFuture>; + + fn player_play_no_context( + &self, + device_id: String, + uris: Vec, + offset: usize, + ) -> BoxFuture>; + + fn player_state(&self) -> BoxFuture>; +} + +enum SpotCacheKey<'a> { + SavedAlbums(usize, usize), + SavedTracks(usize, usize), + SavedPlaylists(usize, usize), + Album(&'a str), + AlbumLiked(&'a str), + AlbumTracks(&'a str, usize, usize), + Playlist(&'a str), + PlaylistTracks(&'a str, usize, usize), + ArtistAlbums(&'a str, usize, usize), + Artist(&'a str), + ArtistTopTracks(&'a str), + User(&'a str), + UserPlaylists(&'a str, usize, usize), +} + +impl<'a> SpotCacheKey<'a> { + fn into_raw(self) -> String { + match self { + Self::SavedAlbums(offset, limit) => format!("me_albums_{offset}_{limit}.json"), + Self::SavedTracks(offset, limit) => format!("me_tracks_{offset}_{limit}.json"), + Self::SavedPlaylists(offset, limit) => format!("me_playlists_{offset}_{limit}.json"), + Self::Album(id) => format!("album_{id}.json"), + Self::AlbumTracks(id, offset, limit) => { + format!("album_item_{id}_{offset}_{limit}.json") + } + Self::AlbumLiked(id) => format!("album_liked_{id}.json"), + Self::Playlist(id) => format!("playlist_{id}.json"), + Self::PlaylistTracks(id, offset, limit) => { + format!("playlist_item_{id}_{offset}_{limit}.json") + } + Self::ArtistAlbums(id, offset, limit) => { + format!("artist_albums_{id}_{offset}_{limit}.json") + } + Self::Artist(id) => format!("artist_{id}.json"), + Self::ArtistTopTracks(id) => format!("artist_top_tracks_{id}.json"), + Self::User(id) => format!("user_{id}.json"), + Self::UserPlaylists(id, offset, limit) => { + format!("user_playlists_{id}_{offset}_{limit}.json") + } + } + } +} + +lazy_static! { + pub static ref ME_TRACKS_CACHE: Regex = Regex::new(r"^me_tracks_\w+_\w+\.json$").unwrap(); + pub static ref ME_ALBUMS_CACHE: Regex = Regex::new(r"^me_albums_\w+_\w+\.json$").unwrap(); + pub static ref USER_CACHE: Regex = + Regex::new(r"^me_(albums|playlists|tracks)_\w+_\w+\.json$").unwrap(); +} + +fn playlist_cache_key(id: &str) -> Regex { + Regex::new(&format!(r"^playlist(_{id}|item_{id}_\w+_\w+)\.json$")).unwrap() +} + +pub struct CachedSpotifyClient { + client: SpotifyClient, + cache: CacheManager, +} + +impl CachedSpotifyClient { + pub fn new() -> CachedSpotifyClient { + CachedSpotifyClient { + client: SpotifyClient::new(), + cache: CacheManager::for_dir("spot/net").unwrap(), + } + } + + fn default_cache_policy(&self) -> CachePolicy { + if self.client.has_token() { + CachePolicy::Default + } else { + CachePolicy::IgnoreExpiry + } + } + + async fn wrap_write(write: &F, etag: Option) -> SpotifyResult + where + O: Future>>, + F: Fn(Option) -> O, + { + write(etag) + .map(|r| { + let SpotifyResponse { + kind, + max_age, + etag, + } = r?; + let expiry = CacheExpiry::expire_in_seconds(max_age, etag); + SpotifyResult::Ok(match kind { + SpotifyResponseKind::Ok(content, _) => { + FetchResult::Modified(content.into_bytes(), expiry) + } + SpotifyResponseKind::NotModified => FetchResult::NotModified(expiry), + }) + }) + .await + } + + async fn cache_get_or_write( + &self, + key: SpotCacheKey<'_>, + cache_policy: Option, + write: F, + ) -> SpotifyResult + where + O: Future>>, + F: Fn(Option) -> O, + T: DeserializeOwned, + { + let write = &write; + let cache_key = key.into_raw(); + let raw = self + .cache + .get_or_write( + &cache_key, + cache_policy.unwrap_or_else(|| self.default_cache_policy()), + |etag| Self::wrap_write(write, etag), + ) + .await?; + + let result = from_slice::(&raw); + match result { + Ok(t) => Ok(t), + // parsing failed: cache is likely invalid, request again, ignoring cache + Err(e) => { + dbg!(&cache_key, e); + let new_raw = self + .cache + .get_or_write(&cache_key, CachePolicy::IgnoreCached, |etag| { + Self::wrap_write(write, etag) + }) + .await?; + Ok(from_slice::(&new_raw)?) + } + } + } +} + +impl SpotifyApiClient for CachedSpotifyClient { + fn update_token(&self, new_token: String) { + self.client.update_token(new_token) + } + + fn get_saved_albums( + &self, + offset: usize, + limit: usize, + ) -> BoxFuture>> { + Box::pin(async move { + let page = self + .cache_get_or_write(SpotCacheKey::SavedAlbums(offset, limit), None, |etag| { + self.client + .get_saved_albums(offset, limit) + .etag(etag) + .send() + }) + .await?; + + let albums = page + .into_iter() + .map(|saved| saved.album.into()) + .collect::>(); + + Ok(albums) + }) + } + + fn get_saved_tracks(&self, offset: usize, limit: usize) -> BoxFuture> { + Box::pin(async move { + let page = self + .cache_get_or_write(SpotCacheKey::SavedTracks(offset, limit), None, |etag| { + self.client + .get_saved_tracks(offset, limit) + .etag(etag) + .send() + }) + .await?; + + Ok(page.into()) + }) + } + + fn get_saved_playlists( + &self, + offset: usize, + limit: usize, + ) -> BoxFuture>> { + Box::pin(async move { + let page = self + .cache_get_or_write(SpotCacheKey::SavedPlaylists(offset, limit), None, |etag| { + self.client + .get_saved_playlists(offset, limit) + .etag(etag) + .send() + }) + .await?; + + let albums = page + .into_iter() + .map(|playlist| playlist.into()) + .collect::>(); + + Ok(albums) + }) + } + + fn add_to_playlist(&self, id: &str, uris: Vec) -> BoxFuture> { + let id = id.to_owned(); + + Box::pin(async move { + self.cache + .set_expired_pattern(&playlist_cache_key(&id)) + .await + .unwrap_or(()); + + self.client + .add_to_playlist(&id, uris) + .send_no_response() + .await?; + Ok(()) + }) + } + + fn create_new_playlist( + &self, + name: &str, + user_id: &str, + ) -> BoxFuture> { + let name = name.to_owned(); + let user_id = user_id.to_owned(); + + Box::pin(async move { + let playlist = self + .client + .create_new_playlist(&name, &user_id) + .send() + .await? + .deserialize() + .unwrap(); + + Ok(playlist.into()) + }) + } + + fn remove_from_playlist(&self, id: &str, uris: Vec) -> BoxFuture> { + let id = id.to_owned(); + + Box::pin(async move { + self.cache + .set_expired_pattern(&playlist_cache_key(&id)) + .await + .unwrap_or(()); + + self.client + .remove_from_playlist(&id, uris) + .send_no_response() + .await?; + Ok(()) + }) + } + + fn update_playlist_details(&self, id: &str, name: String) -> BoxFuture> { + let id = id.to_owned(); + + Box::pin(async move { + self.cache + .set_expired_pattern(&playlist_cache_key(&id)) + .await + .unwrap_or(()); + + self.client + .update_playlist_details(&id, name) + .send_no_response() + .await?; + + Ok(()) + }) + } + + fn get_album(&self, id: &str) -> BoxFuture> { + let id = id.to_owned(); + + Box::pin(async move { + let album = self.cache_get_or_write(SpotCacheKey::Album(&id), None, |etag| { + self.client.get_album(&id).etag(etag).send() + }); + + let liked = self.cache_get_or_write( + SpotCacheKey::AlbumLiked(&id), + Some(if self.client.has_token() { + CachePolicy::Revalidate + } else { + CachePolicy::IgnoreExpiry + }), + |etag| self.client.is_album_saved(&id).etag(etag).send(), + ); + + let (album, liked) = join!(album, liked); + + let mut album: AlbumFullDescription = album?.into(); + album.description.is_liked = liked?[0]; + + Ok(album) + }) + } + + fn save_album(&self, id: &str) -> BoxFuture> { + let id = id.to_owned(); + + Box::pin(async move { + let _ = self.cache.set_expired_pattern(&ME_ALBUMS_CACHE).await; + self.client.save_album(&id).send_no_response().await?; + self.get_album(&id[..]).await.map(|a| a.description) + }) + } + + fn save_tracks(&self, ids: Vec) -> BoxFuture> { + Box::pin(async move { + let _ = self.cache.set_expired_pattern(&ME_TRACKS_CACHE).await; + self.client.save_tracks(ids).send_no_response().await?; + Ok(()) + }) + } + + fn remove_saved_album(&self, id: &str) -> BoxFuture> { + let id = id.to_owned(); + + Box::pin(async move { + let _ = self.cache.set_expired_pattern(&ME_ALBUMS_CACHE).await; + self.client.remove_saved_album(&id).send_no_response().await + }) + } + + fn remove_saved_tracks(&self, ids: Vec) -> BoxFuture> { + Box::pin(async move { + let _ = self.cache.set_expired_pattern(&ME_TRACKS_CACHE).await; + self.client + .remove_saved_tracks(ids) + .send_no_response() + .await + }) + } + + fn get_album_tracks( + &self, + id: &str, + offset: usize, + limit: usize, + ) -> BoxFuture> { + let id = id.to_owned(); + + Box::pin(async move { + let album = self.cache_get_or_write( + SpotCacheKey::Album(&id), + Some(CachePolicy::IgnoreExpiry), + |etag| self.client.get_album(&id).etag(etag).send(), + ); + + let songs = self.cache_get_or_write( + SpotCacheKey::AlbumTracks(&id, offset, limit), + None, + |etag| { + self.client + .get_album_tracks(&id, offset, limit) + .etag(etag) + .send() + }, + ); + + let (album, songs) = join!(album, songs); + Ok((songs?, &album?.album).into()) + }) + } + + fn get_playlist(&self, id: &str) -> BoxFuture> { + let id = id.to_owned(); + + Box::pin(async move { + let playlist = self + .cache_get_or_write(SpotCacheKey::Playlist(&id), None, |etag| { + self.client.get_playlist(&id).etag(etag).send() + }) + .await?; + + Ok(playlist.into()) + }) + } + + fn get_playlist_tracks( + &self, + id: &str, + offset: usize, + limit: usize, + ) -> BoxFuture> { + let id = id.to_owned(); + + Box::pin(async move { + let songs = self + .cache_get_or_write( + SpotCacheKey::PlaylistTracks(&id, offset, limit), + None, + |etag| { + self.client + .get_playlist_tracks(&id, offset, limit) + .etag(etag) + .send() + }, + ) + .await?; + + Ok(songs.into()) + }) + } + + fn get_artist_albums( + &self, + id: &str, + offset: usize, + limit: usize, + ) -> BoxFuture>> { + let id = id.to_owned(); + + Box::pin(async move { + let albums = self + .cache_get_or_write( + SpotCacheKey::ArtistAlbums(&id, offset, limit), + None, + |etag| { + self.client + .get_artist_albums(&id, offset, limit) + .etag(etag) + .send() + }, + ) + .await?; + + let albums = albums + .into_iter() + .map(|a| a.into()) + .collect::>(); + + Ok(albums) + }) + } + + fn get_artist(&self, id: &str) -> BoxFuture> { + let id = id.to_owned(); + + Box::pin(async move { + let artist = self.cache_get_or_write(SpotCacheKey::Artist(&id), None, |etag| { + self.client.get_artist(&id).etag(etag).send() + }); + + let albums = self.get_artist_albums(&id, 0, 20); + + let top_tracks = + self.cache_get_or_write(SpotCacheKey::ArtistTopTracks(&id), None, |etag| { + self.client.get_artist_top_tracks(&id).etag(etag).send() + }); + + let (artist, albums, top_tracks) = join!(artist, albums, top_tracks); + + let artist = artist?; + let result = ArtistDescription { + id: artist.id, + name: artist.name, + albums: albums?, + top_tracks: top_tracks?.into(), + }; + Ok(result) + }) + } + + fn search( + &self, + query: &str, + offset: usize, + limit: usize, + ) -> BoxFuture> { + let query = query.to_owned(); + + Box::pin(async move { + let results = self + .client + .search(query, offset, limit) + .send() + .await? + .deserialize() + .ok_or(SpotifyApiError::NoContent)?; + + let albums = results + .albums + .unwrap_or_default() + .into_iter() + .map(|saved| saved.into()) + .collect::>(); + + let artists = results + .artists + .unwrap_or_default() + .into_iter() + .map(|saved| saved.into()) + .collect::>(); + + Ok(SearchResults { albums, artists }) + }) + } + + fn get_user_playlists( + &self, + id: &str, + offset: usize, + limit: usize, + ) -> BoxFuture>> { + let id = id.to_owned(); + + Box::pin(async move { + let playlists = self + .cache_get_or_write( + SpotCacheKey::UserPlaylists(&id, offset, limit), + None, + |etag| { + self.client + .get_user_playlists(&id, offset, limit) + .etag(etag) + .send() + }, + ) + .await?; + + let playlists = playlists + .into_iter() + .map(|a| a.into()) + .collect::>(); + + Ok(playlists) + }) + } + + fn get_user(&self, id: &str) -> BoxFuture> { + let id = id.to_owned(); + + Box::pin(async move { + let user = self.cache_get_or_write(SpotCacheKey::User(&id), None, |etag| { + self.client.get_user(&id).etag(etag).send() + }); + + let playlists = self.get_user_playlists(&id, 0, 30); + + let (user, playlists) = join!(user, playlists); + + let user = user?; + let result = UserDescription { + id: user.id, + name: user.display_name, + playlists: playlists?, + }; + Ok(result) + }) + } + + fn list_available_devices(&self) -> BoxFuture>> { + Box::pin(async move { + let devices = self + .client + .get_player_devices() + .send() + .await? + .deserialize() + .ok_or(SpotifyApiError::NoContent)?; + Ok(devices + .devices + .into_iter() + .filter(|d| { + debug!("found device: {:?}", d); + !d.is_restricted + }) + .map(ConnectDevice::from) + .collect()) + }) + } + + fn get_player_queue(&self) -> BoxFuture>> { + Box::pin(async move { + let queue = self + .client + .get_player_queue() + .send() + .await? + .deserialize() + .ok_or(SpotifyApiError::NoContent)?; + Ok(queue.into()) + }) + } + + fn player_pause(&self, device_id: String) -> BoxFuture> { + Box::pin(self.client.player_pause(&device_id).send_no_response()) + } + + fn player_resume(&self, device_id: String) -> BoxFuture> { + Box::pin(self.client.player_resume(&device_id).send_no_response()) + } + + fn player_play_in_context( + &self, + device_id: String, + context_uri: String, + offset: usize, + ) -> BoxFuture> { + Box::pin( + self.client + .player_set_playing( + &device_id, + PlayRequest::Contextual { + context_uri, + offset: PlayOffset { + position: offset as u32, + }, + }, + ) + .send_no_response(), + ) + } + + fn player_play_no_context( + &self, + device_id: String, + uris: Vec, + offset: usize, + ) -> BoxFuture> { + Box::pin( + self.client + .player_set_playing( + &device_id, + PlayRequest::Uris { + uris, + offset: PlayOffset { + position: offset as u32, + }, + }, + ) + .send_no_response(), + ) + } + + fn player_next(&self, device_id: String) -> BoxFuture> { + Box::pin(self.client.player_next(&device_id).send_no_response()) + } + + fn player_seek(&self, device_id: String, pos: usize) -> BoxFuture> { + Box::pin(self.client.player_seek(&device_id, pos).send_no_response()) + } + + fn player_state(&self) -> BoxFuture> { + Box::pin(async move { + let result = self + .client + .player_state() + .send() + .await? + .deserialize() + .ok_or(SpotifyApiError::NoContent)?; + Ok(result.into()) + }) + } + + fn player_repeat(&self, device_id: String, mode: RepeatMode) -> BoxFuture> { + Box::pin( + self.client + .player_repeat( + &device_id, + match mode { + RepeatMode::Song => "track", + RepeatMode::Playlist => "context", + RepeatMode::None => "off", + }, + ) + .send_no_response(), + ) + } + + fn player_shuffle(&self, device_id: String, shuffle: bool) -> BoxFuture> { + Box::pin( + self.client + .player_shuffle(&device_id, shuffle) + .send_no_response(), + ) + } + + fn player_volume(&self, device_id: String, volume: u8) -> BoxFuture> { + Box::pin( + self.client + .player_volume(&device_id, volume) + .send_no_response(), + ) + } +} + +#[cfg(test)] +pub mod tests { + + use crate::api::api_models::*; + + #[test] + fn test_search_query() { + let query = SearchQuery { + query: "test".to_string(), + types: vec![SearchType::Album, SearchType::Artist], + limit: 5, + offset: 0, + }; + + assert_eq!( + query.into_query_string(), + "type=album,artist&q=test&offset=0&limit=5&market=from_token" + ); + } + + #[test] + fn test_search_query_spaces_and_stuff() { + let query = SearchQuery { + query: "test??? wow".to_string(), + types: vec![SearchType::Album], + limit: 5, + offset: 0, + }; + + assert_eq!( + query.into_query_string(), + "type=album&q=test+wow&offset=0&limit=5&market=from_token" + ); + } + + #[test] + fn test_search_query_encoding() { + let query = SearchQuery { + query: "кириллица".to_string(), + types: vec![SearchType::Album], + limit: 5, + offset: 0, + }; + + assert_eq!(query.into_query_string(), "type=album&q=%D0%BA%D0%B8%D1%80%D0%B8%D0%BB%D0%BB%D0%B8%D1%86%D0%B0&offset=0&limit=5&market=from_token"); + } +} diff --git a/src/api/client.rs b/src/api/client.rs new file mode 100644 index 0000000..b17d737 --- /dev/null +++ b/src/api/client.rs @@ -0,0 +1,716 @@ +use form_urlencoded::Serializer; +use isahc::config::Configurable; +use isahc::http::{method::Method, request::Builder, StatusCode, Uri}; +use isahc::{AsyncReadResponseExt, HttpClient, Request}; +use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; +use serde::{de::Deserialize, Serialize}; +use serde_json::from_str; +use std::convert::Into; +use std::marker::PhantomData; +use std::str::FromStr; +use std::sync::Mutex; +use thiserror::Error; + +pub use super::api_models::*; +use super::cache::CacheError; + +const SPOTIFY_HOST: &str = "api.spotify.com"; + +// https://url.spec.whatwg.org/#path-percent-encode-set +const PATH_ENCODE_SET: &AsciiSet = &CONTROLS + .add(b' ') + .add(b'"') + .add(b'#') + .add(b'<') + .add(b'>') + .add(b'?') + .add(b'`') + .add(b'{') + .add(b'}'); + +fn make_query_params<'a>() -> Serializer<'a, String> { + Serializer::new(String::new()) +} + +pub(crate) struct SpotifyRequest<'a, Body, Response> { + client: &'a SpotifyClient, + request: Builder, + body: Body, + _type: PhantomData, +} + +impl<'a, B, R> SpotifyRequest<'a, B, R> +where + B: Into, +{ + fn method(mut self, method: Method) -> Self { + self.request = self.request.method(method); + self + } + + fn uri(mut self, path: String, query: Option<&str>) -> Self { + let path_and_query = match query { + None => path, + Some(query) => format!("{path}?{query}"), + }; + let uri = Uri::builder() + .scheme("https") + .authority(SPOTIFY_HOST) + .path_and_query(&path_and_query[..]) + .build() + .unwrap(); + self.request = self.request.uri(uri); + self + } + + fn authenticated(mut self) -> Result { + let token = self.client.token.lock().unwrap(); + let token = token.as_ref().ok_or(SpotifyApiError::NoToken)?; + self.request = self + .request + .header("Authorization", format!("Bearer {token}")); + Ok(self) + } + + pub(crate) fn etag(mut self, etag: Option) -> Self { + if let Some(etag) = etag { + self.request = self.request.header("If-None-Match", etag); + } + self + } + + pub(crate) fn json_body(self, body: NewBody) -> SpotifyRequest<'a, Vec, R> + where + NewBody: Serialize, + { + let Self { + client, + request, + _type, + .. + } = self; + SpotifyRequest { + client, + request: request.header("Content-Type", "application/json"), + body: serde_json::to_vec(&body).unwrap(), + _type, + } + } + + pub(crate) async fn send(self) -> Result, SpotifyApiError> { + let Self { + client, + request, + body, + .. + } = self.authenticated()?; + client.send_req(request.body(body).unwrap()).await + } + + pub(crate) async fn send_no_response(self) -> Result<(), SpotifyApiError> { + let Self { + client, + request, + body, + .. + } = self.authenticated()?; + client + .send_req_no_response(request.body(body).unwrap()) + .await + } +} + +pub(crate) enum SpotifyResponseKind { + Ok(String, PhantomData), + NotModified, +} + +pub(crate) struct SpotifyResponse { + pub kind: SpotifyResponseKind, + pub max_age: u64, + pub etag: Option, +} + +impl<'a, T> SpotifyResponse +where + T: Deserialize<'a>, +{ + pub(crate) fn deserialize(&'a self) -> Option { + if let SpotifyResponseKind::Ok(ref content, _) = self.kind { + from_str(content) + .map_err(|e| error!("Deserialization failed: {}", e)) + .ok() + } else { + None + } + } +} + +#[derive(Error, Debug)] +pub enum SpotifyApiError { + #[error("Invalid token")] + InvalidToken, + #[error("No token")] + NoToken, + #[error("No content from request")] + NoContent, + #[error("Request rate exceeded")] + TooManyRequests, + #[error("Request failed ({0}): {1}")] + BadStatus(u16, String), + #[error(transparent)] + ClientError(#[from] isahc::Error), + #[error(transparent)] + IoError(#[from] std::io::Error), + #[error(transparent)] + CacheError(#[from] CacheError), + #[error(transparent)] + ParseError(#[from] serde_json::Error), + #[error(transparent)] + ConversionError(#[from] std::string::FromUtf8Error), +} + +pub(crate) struct SpotifyClient { + token: Mutex>, + client: HttpClient, +} + +impl SpotifyClient { + pub(crate) fn new() -> Self { + let mut builder = HttpClient::builder(); + if cfg!(debug_assertions) { + builder = builder.ssl_options(isahc::config::SslOption::DANGER_ACCEPT_INVALID_CERTS); + } + let client = builder.build().unwrap(); + Self { + token: Mutex::new(None), + client, + } + } + + pub(crate) fn request(&self) -> SpotifyRequest<'_, (), T> { + SpotifyRequest { + client: self, + request: Builder::new(), + body: (), + _type: PhantomData, + } + } + + pub(crate) fn has_token(&self) -> bool { + self.token.lock().unwrap().is_some() + } + + pub(crate) fn update_token(&self, new_token: String) { + if let Ok(mut token) = self.token.lock() { + *token = Some(new_token) + } + } + + fn clear_token(&self) { + if let Ok(mut token) = self.token.lock() { + *token = None + } + } + + fn parse_cache_control(cache_control: &str) -> Option { + cache_control + .split(',') + .find(|s| s.trim().starts_with("max-age=")) + .and_then(|s| s.split('=').nth(1)) + .and_then(|s| u64::from_str(s).ok()) + } + + async fn send_req( + &self, + request: Request, + ) -> Result, SpotifyApiError> + where + B: Into, + { + let mut result = self.client.send_async(request).await?; + + let etag = result + .headers() + .get("etag") + .and_then(|header| header.to_str().ok()) + .map(|s| s.to_owned()); + + let cache_control = result + .headers() + .get("cache-control") + .and_then(|header| header.to_str().ok()) + .and_then(Self::parse_cache_control); + + match result.status() { + StatusCode::NO_CONTENT => Err(SpotifyApiError::NoContent), + s if s.is_success() => Ok(SpotifyResponse { + kind: SpotifyResponseKind::Ok(result.text().await?, PhantomData), + max_age: cache_control.unwrap_or(10), + etag, + }), + StatusCode::UNAUTHORIZED => { + self.clear_token(); + Err(SpotifyApiError::InvalidToken) + } + StatusCode::TOO_MANY_REQUESTS => Err(SpotifyApiError::TooManyRequests), + StatusCode::NOT_MODIFIED => Ok(SpotifyResponse { + kind: SpotifyResponseKind::NotModified, + max_age: cache_control.unwrap_or(10), + etag, + }), + s => Err(SpotifyApiError::BadStatus( + s.as_u16(), + result + .text() + .await + .unwrap_or_else(|_| "(no details available)".to_string()), + )), + } + } + + async fn send_req_no_response(&self, request: Request) -> Result<(), SpotifyApiError> + where + B: Into, + { + let mut result = self.client.send_async(request).await?; + match result.status() { + StatusCode::UNAUTHORIZED => { + self.clear_token(); + Err(SpotifyApiError::InvalidToken) + } + StatusCode::TOO_MANY_REQUESTS => Err(SpotifyApiError::TooManyRequests), + StatusCode::NOT_MODIFIED => Ok(()), + s if s.is_success() => Ok(()), + s => Err(SpotifyApiError::BadStatus( + s.as_u16(), + result + .text() + .await + .unwrap_or_else(|_| "(no details available)".to_string()), + )), + } + } +} + +impl SpotifyClient { + pub(crate) fn get_artist(&self, id: &str) -> SpotifyRequest<'_, (), Artist> { + self.request() + .method(Method::GET) + .uri(format!("/v1/artists/{id}"), None) + } + + pub(crate) fn get_artist_albums( + &self, + id: &str, + offset: usize, + limit: usize, + ) -> SpotifyRequest<'_, (), Page> { + let query = make_query_params() + .append_pair("include_groups", "album,single") + .append_pair("country", "from_token") + .append_pair("offset", &offset.to_string()[..]) + .append_pair("limit", &limit.to_string()[..]) + .finish(); + + self.request() + .method(Method::GET) + .uri(format!("/v1/artists/{id}/albums"), Some(&query)) + } + + pub(crate) fn get_artist_top_tracks(&self, id: &str) -> SpotifyRequest<'_, (), TopTracks> { + let query = make_query_params() + .append_pair("market", "from_token") + .finish(); + + self.request() + .method(Method::GET) + .uri(format!("/v1/artists/{id}/top-tracks"), Some(&query)) + } + + pub(crate) fn is_album_saved(&self, id: &str) -> SpotifyRequest<'_, (), Vec> { + let query = make_query_params().append_pair("ids", id).finish(); + self.request() + .method(Method::GET) + .uri("/v1/me/albums/contains".to_string(), Some(&query)) + } + + pub(crate) fn save_album(&self, id: &str) -> SpotifyRequest<'_, (), ()> { + let query = make_query_params().append_pair("ids", id).finish(); + self.request() + .method(Method::PUT) + .uri("/v1/me/albums".to_string(), Some(&query)) + } + + pub(crate) fn save_tracks(&self, ids: Vec) -> SpotifyRequest<'_, Vec, ()> { + self.request() + .method(Method::PUT) + .uri("/v1/me/tracks".to_string(), None) + .json_body(Ids { ids }) + } + + pub(crate) fn remove_saved_album(&self, id: &str) -> SpotifyRequest<'_, (), ()> { + let query = make_query_params().append_pair("ids", id).finish(); + self.request() + .method(Method::DELETE) + .uri("/v1/me/albums".to_string(), Some(&query)) + } + + pub(crate) fn remove_saved_tracks(&self, ids: Vec) -> SpotifyRequest<'_, Vec, ()> { + self.request() + .method(Method::DELETE) + .uri("/v1/me/tracks".to_string(), None) + .json_body(Ids { ids }) + } + + pub(crate) fn get_album(&self, id: &str) -> SpotifyRequest<'_, (), FullAlbum> { + self.request() + .method(Method::GET) + .uri(format!("/v1/albums/{id}"), None) + } + + pub(crate) fn get_album_tracks( + &self, + id: &str, + offset: usize, + limit: usize, + ) -> SpotifyRequest<'_, (), Page> { + let query = make_query_params() + .append_pair("offset", &offset.to_string()[..]) + .append_pair("limit", &limit.to_string()[..]) + .finish(); + + self.request() + .method(Method::GET) + .uri(format!("/v1/albums/{id}/tracks"), Some(&query)) + } + + pub(crate) fn get_playlist(&self, id: &str) -> SpotifyRequest<'_, (), Playlist> { + let query = make_query_params() + .append_pair( + "fields", + "id,name,images,owner,tracks(total,items(is_local,track(name,id,uri,duration_ms,artists(name,id),album(name,id,images,artists))))", + ) + .finish(); + self.request() + .method(Method::GET) + .uri(format!("/v1/playlists/{id}"), Some(&query)) + } + + pub(crate) fn get_playlist_tracks( + &self, + id: &str, + offset: usize, + limit: usize, + ) -> SpotifyRequest<'_, (), Page> { + let query = make_query_params() + .append_pair("offset", &offset.to_string()[..]) + .append_pair("limit", &limit.to_string()[..]) + .finish(); + + self.request() + .method(Method::GET) + .uri(format!("/v1/playlists/{id}/tracks"), Some(&query)) + } + + pub(crate) fn add_to_playlist( + &self, + playlist: &str, + uris: Vec, + ) -> SpotifyRequest<'_, Vec, ()> { + self.request() + .method(Method::POST) + .uri(format!("/v1/playlists/{playlist}/tracks"), None) + .json_body(Uris { uris }) + } + + pub(crate) fn create_new_playlist( + &self, + name: &str, + user_id: &str, + ) -> SpotifyRequest<'_, Vec, Playlist> { + self.request() + .method(Method::POST) + .uri(format!("/v1/users/{user_id}/playlists"), None) + .json_body(Name { name }) + } + + pub(crate) fn remove_from_playlist( + &self, + playlist: &str, + uris: Vec, + ) -> SpotifyRequest<'_, Vec, ()> { + self.request() + .method(Method::DELETE) + .uri(format!("/v1/playlists/{playlist}/tracks"), None) + .json_body(Uris { uris }) + } + + pub(crate) fn update_playlist_details( + &self, + playlist: &str, + name: String, + ) -> SpotifyRequest<'_, Vec, ()> { + self.request() + .method(Method::PUT) + .uri(format!("/v1/playlists/{playlist}"), None) + .json_body(PlaylistDetails { name }) + } + + pub(crate) fn get_saved_albums( + &self, + offset: usize, + limit: usize, + ) -> SpotifyRequest<'_, (), Page> { + let query = make_query_params() + .append_pair("offset", &offset.to_string()[..]) + .append_pair("limit", &limit.to_string()[..]) + .finish(); + + self.request() + .method(Method::GET) + .uri("/v1/me/albums".to_string(), Some(&query)) + } + + pub(crate) fn get_saved_tracks( + &self, + offset: usize, + limit: usize, + ) -> SpotifyRequest<'_, (), Page> { + let query = make_query_params() + .append_pair("offset", &offset.to_string()[..]) + .append_pair("limit", &limit.to_string()[..]) + .finish(); + + self.request() + .method(Method::GET) + .uri("/v1/me/tracks".to_string(), Some(&query)) + } + + pub(crate) fn get_saved_playlists( + &self, + offset: usize, + limit: usize, + ) -> SpotifyRequest<'_, (), Page> { + let query = make_query_params() + .append_pair("offset", &offset.to_string()[..]) + .append_pair("limit", &limit.to_string()[..]) + .finish(); + + self.request() + .method(Method::GET) + .uri("/v1/me/playlists".to_string(), Some(&query)) + } + + pub(crate) fn search( + &self, + query: String, + offset: usize, + limit: usize, + ) -> SpotifyRequest<'_, (), RawSearchResults> { + let query = SearchQuery { + query, + types: vec![SearchType::Album, SearchType::Artist], + limit, + offset, + }; + + self.request() + .method(Method::GET) + .uri("/v1/search".to_string(), Some(&query.into_query_string())) + } + + pub(crate) fn get_user(&self, id: &str) -> SpotifyRequest<'_, (), User> { + let id = utf8_percent_encode(id, PATH_ENCODE_SET); + self.request() + .method(Method::GET) + .uri(format!("/v1/users/{id}"), None) + } + + pub(crate) fn get_user_playlists( + &self, + id: &str, + offset: usize, + limit: usize, + ) -> SpotifyRequest<'_, (), Page> { + let id = utf8_percent_encode(id, PATH_ENCODE_SET); + let query = make_query_params() + .append_pair("offset", &offset.to_string()[..]) + .append_pair("limit", &limit.to_string()[..]) + .finish(); + + self.request() + .method(Method::GET) + .uri(format!("/v1/users/{id}/playlists"), Some(&query)) + } + + pub(crate) fn get_player_devices(&self) -> SpotifyRequest<'_, (), Devices> { + self.request() + .method(Method::GET) + .uri("/v1/me/player/devices".to_string(), None) + } + + pub(crate) fn get_player_queue(&self) -> SpotifyRequest<'_, (), PlayerQueue> { + self.request() + .method(Method::GET) + .uri("/v1/me/player/queue".to_string(), None) + } + + pub(crate) fn player_state(&self) -> SpotifyRequest<'_, (), PlayerState> { + self.request() + .method(Method::GET) + .uri("/v1/me/player".to_string(), None) + } + + pub(crate) fn player_resume(&self, device_id: &str) -> SpotifyRequest<'_, (), ()> { + let query = make_query_params() + .append_pair("device_id", device_id) + .finish(); + self.request() + .method(Method::PUT) + .uri("/v1/me/player/play".to_string(), Some(&query)) + } + + pub(crate) fn player_set_playing( + &self, + device_id: &str, + request: PlayRequest, + ) -> SpotifyRequest<'_, Vec, ()> { + let query = make_query_params() + .append_pair("device_id", device_id) + .finish(); + self.request() + .method(Method::PUT) + .uri("/v1/me/player/play".to_string(), Some(&query)) + .json_body(request) + } + + pub(crate) fn player_pause(&self, device_id: &str) -> SpotifyRequest<'_, (), ()> { + let query = make_query_params() + .append_pair("device_id", device_id) + .finish(); + self.request() + .method(Method::PUT) + .uri("/v1/me/player/pause".to_string(), Some(&query)) + } + + pub(crate) fn player_next(&self, device_id: &str) -> SpotifyRequest<'_, (), ()> { + let query = make_query_params() + .append_pair("device_id", device_id) + .finish(); + self.request() + .method(Method::PUT) + .uri("/v1/me/player/next".to_string(), Some(&query)) + } + + pub(crate) fn player_seek(&self, device_id: &str, pos: usize) -> SpotifyRequest<'_, (), ()> { + let query = make_query_params() + .append_pair("device_id", device_id) + .append_pair("position_ms", &pos.to_string()[..]) + .finish(); + + self.request() + .method(Method::PUT) + .uri("/v1/me/player/seek".to_string(), Some(&query)) + } + + pub(crate) fn player_repeat(&self, device_id: &str, state: &str) -> SpotifyRequest<'_, (), ()> { + let query = make_query_params() + .append_pair("device_id", device_id) + .append_pair("state", state) + .finish(); + + self.request() + .method(Method::PUT) + .uri("/v1/me/player/repeat".to_string(), Some(&query)) + } + + pub(crate) fn player_shuffle( + &self, + device_id: &str, + shuffle: bool, + ) -> SpotifyRequest<'_, (), ()> { + let query = make_query_params() + .append_pair("device_id", device_id) + .append_pair("state", if shuffle { "true" } else { "false" }) + .finish(); + + self.request() + .method(Method::PUT) + .uri("/v1/me/player/shuffle".to_string(), Some(&query)) + } + + pub(crate) fn player_volume(&self, device_id: &str, volume: u8) -> SpotifyRequest<'_, (), ()> { + let query = make_query_params() + .append_pair("device_id", device_id) + .append_pair("volume_percent", &volume.to_string()) + .finish(); + + self.request() + .method(Method::PUT) + .uri("/v1/me/player/volume".to_string(), Some(&query)) + } +} + +#[cfg(test)] +pub mod tests { + + use super::*; + + #[test] + fn test_username_encoding() { + let username = "anna.lafuente❤"; + let client = SpotifyClient::new(); + let req = client.get_user(username); + assert_eq!( + req.request + .uri_ref() + .and_then(|u| u.path_and_query()) + .unwrap() + .as_str(), + "/v1/users/anna.lafuente%E2%9D%A4" + ); + } + + #[test] + fn test_search_query() { + let query = SearchQuery { + query: "test".to_string(), + types: vec![SearchType::Album, SearchType::Artist], + limit: 5, + offset: 0, + }; + + assert_eq!( + query.into_query_string(), + "type=album,artist&q=test&offset=0&limit=5&market=from_token" + ); + } + + #[test] + fn test_search_query_spaces_and_stuff() { + let query = SearchQuery { + query: "test??? wow".to_string(), + types: vec![SearchType::Album], + limit: 5, + offset: 0, + }; + + assert_eq!( + query.into_query_string(), + "type=album&q=test+wow&offset=0&limit=5&market=from_token" + ); + } + + #[test] + fn test_search_query_encoding() { + let query = SearchQuery { + query: "кириллица".to_string(), + types: vec![SearchType::Album], + limit: 5, + offset: 0, + }; + + assert_eq!(query.into_query_string(), "type=album&q=%D0%BA%D0%B8%D1%80%D0%B8%D0%BB%D0%BB%D0%B8%D1%86%D0%B0&offset=0&limit=5&market=from_token"); + } +} diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..e2900d2 --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,16 @@ +mod api_models; +mod cached_client; +mod client; + +pub mod cache; +pub(crate) mod oauth2; + +pub use cached_client::{CachedSpotifyClient, SpotifyApiClient, SpotifyResult}; +pub use client::SpotifyApiError; + +pub async fn clear_user_cache() -> Option<()> { + cache::CacheManager::for_dir("spot/net")? + .clear_cache_pattern(&cached_client::USER_CACHE) + .await + .ok() +} diff --git a/src/api/oauth2.rs b/src/api/oauth2.rs new file mode 100644 index 0000000..22ae6ad --- /dev/null +++ b/src/api/oauth2.rs @@ -0,0 +1,293 @@ +//! Provides a Spotify access token using the OAuth authorization code flow +//! with PKCE. +//! +//! Assuming sufficient scopes, the returned access token may be used with Spotify's +//! Web API, and/or to establish a new Session with [`librespot_core`]. +//! +//! The authorization code flow is an interactive process which requires a web browser +//! to complete. The resulting code must then be provided back from the browser to this +//! library for exchange into an access token. Providing the code can be automatic via +//! a spawned http server (mimicking Spotify's client), or manually via stdin. The latter +//! is appropriate for headless systems. + +use log::{error, info, trace}; +use oauth2::reqwest::http_client; +use oauth2::{ + basic::BasicClient, AuthUrl, AuthorizationCode, ClientId, CsrfToken, PkceCodeChallenge, + RedirectUrl, Scope, TokenResponse, TokenUrl, +}; +use std::io; +use std::time::{Duration, Instant}; +use std::{ + io::{BufRead, BufReader, Write}, + net::{SocketAddr, TcpListener}, + sync::mpsc, +}; +use thiserror::Error; +use url::Url; + +#[derive(Debug, Error)] +pub enum OAuthError { + #[error("Unable to parse redirect URI {uri} ({e})")] + AuthCodeBadUri { uri: String, e: url::ParseError }, + + #[error("Auth code param not found in URI {uri}")] + AuthCodeNotFound { uri: String }, + + #[error("Failed to read redirect URI from stdin")] + AuthCodeStdinRead, + + #[error("Failed to bind server to {addr} ({e})")] + AuthCodeListenerBind { addr: SocketAddr, e: io::Error }, + + #[error("Listener terminated without accepting a connection")] + AuthCodeListenerTerminated, + + #[error("Failed to read redirect URI from HTTP request")] + AuthCodeListenerRead, + + #[error("Failed to parse redirect URI from HTTP request")] + AuthCodeListenerParse, + + #[error("Failed to write HTTP response")] + AuthCodeListenerWrite, + + #[error("Invalid Spotify OAuth URI")] + InvalidSpotifyUri, + + #[error("Invalid Redirect URI {uri} ({e})")] + InvalidRedirectUri { uri: String, e: url::ParseError }, + + #[error("Failed to receive code")] + Recv, + + #[error("Failed to exchange code for access token ({e})")] + ExchangeCode { e: String }, +} + +#[derive(Debug)] +pub struct OAuthToken { + pub access_token: String, + #[allow(dead_code)] + pub refresh_token: String, + pub expires_at: Instant, + #[allow(dead_code)] + pub token_type: String, + #[allow(dead_code)] + pub scopes: Vec, +} + +/// Return code query-string parameter from the redirect URI. +fn get_code(redirect_url: &str) -> Result { + let url = Url::parse(redirect_url).map_err(|e| OAuthError::AuthCodeBadUri { + uri: redirect_url.to_string(), + e, + })?; + let code = url + .query_pairs() + .find(|(key, _)| key == "code") + .map(|(_, code)| AuthorizationCode::new(code.into_owned())) + .ok_or(OAuthError::AuthCodeNotFound { + uri: redirect_url.to_string(), + })?; + + Ok(code) +} + +/// Prompt for redirect URI on stdin and return auth code. +fn get_authcode_stdin() -> Result { + println!("Provide redirect URL"); + let mut buffer = String::new(); + let stdin = io::stdin(); + stdin + .read_line(&mut buffer) + .map_err(|_| OAuthError::AuthCodeStdinRead)?; + + get_code(buffer.trim()) +} + +/// Spawn HTTP server at provided socket address to accept OAuth callback and return auth code. +fn get_authcode_listener(socket_address: SocketAddr) -> Result { + let listener = + TcpListener::bind(socket_address).map_err(|e| OAuthError::AuthCodeListenerBind { + addr: socket_address, + e, + })?; + info!("OAuth server listening on {:?}", socket_address); + + // The server will terminate itself after collecting the first code. + let mut stream = listener + .incoming() + .flatten() + .next() + .ok_or(OAuthError::AuthCodeListenerTerminated)?; + let mut reader = BufReader::new(&stream); + let mut request_line = String::new(); + reader + .read_line(&mut request_line) + .map_err(|_| OAuthError::AuthCodeListenerRead)?; + + let redirect_url = request_line + .split_whitespace() + .nth(1) + .ok_or(OAuthError::AuthCodeListenerParse)?; + let code = get_code(&("http://localhost".to_string() + redirect_url)); + + let message = "Go back to your terminal :)"; + let response = format!( + "HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}", + message.len(), + message + ); + stream + .write_all(response.as_bytes()) + .map_err(|_| OAuthError::AuthCodeListenerWrite)?; + + code +} + +// If the specified `redirect_uri` is HTTP, loopback, and contains a port, +// then the corresponding socket address is returned. +fn get_socket_address(redirect_uri: &str) -> Option { + let url = match Url::parse(redirect_uri) { + Ok(u) if u.scheme() == "http" && u.port().is_some() => u, + _ => return None, + }; + let socket_addr = match url.socket_addrs(|| None) { + Ok(mut addrs) => addrs.pop(), + _ => None, + }; + if let Some(s) = socket_addr { + if s.ip().is_loopback() { + return socket_addr; + } + } + None +} + +/// Obtain a Spotify access token using the authorization code with PKCE OAuth flow. +/// The redirect_uri must match what is registered to the client ID. +pub fn get_access_token( + client_id: &str, + redirect_uri: &str, + scopes: Vec<&str>, +) -> Result { + let auth_url = AuthUrl::new("https://accounts.spotify.com/authorize".to_string()) + .map_err(|_| OAuthError::InvalidSpotifyUri)?; + let token_url = TokenUrl::new("https://accounts.spotify.com/api/token".to_string()) + .map_err(|_| OAuthError::InvalidSpotifyUri)?; + let redirect_url = + RedirectUrl::new(redirect_uri.to_string()).map_err(|e| OAuthError::InvalidRedirectUri { + uri: redirect_uri.to_string(), + e, + })?; + let client = BasicClient::new( + ClientId::new(client_id.to_string()), + None, + auth_url, + Some(token_url), + ) + .set_redirect_uri(redirect_url); + + let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); + + // Generate the full authorization URL. + // Some of these scopes are unavailable for custom client IDs. Which? + let request_scopes: Vec = scopes + .clone() + .into_iter() + .map(|s| Scope::new(s.into())) + .collect(); + let (auth_url, _) = client + .authorize_url(CsrfToken::new_random) + .add_scopes(request_scopes) + .set_pkce_challenge(pkce_challenge) + .url(); + + println!("Browse to: {}", auth_url); + if let Err(err) = open::that(auth_url.to_string()) { + eprintln!("An error occurred when opening '{}': {}", auth_url, err) + } + + let code = match get_socket_address(redirect_uri) { + Some(addr) => get_authcode_listener(addr), + _ => get_authcode_stdin(), + }?; + trace!("Exchange {code:?} for access token"); + + // Do this sync in another thread because I am too stupid to make the async version work. + let (tx, rx) = mpsc::channel(); + std::thread::spawn(move || { + let resp = client + .exchange_code(code) + .set_pkce_verifier(pkce_verifier) + .request(http_client); + if let Err(e) = tx.send(resp) { + error!("OAuth channel send error: {e}"); + } + }); + let token_response = rx.recv().map_err(|_| OAuthError::Recv)?; + let token = token_response.map_err(|e| OAuthError::ExchangeCode { e: e.to_string() })?; + trace!("Obtained new access token: {token:?}"); + + let token_scopes: Vec = match token.scopes() { + Some(s) => s.iter().map(|s| s.to_string()).collect(), + _ => scopes.into_iter().map(|s| s.to_string()).collect(), + }; + let refresh_token = match token.refresh_token() { + Some(t) => t.secret().to_string(), + _ => "".to_string(), // Spotify always provides a refresh token. + }; + Ok(OAuthToken { + access_token: token.access_token().secret().to_string(), + refresh_token, + expires_at: Instant::now() + + token + .expires_in() + .unwrap_or_else(|| Duration::from_secs(3600)), + token_type: format!("{:?}", token.token_type()).to_string(), // Urgh!? + scopes: token_scopes, + }) +} + +#[cfg(test)] +mod test { + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; + + use super::*; + + #[test] + fn get_socket_address_none() { + // No port + assert_eq!(get_socket_address("http://127.0.0.1/foo"), None); + assert_eq!(get_socket_address("http://127.0.0.1:/foo"), None); + assert_eq!(get_socket_address("http://[::1]/foo"), None); + // Not localhost + assert_eq!(get_socket_address("http://56.0.0.1:1234/foo"), None); + assert_eq!( + get_socket_address("http://[3ffe:2a00:100:7031::1]:1234/foo"), + None + ); + // Not http + assert_eq!(get_socket_address("https://127.0.0.1/foo"), None); + } + + #[test] + fn get_socket_address_localhost() { + let localhost_v4 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1234); + let localhost_v6 = SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 8888); + + assert_eq!( + get_socket_address("http://127.0.0.1:1234/foo"), + Some(localhost_v4) + ); + assert_eq!( + get_socket_address("http://[0:0:0:0:0:0:0:1]:8888/foo"), + Some(localhost_v6) + ); + assert_eq!( + get_socket_address("http://[::1]:8888/foo"), + Some(localhost_v6) + ); + } +} diff --git a/src/app.css b/src/app.css new file mode 100644 index 0000000..310d315 --- /dev/null +++ b/src/app.css @@ -0,0 +1,15 @@ +@define-color accent_bg_color @green_5; +@define-color accent_color @green_3; + +.container { + transition: opacity 0.3s ease; + opacity: 0; +} + +.container--loaded { + opacity: 1; +} + +.playlist__title-entry--ro { + background: none; +} diff --git a/src/app/batch_loader.rs b/src/app/batch_loader.rs new file mode 100644 index 0000000..7c0dd3b --- /dev/null +++ b/src/app/batch_loader.rs @@ -0,0 +1,105 @@ +use gettextrs::gettext; +use std::sync::Arc; + +use crate::api::{SpotifyApiClient, SpotifyApiError}; +use crate::app::models::*; +use crate::app::AppAction; + +// A wrapper around the Spotify API to load batches of songs from various sources (see below) +#[derive(Clone)] +pub struct BatchLoader { + api: Arc, +} + +// The sources mentionned above +#[derive(Clone, Debug)] +pub enum SongsSource { + Playlist(String), + Album(String), + SavedTracks, +} + +impl PartialEq for SongsSource { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Playlist(l), Self::Playlist(r)) => l == r, + (Self::Album(l), Self::Album(r)) => l == r, + (Self::SavedTracks, Self::SavedTracks) => true, + _ => false, + } + } +} + +impl Eq for SongsSource {} + +impl SongsSource { + pub fn has_spotify_uri(&self) -> bool { + matches!(self, Self::Playlist(_) | Self::Album(_)) + } + + pub fn spotify_uri(&self) -> Option { + match self { + Self::Playlist(id) => Some(format!("spotify:playlist:{}", id)), + Self::Album(id) => Some(format!("spotify:album:{}", id)), + _ => None, + } + } +} + +// How to query for a batch: specify a source, and a batch to get (offset + number of elements to get) +#[derive(Debug)] +pub struct BatchQuery { + pub source: SongsSource, + pub batch: Batch, +} + +impl BatchQuery { + // Given a query, compute the next batch to get (if any) + pub fn next(&self) -> Option { + let Self { source, batch } = self; + Some(Self { + source: source.clone(), + batch: batch.next()?, + }) + } +} + +impl BatchLoader { + pub fn new(api: Arc) -> Self { + Self { api } + } + + // Query a batch and create an action when it's been retrieved succesfully + pub async fn query( + &self, + query: BatchQuery, + create_action: ActionCreator, + ) -> Option + where + ActionCreator: FnOnce(SongsSource, SongBatch) -> AppAction, + { + let api = Arc::clone(&self.api); + + let Batch { + offset, batch_size, .. + } = query.batch; + let result = match &query.source { + SongsSource::Playlist(id) => api.get_playlist_tracks(id, offset, batch_size).await, + SongsSource::SavedTracks => api.get_saved_tracks(offset, batch_size).await, + SongsSource::Album(id) => api.get_album_tracks(id, offset, batch_size).await, + }; + + match result { + Ok(batch) => Some(create_action(query.source, batch)), + // No token? Why was the batch loader called? Ah, whatever + Err(SpotifyApiError::NoToken) => None, + Err(err) => { + error!("Spotify API error: {}", err); + Some(AppAction::ShowNotification(gettext( + // translators: This notification is the default message for unhandled errors. Logs refer to console output. + "An error occured. Check logs for details!", + ))) + } + } + } +} diff --git a/src/app/components/album/album.blp b/src/app/components/album/album.blp new file mode 100644 index 0000000..661bbf7 --- /dev/null +++ b/src/app/components/album/album.blp @@ -0,0 +1,75 @@ +using Gtk 4.0; +using Adw 1; + +template $AlbumWidget : Adw.Bin { + Button cover_btn { + hexpand: false; + halign: center; + + Box { + halign: center; + valign: start; + margin-top: 6; + margin-bottom: 6; + orientation: vertical; + spacing: 6; + + Image cover_image { + icon-name: "media-playback-start-symbolic"; + + styles [ + "card", + ] + } + + Label album_label { + label: "Album"; + justify: center; + wrap: true; + wrap-mode: word; + ellipsize: end; + max-width-chars: 1; + margin-top: 6; + + styles [ + "title-4", + ] + } + + Label artist_label { + label: "Artist"; + justify: center; + wrap: true; + wrap-mode: word; + ellipsize: end; + max-width-chars: 1; + + styles [ + "body", + ] + } + + Label year_label { + label: "Year"; + justify: center; + wrap: true; + wrap-mode: word_char; + max-width-chars: 1; + sensitive: false; + + styles [ + "body", + ] + } + } + + styles [ + "flat", + ] + } + + styles [ + "container", + "album", + ] +} diff --git a/src/app/components/album/album.css b/src/app/components/album/album.css new file mode 100644 index 0000000..9226f52 --- /dev/null +++ b/src/app/components/album/album.css @@ -0,0 +1,34 @@ +/* large style */ + +leaflet.unfolded .album .card { + min-width: 200px; + min-height: 200px; + border-radius: 6px; +} + +leaflet.unfolded .album { + margin-top: 6px; + margin-bottom: 6px; +} + +leaflet.unfolded .album button { + border-radius: 12px; +} +/* small style */ + +leaflet.folded .album .card { + min-width: 100px; + min-height: 100px; + border-radius: 6px; + margin-top: 0px; + margin-bottom: 0px; +} + +leaflet.unfolded .album { + margin-top: 0px; + margin-bottom: 0px; +} + +leaflet.folded .album button { + border-radius: 6px; +} diff --git a/src/app/components/album/album.rs b/src/app/components/album/album.rs new file mode 100644 index 0000000..cc88953 --- /dev/null +++ b/src/app/components/album/album.rs @@ -0,0 +1,129 @@ +use crate::app::components::display_add_css_provider; +use crate::app::dispatch::Worker; +use crate::app::loader::ImageLoader; +use crate::app::models::AlbumModel; + +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; +use libadwaita::subclass::prelude::BinImpl; + +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/album.ui")] + pub struct AlbumWidget { + #[template_child] + pub album_label: TemplateChild, + + #[template_child] + pub artist_label: TemplateChild, + + #[template_child] + pub year_label: TemplateChild, + + #[template_child] + pub cover_btn: TemplateChild, + + #[template_child] + pub cover_image: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for AlbumWidget { + const NAME: &'static str = "AlbumWidget"; + type Type = super::AlbumWidget; + type ParentType = libadwaita::Bin; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for AlbumWidget {} + impl WidgetImpl for AlbumWidget {} + impl BinImpl for AlbumWidget {} +} + +glib::wrapper! { + pub struct AlbumWidget(ObjectSubclass) @extends gtk::Widget, libadwaita::Bin; +} + +impl Default for AlbumWidget { + fn default() -> Self { + Self::new() + } +} + +impl AlbumWidget { + pub fn new() -> Self { + display_add_css_provider(resource!("/components/album.css")); + glib::Object::new() + } + + pub fn for_model(album_model: &AlbumModel, worker: Worker) -> Self { + let _self = Self::new(); + _self.bind(album_model, worker); + _self + } + + fn set_loaded(&self) { + self.add_css_class("container--loaded"); + } + + fn set_image(&self, pixbuf: Option<&gdk_pixbuf::Pixbuf>) { + self.imp().cover_image.set_from_pixbuf(pixbuf); + } + + fn bind(&self, album_model: &AlbumModel, worker: Worker) { + let widget = self.imp(); + widget.cover_image.set_overflow(gtk::Overflow::Hidden); + + if let Some(cover_art) = album_model.cover() { + let _self = self.downgrade(); + worker.send_local_task(async move { + if let Some(_self) = _self.upgrade() { + let loader = ImageLoader::new(); + let result = loader.load_remote(&cover_art, "jpg", 200, 200).await; + _self.set_image(result.as_ref()); + _self.set_loaded(); + } + }); + } else { + self.set_loaded(); + } + + album_model + .bind_property("album", &*widget.album_label, "label") + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(); + + album_model + .bind_property("artist", &*widget.artist_label, "label") + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(); + + if album_model.year() > 0 { + album_model + .bind_property("year", &*widget.year_label, "label") + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(); + } else { + widget.year_label.set_visible(false); + } + } + + pub fn connect_album_pressed(&self, f: F) { + self.imp() + .cover_btn + .connect_clicked(clone!(@weak self as _self => move |_| { + f(&_self); + })); + } +} diff --git a/src/app/components/album/mod.rs b/src/app/components/album/mod.rs new file mode 100644 index 0000000..59a8a08 --- /dev/null +++ b/src/app/components/album/mod.rs @@ -0,0 +1,3 @@ +#[allow(clippy::module_inception)] +mod album; +pub use album::AlbumWidget; diff --git a/src/app/components/artist/artist.blp b/src/app/components/artist/artist.blp new file mode 100644 index 0000000..9f75990 --- /dev/null +++ b/src/app/components/artist/artist.blp @@ -0,0 +1,31 @@ +using Gtk 4.0; +using Adw 1; + +template $ArtistWidget : Box { + orientation: vertical; + + Button avatar_btn { + vexpand: true; + width-request: 150; + height-request: 150; + receives-default: true; + halign: center; + valign: center; + has-frame: false; + + Adw.Avatar avatar { + halign: center; + valign: center; + show-initials: true; + size: 150; + } + + styles [ + "circular", + ] + } + + Label artist { + label: "Artist Name"; + } +} diff --git a/src/app/components/artist/mod.rs b/src/app/components/artist/mod.rs new file mode 100644 index 0000000..31e0c4c --- /dev/null +++ b/src/app/components/artist/mod.rs @@ -0,0 +1,95 @@ +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; + +use crate::app::loader::ImageLoader; +use crate::app::models::ArtistModel; +use crate::app::Worker; + +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/artist.ui")] + pub struct ArtistWidget { + #[template_child] + pub artist: TemplateChild, + + #[template_child] + pub avatar_btn: TemplateChild, + + #[template_child] + pub avatar: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for ArtistWidget { + const NAME: &'static str = "ArtistWidget"; + type Type = super::ArtistWidget; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for ArtistWidget {} + impl WidgetImpl for ArtistWidget {} + impl BoxImpl for ArtistWidget {} +} + +glib::wrapper! { + pub struct ArtistWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; +} + +impl Default for ArtistWidget { + fn default() -> Self { + Self::new() + } +} + +impl ArtistWidget { + pub fn new() -> Self { + glib::Object::new() + } + + pub fn for_model(model: &ArtistModel, worker: Worker) -> Self { + let _self = Self::new(); + _self.bind(model, worker); + _self + } + + pub fn connect_artist_pressed(&self, f: F) { + self.imp() + .avatar_btn + .connect_clicked(clone!(@weak self as _self => move |_| { + f(&_self); + })); + } + + fn bind(&self, model: &ArtistModel, worker: Worker) { + let widget = self.imp(); + + if let Some(url) = model.image() { + let avatar = widget.avatar.downgrade(); + worker.send_local_task(async move { + if let Some(avatar) = avatar.upgrade() { + let loader = ImageLoader::new(); + let pixbuf = loader.load_remote(&url, "jpg", 200, 200).await; + let texture = pixbuf.as_ref().map(gdk::Texture::for_pixbuf); + avatar.set_custom_image(texture.as_ref()); + } + }); + } + + model + .bind_property("artist", &*widget.artist, "label") + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(); + } +} diff --git a/src/app/components/artist_details/artist_details.blp b/src/app/components/artist_details/artist_details.blp new file mode 100644 index 0000000..44446d9 --- /dev/null +++ b/src/app/components/artist_details/artist_details.blp @@ -0,0 +1,63 @@ +using Gtk 4.0; + +template $ArtistDetailsWidget : Box { + ScrolledWindow scrolled_window { + hscrollbar-policy: never; + hexpand: true; + vexpand: true; + Box { + margin-start: 8; + margin-end: 8; + margin-top: 8; + margin-bottom: 8; + orientation: vertical; + spacing: 16; + + Box { + orientation: vertical; + + Label { + halign: start; + margin-start: 8; + margin-end: 8; + + /* Translators: Title of the section that shows 5 of the top tracks for an artist, as defined by Spotify. */ + + label: _("Top tracks"); + + styles [ + "title-4", + ] + } + + ListView top_tracks { + } + } + + Expander { + margin-top: 8; + margin-bottom: 8; + expanded: true; + + FlowBox artist_releases { + height-request: 100; + hexpand: true; + min-children-per-line: 1; + selection-mode: none; + activate-on-single-click: false; + } + + [label] + Label { + /* Translators: Title of the sections that contains all releases from an artist (both singles and albums). */ + + label: _("Releases"); + } + } + } + } + + styles [ + "artist", + ] +} diff --git a/src/app/components/artist_details/artist_details.css b/src/app/components/artist_details/artist_details.css new file mode 100644 index 0000000..ef5f0fa --- /dev/null +++ b/src/app/components/artist_details/artist_details.css @@ -0,0 +1,18 @@ +listview.artist__top-tracks { + padding: 8px; + border-radius: 8px; + margin: 8px; +} + +listview.artist__top-tracks row { + border-radius: 4px; +} + +.artist { + transition: opacity .3s ease; + opacity: 0; +} + +.artist__loaded { + opacity: 1; +} diff --git a/src/app/components/artist_details/artist_details.rs b/src/app/components/artist_details/artist_details.rs new file mode 100644 index 0000000..317201a --- /dev/null +++ b/src/app/components/artist_details/artist_details.rs @@ -0,0 +1,168 @@ +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; +use std::rc::Rc; + +use crate::app::components::{ + display_add_css_provider, AlbumWidget, Component, EventListener, Playlist, +}; +use crate::app::{models::*, ListStore}; +use crate::app::{AppEvent, BrowserEvent, Worker}; + +use super::ArtistDetailsModel; + +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/artist_details.ui")] + pub struct ArtistDetailsWidget { + #[template_child] + pub scrolled_window: TemplateChild, + + #[template_child] + pub top_tracks: TemplateChild, + + #[template_child] + pub artist_releases: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for ArtistDetailsWidget { + const NAME: &'static str = "ArtistDetailsWidget"; + type Type = super::ArtistDetailsWidget; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for ArtistDetailsWidget {} + impl WidgetImpl for ArtistDetailsWidget {} + impl BoxImpl for ArtistDetailsWidget {} +} + +glib::wrapper! { + pub struct ArtistDetailsWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; +} + +impl ArtistDetailsWidget { + fn new() -> Self { + display_add_css_provider(resource!("/components/artist_details.css")); + glib::Object::new() + } + + fn top_tracks_widget(&self) -> >k::ListView { + self.imp().top_tracks.as_ref() + } + + fn set_loaded(&self) { + self.add_css_class("artist__loaded"); + } + + fn connect_bottom_edge(&self, f: F) + where + F: Fn() + 'static, + { + self.imp() + .scrolled_window + .connect_edge_reached(move |_, pos| { + if let gtk::PositionType::Bottom = pos { + f() + } + }); + } + + fn bind_artist_releases( + &self, + worker: Worker, + store: &ListStore, + on_album_pressed: F, + ) where + F: Fn(String) + Clone + 'static, + { + self.imp() + .artist_releases + .bind_model(Some(store.unsafe_store()), move |item| { + let item = item.downcast_ref::().unwrap(); + let child = gtk::FlowBoxChild::new(); + let album = AlbumWidget::for_model(item, worker.clone()); + let f = on_album_pressed.clone(); + album.connect_album_pressed(clone!(@weak item => move |_| { + f(item.uri()); + })); + child.set_child(Some(&album)); + child.upcast::() + }); + } +} + +pub struct ArtistDetails { + model: Rc, + widget: ArtistDetailsWidget, + children: Vec>, +} + +impl ArtistDetails { + pub fn new(model: Rc, worker: Worker) -> Self { + model.load_artist_details(model.id.clone()); + + let widget = ArtistDetailsWidget::new(); + + widget.connect_bottom_edge(clone!(@weak model => move || { + model.load_more(); + })); + + if let Some(store) = model.get_list_store() { + widget.bind_artist_releases( + worker.clone(), + &store, + clone!(@weak model => move |id| { + model.open_album(id); + }), + ); + } + + let playlist = Box::new(Playlist::new( + widget.top_tracks_widget().clone(), + Rc::clone(&model), + worker, + )); + + Self { + model, + widget, + children: vec![playlist], + } + } +} + +impl Component for ArtistDetails { + fn get_root_widget(&self) -> >k::Widget { + self.widget.upcast_ref() + } + + fn get_children(&mut self) -> Option<&mut Vec>> { + Some(&mut self.children) + } +} + +impl EventListener for ArtistDetails { + fn on_event(&mut self, event: &AppEvent) { + match event { + AppEvent::BrowserEvent(BrowserEvent::ArtistDetailsUpdated(id)) + if id == &self.model.id => + { + self.widget.set_loaded(); + } + _ => {} + } + self.broadcast_event(event); + } +} diff --git a/src/app/components/artist_details/artist_details_model.rs b/src/app/components/artist_details/artist_details_model.rs new file mode 100644 index 0000000..94f6782 --- /dev/null +++ b/src/app/components/artist_details/artist_details_model.rs @@ -0,0 +1,187 @@ +use gio::prelude::*; +use gio::SimpleActionGroup; +use std::ops::Deref; +use std::rc::Rc; + +use crate::api::SpotifyApiError; +use crate::app::components::SimpleHeaderBarModel; +use crate::app::components::{labels, PlaylistModel}; +use crate::app::models::*; +use crate::app::state::SelectionContext; +use crate::app::state::{ + BrowserAction, BrowserEvent, PlaybackAction, SelectionAction, SelectionState, +}; +use crate::app::{ActionDispatcher, AppAction, AppEvent, AppModel, ListStore}; + +pub struct ArtistDetailsModel { + pub id: String, + app_model: Rc, + dispatcher: Box, +} + +impl ArtistDetailsModel { + pub fn new(id: String, app_model: Rc, dispatcher: Box) -> Self { + Self { + id, + app_model, + dispatcher, + } + } + + pub fn get_artist_name(&self) -> Option + '_> { + self.app_model + .map_state_opt(|s| s.browser.artist_state(&self.id)?.artist.as_ref()) + } + + pub fn get_list_store(&self) -> Option> + '_> { + self.app_model + .map_state_opt(|s| Some(&s.browser.artist_state(&self.id)?.albums)) + } + + pub fn load_artist_details(&self, id: String) { + let api = self.app_model.get_spotify(); + self.dispatcher + .call_spotify_and_dispatch(move || async move { + let artist = api.get_artist(&id).await; + match artist { + Ok(artist) => Ok(BrowserAction::SetArtistDetails(Box::new(artist)).into()), + Err(SpotifyApiError::BadStatus(400, _)) + | Err(SpotifyApiError::BadStatus(404, _)) => { + Ok(BrowserAction::NavigationPop.into()) + } + Err(e) => Err(e), + } + }); + } + + pub fn open_album(&self, id: String) { + self.dispatcher.dispatch(AppAction::ViewAlbum(id)); + } + + pub fn load_more(&self) -> Option<()> { + let api = self.app_model.get_spotify(); + let state = self.app_model.get_state(); + let next_page = &state.browser.artist_state(&self.id)?.next_page; + + let id = next_page.data.clone(); + let batch_size = next_page.batch_size; + let offset = next_page.next_offset?; + + self.dispatcher + .call_spotify_and_dispatch(move || async move { + api.get_artist_albums(&id, offset, batch_size) + .await + .map(|albums| BrowserAction::AppendArtistReleases(id, albums).into()) + }); + + Some(()) + } +} + +impl PlaylistModel for ArtistDetailsModel { + fn song_list_model(&self) -> SongListModel { + self.app_model + .get_state() + .browser + .artist_state(&self.id) + .expect("illegal attempt to read artist_state") + .top_tracks + .clone() + } + + fn is_paused(&self) -> bool { + !self.app_model.get_state().playback.is_playing() + } + + fn current_song_id(&self) -> Option { + self.app_model.get_state().playback.current_song_id() + } + + fn play_song_at(&self, _pos: usize, id: &str) { + let tracks: Vec = self.song_list_model().collect(); + self.dispatcher + .dispatch(PlaybackAction::LoadSongs(tracks).into()); + self.dispatcher + .dispatch(PlaybackAction::Load(id.to_string()).into()); + } + + fn actions_for(&self, id: &str) -> Option { + let song = self.song_list_model().get(id)?; + let song = song.description(); + + let group = SimpleActionGroup::new(); + + for view_artist in song.make_artist_actions(self.dispatcher.box_clone(), None) { + group.add_action(&view_artist); + } + group.add_action(&song.make_album_action(self.dispatcher.box_clone(), None)); + group.add_action(&song.make_link_action(None)); + group.add_action(&song.make_queue_action(self.dispatcher.box_clone(), None)); + + Some(group.upcast()) + } + + fn menu_for(&self, id: &str) -> Option { + let song = self.song_list_model().get(id)?; + let song = song.description(); + + let menu = gio::Menu::new(); + menu.append(Some(&*labels::VIEW_ALBUM), Some("song.view_album")); + for artist in song.artists.iter().filter(|a| self.id != a.id) { + menu.append( + Some(&labels::more_from_label(&artist.name)), + Some(&format!("song.view_artist_{}", artist.id)), + ); + } + + menu.append(Some(&*labels::COPY_LINK), Some("song.copy_link")); + menu.append(Some(&*labels::ADD_TO_QUEUE), Some("song.queue")); + Some(menu.upcast()) + } + + fn select_song(&self, id: &str) { + let song = self.song_list_model().get(id); + if let Some(song) = song { + self.dispatcher + .dispatch(SelectionAction::Select(vec![song.into_description()]).into()); + } + } + + fn deselect_song(&self, id: &str) { + self.dispatcher + .dispatch(SelectionAction::Deselect(vec![id.to_string()]).into()); + } + + fn enable_selection(&self) -> bool { + self.dispatcher + .dispatch(AppAction::EnableSelection(SelectionContext::Default)); + true + } + + fn selection(&self) -> Option + '_>> { + Some(Box::new(self.app_model.map_state(|s| &s.selection))) + } +} + +impl SimpleHeaderBarModel for ArtistDetailsModel { + fn title(&self) -> Option { + Some(self.get_artist_name()?.clone()) + } + + fn title_updated(&self, event: &AppEvent) -> bool { + matches!( + event, + AppEvent::BrowserEvent(BrowserEvent::ArtistDetailsUpdated(_)) + ) + } + + fn selection_context(&self) -> Option { + Some(SelectionContext::Default) + } + + fn select_all(&self) { + let songs: Vec = self.song_list_model().collect(); + self.dispatcher + .dispatch(SelectionAction::Select(songs).into()); + } +} diff --git a/src/app/components/artist_details/mod.rs b/src/app/components/artist_details/mod.rs new file mode 100644 index 0000000..ec36bfd --- /dev/null +++ b/src/app/components/artist_details/mod.rs @@ -0,0 +1,6 @@ +#[allow(clippy::module_inception)] +mod artist_details; +pub use artist_details::*; + +mod artist_details_model; +pub use artist_details_model::*; diff --git a/src/app/components/details/album_header.blp b/src/app/components/details/album_header.blp new file mode 100644 index 0000000..20f3af3 --- /dev/null +++ b/src/app/components/details/album_header.blp @@ -0,0 +1,136 @@ +using Gtk 4.0; + +template $AlbumHeaderWidget : Box { + valign: start; + vexpand: false; + margin-start: 6; + margin-end: 6; + margin-bottom: 6; + + Overlay album_overlay { + overflow: hidden; + halign: center; + margin-top: 18; + margin-bottom: 6; + margin-start: 6; + + Image album_art { + width-request: 160; + height-request: 160; + icon-name: "emblem-music-symbolic"; + } + + [overlay] + Button info_button { + icon-name: "preferences-system-details-symbolic"; + halign: end; + valign: end; + margin-start: 6; + margin-end: 6; + margin-top: 6; + margin-bottom: 6; + tooltip-text: "Album Info"; + + styles [ + "circular", + "osd", + ] + } + + styles [ + "card", + ] + } + + Box album_info { + hexpand: true; + valign: center; + orientation: vertical; + spacing: 6; + margin-start: 18; + + Label album_label { + xalign: 0; + halign: start; + label: "Album"; + wrap: true; + ellipsize: end; + max-width-chars: 50; + lines: 4; + + styles [ + "title-1", + ] + } + + LinkButton artist_button { + receives-default: true; + halign: start; + valign: center; + has-frame: false; + + Label artist_button_label { + hexpand: true; + vexpand: true; + label: "Artist"; + ellipsize: middle; + } + + styles [ + "title-4", + ] + } + + Label year_label { + xalign: 0; + halign: start; + label: "Year"; + ellipsize: end; + max-width-chars: 50; + lines: 1; + sensitive: false; + + styles [ + "body", + ] + } + } + + Box button_box { + orientation: horizontal; + valign: center; + + margin-end: 6; + spacing: 8; + + Button play_button { + receives-default: true; + halign: center; + valign: center; + tooltip-text: "Play"; + icon-name: "media-playback-start-symbolic"; + + styles [ + "circular", + "play__button", + ] + } + + Button like_button { + receives-default: true; + halign: center; + valign: center; + tooltip-text: "Add to Library"; + + styles [ + "circular", + "like__button", + ] + } + } + + + styles [ + "album__header", + ] +} diff --git a/src/app/components/details/album_header.css b/src/app/components/details/album_header.css new file mode 100644 index 0000000..f5a8528 --- /dev/null +++ b/src/app/components/details/album_header.css @@ -0,0 +1,33 @@ +.album__header .title-4 label { + color: @window_fg_color; + font-weight: bold; + text-decoration: none; +} + + +.album__header .title-4:hover { + border-radius: 6px; + background-image: image(alpha(currentColor, 0.08)); +} + +clamp.details__clamp { + background-color: @view_bg_color; + box-shadow: inset 0px -1px 0px @borders; +} + +headerbar.details__headerbar { + transition: background-color .3s ease; +} + +headerbar.flat.details__headerbar windowtitle { + opacity: 0; +} + +headerbar.details__headerbar windowtitle { + transition: opacity .3s ease; + opacity: 1; +} + +.details__headerbar.flat { + background-color: @view_bg_color; +} diff --git a/src/app/components/details/album_header.rs b/src/app/components/details/album_header.rs new file mode 100644 index 0000000..af6f8dd --- /dev/null +++ b/src/app/components/details/album_header.rs @@ -0,0 +1,166 @@ +use crate::app::components::display_add_css_provider; +use gettextrs::gettext; +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::{glib, CompositeTemplate}; + +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/album_header.ui")] + pub struct AlbumHeaderWidget { + #[template_child] + pub album_overlay: TemplateChild, + + #[template_child] + pub album_label: TemplateChild, + + #[template_child] + pub album_art: TemplateChild, + + #[template_child] + pub button_box: TemplateChild, + + #[template_child] + pub like_button: TemplateChild, + + #[template_child] + pub play_button: TemplateChild, + + #[template_child] + pub info_button: TemplateChild, + + #[template_child] + pub album_info: TemplateChild, + + #[template_child] + pub artist_button: TemplateChild, + + #[template_child] + pub artist_button_label: TemplateChild, + + #[template_child] + pub year_label: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for AlbumHeaderWidget { + const NAME: &'static str = "AlbumHeaderWidget"; + type Type = super::AlbumHeaderWidget; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + display_add_css_provider(resource!("/components/album_header.css")); + obj.init_template(); + } + } + + impl ObjectImpl for AlbumHeaderWidget {} + impl WidgetImpl for AlbumHeaderWidget {} + impl BoxImpl for AlbumHeaderWidget {} +} + +glib::wrapper! { + pub struct AlbumHeaderWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; +} + +impl Default for AlbumHeaderWidget { + fn default() -> Self { + Self::new() + } +} + +impl AlbumHeaderWidget { + pub fn new() -> Self { + glib::Object::new() + } + + pub fn connect_play(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().play_button.connect_clicked(move |_| f()); + } + + pub fn connect_liked(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().like_button.connect_clicked(move |_| f()); + } + + pub fn connect_info(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().info_button.connect_clicked(move |_| f()); + } + + pub fn connect_artist_clicked(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().artist_button.connect_activate_link(move |_| { + f(); + glib::signal::Inhibit(true) + }); + } + + pub fn set_liked(&self, is_liked: bool) { + self.imp().like_button.set_icon_name(if is_liked { + "starred-symbolic" + } else { + "non-starred-symbolic" + }); + } + + pub fn set_playing(&self, is_playing: bool) { + let playback_icon = if is_playing { + "media-playback-pause-symbolic" + } else { + "media-playback-start-symbolic" + }; + + let translated_tooltip = if is_playing { + gettext("Pause") + } else { + gettext("Play") + }; + let tooltip_text = Some(translated_tooltip.as_str()); + + self.imp().play_button.set_icon_name(playback_icon); + self.imp().play_button.set_tooltip_text(tooltip_text); + } + + pub fn set_artwork(&self, art: &gdk_pixbuf::Pixbuf) { + self.imp().album_art.set_from_pixbuf(Some(art)); + } + + pub fn set_album_and_artist_and_year(&self, album: &str, artist: &str, year: Option) { + let widget = self.imp(); + widget.album_label.set_label(album); + widget.artist_button_label.set_label(artist); + match year { + Some(year) => widget.year_label.set_label(&year.to_string()), + None => widget.year_label.set_visible(false), + } + } + + pub fn set_centered(&self) { + let widget = self.imp(); + widget.album_label.set_halign(gtk::Align::Center); + widget.album_label.set_justify(gtk::Justification::Center); + widget.artist_button.set_halign(gtk::Align::Center); + widget.year_label.set_halign(gtk::Align::Center); + widget.button_box.set_halign(gtk::Align::Center); + widget.album_overlay.set_margin_start(0); + widget.button_box.set_margin_end(0); + widget.album_info.set_margin_start(0); + } +} diff --git a/src/app/components/details/details.blp b/src/app/components/details/details.blp new file mode 100644 index 0000000..1450a49 --- /dev/null +++ b/src/app/components/details/details.blp @@ -0,0 +1,55 @@ +using Gtk 4.0; +using Adw 1; + +template $AlbumDetailsWidget : Adw.Bin { + Box { + orientation: vertical; + vexpand: true; + hexpand: true; + + $HeaderBarWidget headerbar { + } + + $ScrollingHeaderWidget scrolling_header { + [header] + WindowHandle { + Adw.Clamp { + maximum-size: 900; + + Adw.Squeezer { + switch-threshold-policy: natural; + valign: center; + homogeneous: false; + transition-type: crossfade; + + $AlbumHeaderWidget header_widget { + } + + $AlbumHeaderWidget header_mobile { + orientation: "vertical"; + spacing: "12"; + } + } + + styles [ + "details__clamp", + ] + } + } + + Adw.ClampScrollable { + maximum-size: 900; + + ListView album_tracks { + styles [ + "album__tracks", + ] + } + } + + styles [ + "container", + ] + } + } +} diff --git a/src/app/components/details/details.rs b/src/app/components/details/details.rs new file mode 100644 index 0000000..8bbc93f --- /dev/null +++ b/src/app/components/details/details.rs @@ -0,0 +1,335 @@ +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; +use std::rc::Rc; + +use super::album_header::AlbumHeaderWidget; +use super::release_details::ReleaseDetailsWindow; +use super::DetailsModel; + +use crate::app::components::{ + Component, EventListener, HeaderBarComponent, HeaderBarWidget, Playlist, ScrollingHeaderWidget, +}; +use crate::app::dispatch::Worker; +use crate::app::loader::ImageLoader; +use crate::app::state::PlaybackEvent; +use crate::app::{AppEvent, BrowserEvent}; + +mod imp { + + use libadwaita::subclass::prelude::BinImpl; + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/details.ui")] + pub struct AlbumDetailsWidget { + #[template_child] + pub scrolling_header: TemplateChild, + + #[template_child] + pub headerbar: TemplateChild, + + #[template_child] + pub header_widget: TemplateChild, + + #[template_child] + pub header_mobile: TemplateChild, + + #[template_child] + pub album_tracks: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for AlbumDetailsWidget { + const NAME: &'static str = "AlbumDetailsWidget"; + type Type = super::AlbumDetailsWidget; + type ParentType = libadwaita::Bin; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for AlbumDetailsWidget { + fn constructed(&self) { + self.parent_constructed(); + self.header_mobile.set_centered(); + self.headerbar.add_classes(&["details__headerbar"]); + } + } + + impl WidgetImpl for AlbumDetailsWidget {} + impl BinImpl for AlbumDetailsWidget {} +} + +glib::wrapper! { + pub struct AlbumDetailsWidget(ObjectSubclass) @extends gtk::Widget, libadwaita::Bin; +} + +impl AlbumDetailsWidget { + fn new() -> Self { + glib::Object::new() + } + + fn set_header_visible(&self, visible: bool) { + let widget = self.imp(); + widget.headerbar.set_title_visible(true); + if visible { + widget.headerbar.add_classes(&["flat"]); + } else { + widget.headerbar.remove_classes(&["flat"]); + } + } + + fn connect_header(&self) { + self.set_header_visible(false); + self.imp().scrolling_header.connect_header_visibility( + clone!(@weak self as _self => move |visible| { + _self.set_header_visible(visible); + }), + ); + } + + fn connect_bottom_edge(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().scrolling_header.connect_bottom_edge(f); + } + + fn headerbar_widget(&self) -> &HeaderBarWidget { + self.imp().headerbar.as_ref() + } + + fn album_tracks_widget(&self) -> >k::ListView { + self.imp().album_tracks.as_ref() + } + + fn set_loaded(&self) { + self.imp() + .scrolling_header + .add_css_class("container--loaded"); + } + + fn connect_liked(&self, f: F) + where + F: Fn() + Clone + 'static, + { + self.imp().header_widget.connect_liked(f.clone()); + self.imp().header_mobile.connect_liked(f); + } + + fn connect_play(&self, f: F) + where + F: Fn() + Clone + 'static, + { + self.imp().header_widget.connect_play(f.clone()); + self.imp().header_mobile.connect_play(f); + } + + fn connect_info(&self, f: F) + where + F: Fn() + Clone + 'static, + { + self.imp().header_widget.connect_info(f.clone()); + self.imp().header_mobile.connect_info(f); + } + + fn set_liked(&self, is_liked: bool) { + self.imp().header_widget.set_liked(is_liked); + self.imp().header_mobile.set_liked(is_liked); + } + + fn set_playing(&self, is_playing: bool) { + self.imp().header_widget.set_playing(is_playing); + self.imp().header_mobile.set_playing(is_playing); + } + + fn set_album_and_artist_and_year(&self, album: &str, artist: &str, year: Option) { + self.imp() + .header_widget + .set_album_and_artist_and_year(album, artist, year); + self.imp() + .header_mobile + .set_album_and_artist_and_year(album, artist, year); + self.imp().headerbar.set_title_and_subtitle(album, artist); + } + + fn set_artwork(&self, art: &gdk_pixbuf::Pixbuf) { + self.imp().header_widget.set_artwork(art); + self.imp().header_mobile.set_artwork(art); + } + + fn connect_artist_clicked(&self, f: F) + where + F: Fn() + Clone + 'static, + { + self.imp().header_widget.connect_artist_clicked(f.clone()); + self.imp().header_mobile.connect_artist_clicked(f); + } +} + +pub struct Details { + model: Rc, + worker: Worker, + widget: AlbumDetailsWidget, + modal: ReleaseDetailsWindow, + children: Vec>, +} + +impl Details { + pub fn new(model: Rc, worker: Worker, leaflet: &libadwaita::Leaflet) -> Self { + if model.get_album_info().is_none() { + model.load_album_info(); + } + + let widget = AlbumDetailsWidget::new(); + + let playlist = Box::new(Playlist::new( + widget.album_tracks_widget().clone(), + model.clone(), + worker.clone(), + )); + + let headerbar_widget = widget.headerbar_widget(); + headerbar_widget.bind_to_leaflet(leaflet); + let headerbar = Box::new(HeaderBarComponent::new( + headerbar_widget.clone(), + model.to_headerbar_model(), + )); + + let modal = ReleaseDetailsWindow::new(); + + widget.connect_liked(clone!(@weak model => move || model.toggle_save_album())); + + widget.connect_play(clone!(@weak model => move || model.toggle_play_album())); + + widget.connect_header(); + + widget.connect_bottom_edge(clone!(@weak model => move || { + model.load_more(); + })); + + widget.connect_info(clone!(@weak modal, @weak widget => move || { + let modal = modal.upcast_ref::(); + modal.set_modal(true); + modal.set_transient_for( + widget + .root() + .and_then(|r| r.downcast::().ok()) + .as_ref(), + ); + modal.set_visible(true); + })); + + Self { + model, + worker, + widget, + modal, + children: vec![playlist, headerbar], + } + } + + fn update_liked(&self) { + if let Some(info) = self.model.get_album_info() { + let is_liked = info.description.is_liked; + self.widget.set_liked(is_liked); + self.widget.set_liked(is_liked); + } + } + + fn update_playing(&self, is_playing: bool) { + if !self.model.album_is_playing() || !self.model.is_playing() { + self.widget.set_playing(false); + return; + } + self.widget.set_playing(is_playing); + } + + fn update_details(&mut self) { + if let Some(album) = self.model.get_album_info() { + let details = &album.release_details; + let album = &album.description; + + self.widget.set_liked(album.is_liked); + + self.widget.set_album_and_artist_and_year( + &album.title[..], + &album.artists_name(), + album.year(), + ); + + self.widget.connect_artist_clicked( + clone!(@weak self.model as model => move || model.view_artist()), + ); + + self.modal.set_details( + &album.title, + &album.artists_name(), + &details.label, + album.release_date.as_ref().unwrap(), + details.total_tracks, + &details.copyright_text, + ); + + if let Some(art) = album.art.clone() { + let widget = self.widget.downgrade(); + + self.worker.send_local_task(async move { + let pixbuf = ImageLoader::new() + .load_remote(&art[..], "jpg", 320, 320) + .await; + if let (Some(widget), Some(ref pixbuf)) = (widget.upgrade(), pixbuf) { + widget.set_artwork(pixbuf); + widget.set_loaded(); + } + }); + } else { + self.widget.set_loaded(); + } + } + } +} + +impl Component for Details { + fn get_root_widget(&self) -> >k::Widget { + self.widget.upcast_ref() + } + + fn get_children(&mut self) -> Option<&mut Vec>> { + Some(&mut self.children) + } +} + +impl EventListener for Details { + fn on_event(&mut self, event: &AppEvent) { + match event { + AppEvent::BrowserEvent(BrowserEvent::AlbumDetailsLoaded(id)) + if id == &self.model.id => + { + self.update_details(); + self.update_playing(true); + } + AppEvent::BrowserEvent(BrowserEvent::AlbumSaved(id)) + | AppEvent::BrowserEvent(BrowserEvent::AlbumUnsaved(id)) + if id == &self.model.id => + { + self.update_liked(); + } + AppEvent::PlaybackEvent(PlaybackEvent::PlaybackPaused) => { + self.update_playing(false); + } + AppEvent::PlaybackEvent(PlaybackEvent::PlaybackResumed) => { + self.update_playing(true); + } + _ => {} + } + self.broadcast_event(event); + } +} diff --git a/src/app/components/details/details_model.rs b/src/app/components/details/details_model.rs new file mode 100644 index 0000000..ec6bb57 --- /dev/null +++ b/src/app/components/details/details_model.rs @@ -0,0 +1,272 @@ +use gio::prelude::*; +use gio::SimpleActionGroup; +use std::cell::Ref; +use std::ops::Deref; +use std::rc::Rc; + +use crate::api::SpotifyApiError; +use crate::app::components::labels; +use crate::app::components::HeaderBarModel; +use crate::app::components::PlaylistModel; +use crate::app::components::SimpleHeaderBarModel; +use crate::app::components::SimpleHeaderBarModelWrapper; +use crate::app::dispatch::ActionDispatcher; +use crate::app::models::*; +use crate::app::state::SelectionContext; +use crate::app::state::{BrowserAction, PlaybackAction, SelectionAction, SelectionState}; +use crate::app::{AppAction, AppEvent, AppModel, AppState, BatchQuery, SongsSource}; + +pub struct DetailsModel { + pub id: String, + app_model: Rc, + dispatcher: Box, +} + +impl DetailsModel { + pub fn new(id: String, app_model: Rc, dispatcher: Box) -> Self { + Self { + id, + app_model, + dispatcher, + } + } + + fn state(&self) -> Ref<'_, AppState> { + self.app_model.get_state() + } + + pub fn get_album_info(&self) -> Option + '_> { + self.app_model + .map_state_opt(|s| s.browser.details_state(&self.id)?.content.as_ref()) + } + + pub fn get_album_description(&self) -> Option + '_> { + self.app_model.map_state_opt(|s| { + Some( + &s.browser + .details_state(&self.id)? + .content + .as_ref()? + .description, + ) + }) + } + + pub fn load_album_info(&self) { + let id = self.id.clone(); + let api = self.app_model.get_spotify(); + self.dispatcher + .call_spotify_and_dispatch(move || async move { + let album = api.get_album(&id).await; + match album { + Ok(album) => Ok(BrowserAction::SetAlbumDetails(Box::new(album)).into()), + Err(SpotifyApiError::BadStatus(400, _)) + | Err(SpotifyApiError::BadStatus(404, _)) => { + Ok(BrowserAction::NavigationPop.into()) + } + Err(e) => Err(e), + } + }); + } + + pub fn view_artist(&self) { + if let Some(album) = self.get_album_description() { + let artist = &album.artists.first().unwrap().id; + self.dispatcher + .dispatch(AppAction::ViewArtist(artist.to_owned())); + } + } + + pub fn toggle_save_album(&self) { + if let Some(album) = self.get_album_description() { + let id = album.id.clone(); + let is_liked = album.is_liked; + + let api = self.app_model.get_spotify(); + + self.dispatcher + .call_spotify_and_dispatch(move || async move { + if !is_liked { + api.save_album(&id) + .await + .map(|album| BrowserAction::SaveAlbum(Box::new(album)).into()) + } else { + api.remove_saved_album(&id) + .await + .map(|_| BrowserAction::UnsaveAlbum(id).into()) + } + }); + } + } + + pub fn is_playing(&self) -> bool { + self.state().playback.is_playing() + } + + pub fn album_is_playing(&self) -> bool { + matches!( + self.app_model.get_state().playback.current_source(), + Some(SongsSource::Album(ref id)) if id == &self.id) + } + + pub fn toggle_play_album(&self) { + if let Some(album) = self.get_album_description() { + if !self.album_is_playing() { + if self.state().playback.is_shuffled() { + self.dispatcher + .dispatch(AppAction::PlaybackAction(PlaybackAction::ToggleShuffle)); + } + let id_of_first_song = album.songs.songs[0].id.as_str(); + self.play_song_at(0, id_of_first_song); + return; + } + if self.state().playback.is_playing() { + self.dispatcher + .dispatch(AppAction::PlaybackAction(PlaybackAction::Pause)); + } else { + self.dispatcher + .dispatch(AppAction::PlaybackAction(PlaybackAction::Play)); + } + } + } + + pub fn load_more(&self) -> Option<()> { + let last_batch = self.song_list_model().last_batch()?; + let query = BatchQuery { + source: SongsSource::Album(self.id.clone()), + batch: last_batch, + }; + + let id = self.id.clone(); + let next_query = query.next()?; + let loader = self.app_model.get_batch_loader(); + + self.dispatcher.dispatch_async(Box::pin(async move { + loader + .query(next_query, |_s, song_batch| { + BrowserAction::AppendAlbumTracks(id, Box::new(song_batch)).into() + }) + .await + })); + + Some(()) + } + + pub fn to_headerbar_model(self: &Rc) -> Rc { + Rc::new(SimpleHeaderBarModelWrapper::new( + self.clone(), + self.app_model.clone(), + self.dispatcher.box_clone(), + )) + } +} + +impl PlaylistModel for DetailsModel { + fn song_list_model(&self) -> SongListModel { + self.app_model + .get_state() + .browser + .details_state(&self.id) + .expect("illegal attempt to read details_state") + .songs + .clone() + } + + fn is_paused(&self) -> bool { + !self.app_model.get_state().playback.is_playing() + } + + fn show_song_covers(&self) -> bool { + false + } + + fn select_song(&self, id: &str) { + let songs = self.song_list_model(); + if let Some(song) = songs.get(id) { + self.dispatcher + .dispatch(SelectionAction::Select(vec![song.description().clone()]).into()); + } + } + + fn deselect_song(&self, id: &str) { + self.dispatcher + .dispatch(SelectionAction::Deselect(vec![id.to_string()]).into()); + } + + fn enable_selection(&self) -> bool { + self.dispatcher + .dispatch(AppAction::EnableSelection(SelectionContext::Default)); + true + } + + fn selection(&self) -> Option + '_>> { + Some(Box::new(self.app_model.map_state(|s| &s.selection))) + } + + fn current_song_id(&self) -> Option { + self.state().playback.current_song_id() + } + + fn play_song_at(&self, pos: usize, id: &str) { + let source = SongsSource::Album(self.id.clone()); + let batch = self.song_list_model().song_batch_for(pos); + if let Some(batch) = batch { + self.dispatcher + .dispatch(PlaybackAction::LoadPagedSongs(source, batch).into()); + self.dispatcher + .dispatch(PlaybackAction::Load(id.to_string()).into()); + } + } + + fn actions_for(&self, id: &str) -> Option { + let song = self.song_list_model().get(id)?; + let song = song.description(); + + let group = SimpleActionGroup::new(); + + for view_artist in song.make_artist_actions(self.dispatcher.box_clone(), None) { + group.add_action(&view_artist); + } + group.add_action(&song.make_link_action(None)); + group.add_action(&song.make_queue_action(self.dispatcher.box_clone(), None)); + + Some(group.upcast()) + } + + fn menu_for(&self, id: &str) -> Option { + let song = self.song_list_model().get(id)?; + let song = song.description(); + + let menu = gio::Menu::new(); + for artist in song.artists.iter() { + menu.append( + Some(&labels::more_from_label(&artist.name)), + Some(&format!("song.view_artist_{}", artist.id)), + ); + } + + menu.append(Some(&*labels::COPY_LINK), Some("song.copy_link")); + menu.append(Some(&*labels::ADD_TO_QUEUE), Some("song.queue")); + Some(menu.upcast()) + } +} + +impl SimpleHeaderBarModel for DetailsModel { + fn title(&self) -> Option { + None + } + + fn title_updated(&self, _: &AppEvent) -> bool { + false + } + + fn selection_context(&self) -> Option { + Some(SelectionContext::Default) + } + + fn select_all(&self) { + let songs: Vec = self.song_list_model().collect(); + self.dispatcher + .dispatch(SelectionAction::Select(songs).into()); + } +} diff --git a/src/app/components/details/mod.rs b/src/app/components/details/mod.rs new file mode 100644 index 0000000..00ced26 --- /dev/null +++ b/src/app/components/details/mod.rs @@ -0,0 +1,8 @@ +mod album_header; +#[allow(clippy::module_inception)] +mod details; +mod details_model; +mod release_details; + +pub use details::Details; +pub use details_model::DetailsModel; diff --git a/src/app/components/details/release_details.blp b/src/app/components/details/release_details.blp new file mode 100644 index 0000000..b5b2994 --- /dev/null +++ b/src/app/components/details/release_details.blp @@ -0,0 +1,82 @@ +using Gtk 4.0; +using Adw 1; + +template $ReleaseDetailsWindow : Adw.Window { + modal: true; + hide-on-close: true; + default-width: 360; + + Box { + orientation: vertical; + + Adw.HeaderBar { + show-end-title-buttons: true; + + [title] + Adw.WindowTitle album_artist { + } + + styles [ + "flat", + ] + } + + ListBox { + margin-start: 6; + margin-end: 6; + margin-top: 6; + margin-bottom: 6; + valign: start; + selection-mode: none; + show-separators: true; + overflow: hidden; + + styles [ + "card", + ] + + Adw.ActionRow { + /* Translators: This refers to a music label */ + + title: _("Label"); + + [suffix] + Label label { + label: "Label"; + } + } + + Adw.ActionRow { + /* Translators: This refers to a release date */ + + title: _("Released"); + + [suffix] + Label release { + label: "Released"; + } + } + + Adw.ActionRow { + /* Translators: This refers to a number of tracks */ + + title: _("Tracks"); + + [suffix] + Label tracks { + label: "Tracks"; + } + } + + Adw.ActionRow { + title: _("Copyright"); + + [suffix] + Label copyright { + label: "Copyright"; + ellipsize: middle; + } + } + } + } +} diff --git a/src/app/components/details/release_details.rs b/src/app/components/details/release_details.rs new file mode 100644 index 0000000..7ecbada --- /dev/null +++ b/src/app/components/details/release_details.rs @@ -0,0 +1,92 @@ +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; +use libadwaita::subclass::prelude::*; + +use crate::app::components::labels; + +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/release_details.ui")] + pub struct ReleaseDetailsWindow { + #[template_child] + pub album_artist: TemplateChild, + + #[template_child] + pub label: TemplateChild, + + #[template_child] + pub release: TemplateChild, + + #[template_child] + pub tracks: TemplateChild, + + #[template_child] + pub copyright: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for ReleaseDetailsWindow { + const NAME: &'static str = "ReleaseDetailsWindow"; + type Type = super::ReleaseDetailsWindow; + type ParentType = libadwaita::Window; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for ReleaseDetailsWindow { + fn constructed(&self) { + self.parent_constructed(); + } + } + + impl WidgetImpl for ReleaseDetailsWindow {} + impl AdwWindowImpl for ReleaseDetailsWindow {} + impl WindowImpl for ReleaseDetailsWindow {} +} + +glib::wrapper! { + pub struct ReleaseDetailsWindow(ObjectSubclass) @extends gtk::Widget, libadwaita::Window, libadwaita::PreferencesWindow; +} + +impl Default for ReleaseDetailsWindow { + fn default() -> Self { + Self::new() + } +} + +impl ReleaseDetailsWindow { + pub fn new() -> Self { + glib::Object::new() + } + + #[allow(clippy::too_many_arguments)] + pub fn set_details( + &self, + album: &str, + artist: &str, + label: &str, + release_date: &str, + track_count: usize, + copyright: &str, + ) { + let widget = self.imp(); + + widget + .album_artist + .set_title(&labels::album_by_artist_label(album, artist)); + + widget.label.set_text(label); + widget.release.set_text(release_date); + widget.tracks.set_text(&track_count.to_string()); + widget.copyright.set_text(copyright); + } +} diff --git a/src/app/components/device_selector/component.rs b/src/app/components/device_selector/component.rs new file mode 100644 index 0000000..3321c92 --- /dev/null +++ b/src/app/components/device_selector/component.rs @@ -0,0 +1,100 @@ +use std::ops::Deref; +use std::rc::Rc; + +use glib::Cast; + +use crate::app::components::{Component, EventListener}; +use crate::app::models::ConnectDevice; +use crate::app::state::{Device, LoginEvent, PlaybackAction, PlaybackEvent}; +use crate::app::{ActionDispatcher, AppEvent, AppModel}; + +use super::widget::DeviceSelectorWidget; + +pub struct DeviceSelectorModel { + app_model: Rc, + dispatcher: Box, +} + +impl DeviceSelectorModel { + pub fn new(app_model: Rc, dispatcher: Box) -> Self { + Self { + app_model, + dispatcher, + } + } + + pub fn refresh_available_devices(&self) { + let api = self.app_model.get_spotify(); + + self.dispatcher + .call_spotify_and_dispatch(move || async move { + api.list_available_devices() + .await + .map(|devices| PlaybackAction::SetAvailableDevices(devices).into()) + }); + } + + pub fn get_available_devices(&self) -> impl Deref> + '_ { + self.app_model.map_state(|s| s.playback.available_devices()) + } + + pub fn get_current_device(&self) -> impl Deref + '_ { + self.app_model.map_state(|s| s.playback.current_device()) + } + + pub fn set_current_device(&self, id: Option) { + let devices = self.get_available_devices(); + let connect_device = id + .and_then(|id| devices.iter().find(|&d| d.id == id)) + .cloned(); + let device = connect_device.map(Device::Connect).unwrap_or(Device::Local); + self.dispatcher + .dispatch(PlaybackAction::SwitchDevice(device).into()); + } +} + +pub struct DeviceSelector { + widget: DeviceSelectorWidget, + model: Rc, +} + +impl DeviceSelector { + pub fn new(widget: DeviceSelectorWidget, model: DeviceSelectorModel) -> Self { + let model = Rc::new(model); + + widget.connect_refresh(clone!(@weak model => move || { + model.refresh_available_devices(); + })); + + widget.connect_switch_device(clone!(@weak model => move |id| { + model.set_current_device(id); + })); + + Self { widget, model } + } +} + +impl Component for DeviceSelector { + fn get_root_widget(&self) -> >k::Widget { + self.widget.upcast_ref() + } +} + +impl EventListener for DeviceSelector { + fn on_event(&mut self, event: &AppEvent) { + match event { + AppEvent::LoginEvent(LoginEvent::LoginCompleted(_)) => { + self.model.refresh_available_devices(); + } + AppEvent::PlaybackEvent(PlaybackEvent::AvailableDevicesChanged) => { + self.widget + .update_devices_list(&self.model.get_available_devices()); + } + AppEvent::PlaybackEvent(PlaybackEvent::SwitchedDevice(_)) => { + self.widget + .set_current_device(&self.model.get_current_device()); + } + _ => (), + } + } +} diff --git a/src/app/components/device_selector/device_selector.blp b/src/app/components/device_selector/device_selector.blp new file mode 100644 index 0000000..d220772 --- /dev/null +++ b/src/app/components/device_selector/device_selector.blp @@ -0,0 +1,44 @@ +using Gtk 4.0; +using Adw 1; + +template $DeviceSelectorWidget : Button { + Adw.ButtonContent button_content { + halign: center; + hexpand: false; + icon-name: "audio-x-generic-symbolic"; + label: _("This device"); + } +} + +menu menu { + section { + label: _("Playing on"); + + item { + custom: "custom_content"; + } + } + + section { + item { + label: _("Refresh devices"); + action: "devices.refresh"; + } + } +} + +PopoverMenu popover { +} + +Box custom_content { + orientation: vertical; + + CheckButton this_device_button { + label: _("This device"); + sensitive: false; + } + + Box devices { + orientation: vertical; + } +} diff --git a/src/app/components/device_selector/mod.rs b/src/app/components/device_selector/mod.rs new file mode 100644 index 0000000..84b3256 --- /dev/null +++ b/src/app/components/device_selector/mod.rs @@ -0,0 +1,11 @@ +use glib::StaticType; + +mod component; +pub use component::*; + +mod widget; +pub use widget::*; + +pub fn expose_widgets() { + widget::DeviceSelectorWidget::static_type(); +} diff --git a/src/app/components/device_selector/widget.rs b/src/app/components/device_selector/widget.rs new file mode 100644 index 0000000..8867f72 --- /dev/null +++ b/src/app/components/device_selector/widget.rs @@ -0,0 +1,169 @@ +use crate::app::models::{ConnectDevice, ConnectDeviceKind}; +use crate::app::state::Device; +use gettextrs::gettext; +use gio::{Action, SimpleAction, SimpleActionGroup}; +use glib::FromVariant; +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; + +const ACTIONS: &str = "devices"; +const CONNECT_ACTION: &str = "connect"; +const REFRESH_ACTION: &str = "refresh"; + +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/device_selector.ui")] + pub struct DeviceSelectorWidget { + #[template_child] + pub button_content: TemplateChild, + + #[template_child] + pub popover: TemplateChild, + + #[template_child] + pub custom_content: TemplateChild, + + #[template_child] + pub devices: TemplateChild, + + #[template_child] + pub this_device_button: TemplateChild, + + #[template_child] + pub menu: TemplateChild, + + pub action_group: SimpleActionGroup, + } + + #[glib::object_subclass] + impl ObjectSubclass for DeviceSelectorWidget { + const NAME: &'static str = "DeviceSelectorWidget"; + type Type = super::DeviceSelectorWidget; + type ParentType = gtk::Button; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for DeviceSelectorWidget { + fn constructed(&self) { + self.parent_constructed(); + + let popover: >k::PopoverMenu = &self.popover; + popover.set_menu_model(Some(&*self.menu)); + popover.add_child(&*self.custom_content, "custom_content"); + popover.set_parent(&*self.obj()); + popover.set_autohide(true); + + let this_device = &*self.this_device_button; + this_device.set_action_name(Some(&format!("{}.{}", ACTIONS, CONNECT_ACTION))); + this_device.set_action_target_value(Some(&Option::::None.to_variant())); + + self.obj() + .insert_action_group(ACTIONS, Some(&self.action_group)); + self.obj() + .connect_clicked(clone!(@weak popover => move |_| { + popover.set_visible(true); + popover.present(); + popover.grab_focus(); + })); + } + } + + impl WidgetImpl for DeviceSelectorWidget {} + impl ButtonImpl for DeviceSelectorWidget {} +} + +glib::wrapper! { + pub struct DeviceSelectorWidget(ObjectSubclass) @extends gtk::Widget, gtk::Button; +} + +impl DeviceSelectorWidget { + fn action(&self, name: &str) -> Option { + self.imp().action_group.lookup_action(name) + } + + pub fn connect_refresh(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().action_group.add_action(&{ + let refresh = SimpleAction::new(REFRESH_ACTION, None); + refresh.connect_activate(move |_, _| f()); + refresh + }); + } + + pub fn connect_switch_device(&self, f: F) + where + F: Fn(Option) + 'static, + { + self.imp().action_group.add_action(&{ + let connect = SimpleAction::new_stateful( + CONNECT_ACTION, + Some(Option::::static_variant_type().as_ref()), + Option::::None.to_variant(), + ); + connect.connect_activate(move |action, device_id| { + if let Some(device_id) = device_id { + action.change_state(device_id); + f(Option::::from_variant(device_id).unwrap()); + } + }); + connect + }); + } + + pub fn set_current_device(&self, device: &Device) { + if let Some(action) = self.action(CONNECT_ACTION) { + let device_id = match device { + Device::Local => None, + Device::Connect(connect) => Some(&connect.id), + }; + action.change_state(&device_id.to_variant()); + } + let label = match device { + Device::Local => gettext("This device"), + Device::Connect(connect) => connect.label.clone(), + }; + let icon = match device { + Device::Local => "audio-x-generic-symbolic", + Device::Connect(connect) => match connect.kind { + ConnectDeviceKind::Phone => "phone-symbolic", + ConnectDeviceKind::Computer => "computer-symbolic", + ConnectDeviceKind::Speaker => "audio-speakers-symbolic", + ConnectDeviceKind::Other => "audio-x-generic-symbolic", + }, + }; + self.imp().button_content.set_label(&label); + self.imp().button_content.set_icon_name(icon); + } + + pub fn update_devices_list(&self, devices: &[ConnectDevice]) { + let widget = self.imp(); + widget.this_device_button.set_sensitive(!devices.is_empty()); + + while let Some(child) = widget.devices.upcast_ref::().first_child() { + widget.devices.remove(&child); + } + + for device in devices { + let check = gtk::CheckButton::builder() + .action_name(format!("{}.{}", ACTIONS, CONNECT_ACTION)) + .action_target(&Some(&device.id).to_variant()) + .group(&*widget.this_device_button) + .label(&device.label) + .build(); + widget.devices.append(&check); + } + } +} diff --git a/src/app/components/headerbar/component.rs b/src/app/components/headerbar/component.rs new file mode 100644 index 0000000..ed17fd6 --- /dev/null +++ b/src/app/components/headerbar/component.rs @@ -0,0 +1,296 @@ +use std::rc::Rc; + +use glib::Cast; +use gtk::prelude::*; + +use crate::app::{ + components::{Component, EventListener, ListenerComponent}, + state::{SelectionContext, SelectionEvent}, + ActionDispatcher, AppAction, AppEvent, AppModel, BrowserAction, BrowserEvent, +}; + +use super::widget::HeaderBarWidget; + +pub trait HeaderBarModel { + fn title(&self) -> Option; + fn title_updated(&self, event: &AppEvent) -> bool; + fn go_back(&self); + fn can_go_back(&self) -> bool; + fn selection_context(&self) -> Option; + fn can_select_all(&self) -> bool; + fn start_selection(&self); + fn select_all(&self); + fn cancel_selection(&self); + fn selected_count(&self) -> usize; +} + +pub struct DefaultHeaderBarModel { + title: Option, + selection_context: Option, + app_model: Rc, + dispatcher: Box, +} + +impl DefaultHeaderBarModel { + pub fn new( + title: Option, + selection_context: Option, + app_model: Rc, + dispatcher: Box, + ) -> Self { + Self { + title, + selection_context, + app_model, + dispatcher, + } + } +} + +impl HeaderBarModel for DefaultHeaderBarModel { + fn title(&self) -> Option { + self.title.clone() + } + + fn title_updated(&self, _: &AppEvent) -> bool { + false + } + + fn go_back(&self) { + self.dispatcher + .dispatch(BrowserAction::NavigationPop.into()) + } + + fn can_go_back(&self) -> bool { + self.app_model.get_state().browser.can_pop() + } + + fn selection_context(&self) -> Option { + self.selection_context.clone() + } + + fn can_select_all(&self) -> bool { + false + } + + fn start_selection(&self) { + if let Some(context) = self.selection_context.as_ref() { + self.dispatcher + .dispatch(AppAction::EnableSelection(context.clone())) + } + } + + fn select_all(&self) {} + + fn cancel_selection(&self) { + self.dispatcher.dispatch(AppAction::CancelSelection) + } + + fn selected_count(&self) -> usize { + self.app_model.get_state().selection.count() + } +} + +pub trait SimpleHeaderBarModel { + fn title(&self) -> Option; + fn title_updated(&self, event: &AppEvent) -> bool; + fn selection_context(&self) -> Option; + fn select_all(&self); +} + +pub struct SimpleHeaderBarModelWrapper { + wrapped_model: Rc, + app_model: Rc, + dispatcher: Box, +} + +impl SimpleHeaderBarModelWrapper { + pub fn new( + wrapped_model: Rc, + app_model: Rc, + dispatcher: Box, + ) -> Self { + Self { + wrapped_model, + app_model, + dispatcher, + } + } +} + +impl HeaderBarModel for SimpleHeaderBarModelWrapper +where + M: SimpleHeaderBarModel + 'static, +{ + fn title(&self) -> Option { + self.wrapped_model.title() + } + + fn title_updated(&self, event: &AppEvent) -> bool { + self.wrapped_model.title_updated(event) + } + + fn go_back(&self) { + self.dispatcher + .dispatch(BrowserAction::NavigationPop.into()) + } + + fn can_go_back(&self) -> bool { + self.app_model.get_state().browser.can_pop() + } + + fn selection_context(&self) -> Option { + self.wrapped_model.selection_context() + } + + fn can_select_all(&self) -> bool { + true + } + + fn start_selection(&self) { + if let Some(context) = self.wrapped_model.selection_context() { + self.dispatcher + .dispatch(AppAction::EnableSelection(context)); + } + } + + fn select_all(&self) { + self.wrapped_model.select_all() + } + + fn cancel_selection(&self) { + self.dispatcher.dispatch(AppAction::CancelSelection) + } + + fn selected_count(&self) -> usize { + self.app_model.get_state().selection.count() + } +} + +mod common { + + use super::*; + + pub fn update_for_event(event: &AppEvent, widget: &HeaderBarWidget, model: &Rc) + where + Model: HeaderBarModel + 'static, + { + match event { + AppEvent::SelectionEvent(SelectionEvent::SelectionModeChanged(active)) => { + widget.set_selection_active(*active); + } + AppEvent::SelectionEvent(SelectionEvent::SelectionChanged) => { + widget.set_selection_count(model.selected_count()); + } + AppEvent::BrowserEvent(BrowserEvent::NavigationPushed(_)) + | AppEvent::BrowserEvent(BrowserEvent::NavigationPoppedTo(_)) + | AppEvent::BrowserEvent(BrowserEvent::NavigationPopped) + | AppEvent::BrowserEvent(BrowserEvent::NavigationHidden(_)) => { + model.cancel_selection(); + widget.set_can_go_back(model.can_go_back()); + } + event if model.title_updated(event) => { + widget.set_title(model.title().as_ref().map(|s| &s[..])); + } + _ => {} + } + } + + pub fn bind_headerbar(widget: &HeaderBarWidget, model: &Rc) + where + Model: HeaderBarModel + 'static, + { + widget.connect_selection_start(clone!(@weak model => move || model.start_selection())); + widget.connect_select_all(clone!(@weak model => move || model.select_all())); + widget.connect_selection_cancel(clone!(@weak model => move || model.cancel_selection())); + widget.connect_go_back(clone!(@weak model => move || model.go_back())); + + widget.set_title(model.title().as_ref().map(|s| &s[..])); + widget.set_selection_possible(model.selection_context().is_some()); + widget.set_select_all_possible(model.can_select_all()); + widget.set_can_go_back(model.can_go_back()); + } +} + +pub struct HeaderBarComponent { + widget: HeaderBarWidget, + model: Rc, +} + +impl HeaderBarComponent +where + Model: HeaderBarModel + 'static, +{ + pub fn new(widget: HeaderBarWidget, model: Rc) -> Self { + common::bind_headerbar(&widget, &model); + Self { widget, model } + } +} + +impl EventListener for HeaderBarComponent +where + Model: HeaderBarModel + 'static, +{ + fn on_event(&mut self, event: &AppEvent) { + common::update_for_event(event, &self.widget, &self.model); + } +} + +// wrapper version ("Screen") +pub struct StandardScreen { + root: gtk::Widget, + widget: HeaderBarWidget, + model: Rc, + children: Vec>, +} + +impl StandardScreen +where + Model: HeaderBarModel + 'static, +{ + pub fn new( + wrapped: impl ListenerComponent + 'static, + leaflet: &libadwaita::Leaflet, + model: Rc, + ) -> Self { + let widget = HeaderBarWidget::new(); + common::bind_headerbar(&widget, &model); + widget.bind_to_leaflet(leaflet); + + let root = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .build(); + root.append(&widget); + root.append(wrapped.get_root_widget()); + + Self { + root: root.upcast(), + widget, + model, + children: vec![Box::new(wrapped)], + } + } +} + +impl Component for StandardScreen +where + Model: HeaderBarModel + 'static, +{ + fn get_root_widget(&self) -> >k::Widget { + &self.root + } + + fn get_children(&mut self) -> Option<&mut Vec>> { + Some(&mut self.children) + } +} + +impl EventListener for StandardScreen +where + Model: HeaderBarModel + 'static, +{ + fn on_event(&mut self, event: &AppEvent) { + common::update_for_event(event, &self.widget, &self.model); + self.broadcast_event(event); + } +} diff --git a/src/app/components/headerbar/headerbar.blp b/src/app/components/headerbar/headerbar.blp new file mode 100644 index 0000000..ae28dc0 --- /dev/null +++ b/src/app/components/headerbar/headerbar.blp @@ -0,0 +1,67 @@ +using Gtk 4.0; +using Adw 1; + +template $HeaderBarWidget : Adw.Bin { + [root] + Overlay overlay { + hexpand: true; + + Adw.HeaderBar main_header { + show-end-title-buttons: true; + + Button go_back { + receives-default: true; + halign: start; + valign: center; + icon-name: "go-previous-symbolic"; + has-frame: false; + } + + [title] + Adw.WindowTitle title { + visible: true; + title: "Spot"; + } + + [end] + Button start_selection { + icon-name: "object-select-symbolic"; + } + } + + [overlay] + Adw.HeaderBar selection_header { + show-end-title-buttons: false; + show-start-title-buttons: false; + visible: false; + + styles [ + "selection-mode", + ] + + Button cancel { + receives-default: true; + halign: start; + valign: center; + + /* Translators: Button label. Exits selection mode. */ + + label: _("Cancel"); + } + + [title] + Adw.WindowTitle selection_title { + title: ""; + } + + [end] + Button select_all { + valign: center; + + /* Translators: Button label. Selects all visible songs. */ + + label: _("Select all"); + } + } + } +} diff --git a/src/app/components/headerbar/mod.rs b/src/app/components/headerbar/mod.rs new file mode 100644 index 0000000..dc91647 --- /dev/null +++ b/src/app/components/headerbar/mod.rs @@ -0,0 +1,11 @@ +mod widget; +pub use widget::*; + +mod component; +pub use component::*; + +use glib::prelude::*; + +pub fn expose_widgets() { + widget::HeaderBarWidget::static_type(); +} diff --git a/src/app/components/headerbar/widget.rs b/src/app/components/headerbar/widget.rs new file mode 100644 index 0000000..d44d84f --- /dev/null +++ b/src/app/components/headerbar/widget.rs @@ -0,0 +1,189 @@ +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; +use libadwaita::subclass::prelude::BinImpl; + +use crate::app::components::labels; + +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/headerbar.ui")] + pub struct HeaderBarWidget { + #[template_child] + pub main_header: TemplateChild, + + #[template_child] + pub selection_header: TemplateChild, + + #[template_child] + pub go_back: TemplateChild, + + #[template_child] + pub title: TemplateChild, + + #[template_child] + pub selection_title: TemplateChild, + + #[template_child] + pub start_selection: TemplateChild, + + #[template_child] + pub select_all: TemplateChild, + + #[template_child] + pub cancel: TemplateChild, + + #[template_child] + pub overlay: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for HeaderBarWidget { + const NAME: &'static str = "HeaderBarWidget"; + type Type = super::HeaderBarWidget; + type ParentType = libadwaita::Bin; + type Interfaces = (gtk::Buildable,); + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for HeaderBarWidget {} + + impl BuildableImpl for HeaderBarWidget { + fn add_child(&self, builder: >k::Builder, child: &glib::Object, type_: Option<&str>) { + if Some("root") == type_ { + self.parent_add_child(builder, child, type_); + } else { + self.main_header + .set_title_widget(child.downcast_ref::()); + } + } + } + + impl WidgetImpl for HeaderBarWidget {} + impl BinImpl for HeaderBarWidget {} + impl WindowImpl for HeaderBarWidget {} +} + +glib::wrapper! { + pub struct HeaderBarWidget(ObjectSubclass) @extends gtk::Widget, libadwaita::Bin; +} + +impl Default for HeaderBarWidget { + fn default() -> Self { + Self::new() + } +} + +impl HeaderBarWidget { + pub fn new() -> Self { + glib::Object::new() + } + + pub fn connect_selection_start(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().start_selection.connect_clicked(move |_| f()); + } + + pub fn connect_select_all(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().select_all.connect_clicked(move |_| f()); + } + + pub fn connect_selection_cancel(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().cancel.connect_clicked(move |_| f()); + } + + pub fn connect_go_back(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().go_back.connect_clicked(move |_| f()); + } + + pub fn bind_to_leaflet(&self, leaflet: &libadwaita::Leaflet) { + leaflet + .bind_property( + "folded", + &*self.imp().main_header, + "show-start-title-buttons", + ) + .build(); + leaflet.notify("folded"); + } + + pub fn set_can_go_back(&self, can_go_back: bool) { + self.imp().go_back.set_visible(can_go_back); + } + + pub fn set_selection_possible(&self, possible: bool) { + self.imp().start_selection.set_visible(possible); + } + + pub fn set_select_all_possible(&self, possible: bool) { + self.imp().select_all.set_visible(possible); + } + + pub fn set_selection_active(&self, active: bool) { + if active { + self.imp() + .selection_title + .set_title(&labels::n_songs_selected_label(0)); + self.imp().selection_title.set_visible(true); + self.imp().selection_header.set_visible(true); + } else { + self.imp().selection_title.set_visible(false); + self.imp().selection_header.set_visible(false); + } + } + + pub fn set_selection_count(&self, count: usize) { + self.imp() + .selection_title + .set_title(&labels::n_songs_selected_label(count)); + } + + pub fn add_classes(&self, classes: &[&str]) { + for &class in classes { + self.imp().main_header.add_css_class(class); + } + } + + pub fn remove_classes(&self, classes: &[&str]) { + for &class in classes { + self.imp().main_header.remove_css_class(class); + } + } + + pub fn set_title_visible(&self, visible: bool) { + self.imp().title.set_visible(visible); + } + + pub fn set_title_and_subtitle(&self, title: &str, subtitle: &str) { + self.imp().title.set_title(title); + self.imp().title.set_subtitle(subtitle); + } + + pub fn set_title(&self, title: Option<&str>) { + self.imp().title.set_visible(title.is_some()); + if let Some(title) = title { + self.imp().title.set_title(title); + } + } +} diff --git a/src/app/components/labels.rs b/src/app/components/labels.rs new file mode 100644 index 0000000..3bb216f --- /dev/null +++ b/src/app/components/labels.rs @@ -0,0 +1,55 @@ +use gettextrs::*; + +lazy_static! { + // translators: This is part of a contextual menu attached to a single track; this entry allows viewing the album containing a specific track. + pub static ref VIEW_ALBUM: String = gettext("View album"); + + // translators: This is part of a contextual menu attached to a single track; the intent is to copy the link (public URL) to a specific track. + pub static ref COPY_LINK: String = gettext("Copy link"); + + // translators: This is part of a contextual menu attached to a single track; this entry adds a track at the end of the play queue. + pub static ref ADD_TO_QUEUE: String = gettext("Add to queue"); + + // translators: This is part of a contextual menu attached to a single track; this entry removes a track from the play queue. + pub static ref REMOVE_FROM_QUEUE: String = gettext("Remove from queue"); +} + +pub fn add_to_playlist_label(playlist: &str) -> String { + // this is just to fool xgettext, it doesn't like macros (or rust for that matter) :( + if cfg!(debug_assertions) { + // translators: This is part of a larger text that says "Add to ". This text should be as short as possible. + gettext("Add to {}"); + } + gettext!("Add to {}", playlist) +} + +pub fn n_songs_selected_label(n: usize) -> String { + // this is just to fool xgettext, it doesn't like macros (or rust for that matter) :( + if cfg!(debug_assertions) { + // translators: This shows up when in selection mode. This text should be as short as possible. + ngettext("{} song selected", "{} songs selected", n as u32); + } + ngettext!("{} song selected", "{} songs selected", n as u32, n) +} + +pub fn more_from_label(artist: &str) -> String { + // this is just to fool xgettext, it doesn't like macros (or rust for that matter) :( + if cfg!(debug_assertions) { + // translators: This is part of a contextual menu attached to a single track; the full text is "More from ". + gettext("More from {}"); + } + gettext!("More from {}", glib::markup_escape_text(artist)) +} + +pub fn album_by_artist_label(album: &str, artist: &str) -> String { + // this is just to fool xgettext, it doesn't like macros (or rust for that matter) :( + if cfg!(debug_assertions) { + // translators: This is part of a larger label that reads " by " + gettext("{} by {}"); + } + gettext!( + "{} by {}", + glib::markup_escape_text(album), + glib::markup_escape_text(artist) + ) +} diff --git a/src/app/components/library/library.blp b/src/app/components/library/library.blp new file mode 100644 index 0000000..5e3d2a7 --- /dev/null +++ b/src/app/components/library/library.blp @@ -0,0 +1,35 @@ +using Gtk 4.0; +using Adw 1; + +template $LibraryWidget : Box { + ScrolledWindow scrolled_window { + hexpand: true; + vexpand: true; + vscrollbar-policy: always; + min-content-width: 250; + Overlay overlay { + FlowBox flowbox { + margin-start: 6; + margin-end: 6; + margin-top: 6; + margin-bottom: 6; + min-children-per-line: 1; + selection-mode: none; + activate-on-single-click: false; + } + + [overlay] + Adw.StatusPage status_page { + /* Translators: A title that is shown when the user has not saved any albums. */ + + title: _("You have no saved albums."); + + /* Translators: A description of what happens when the user has saved albums. */ + + description: _("Your library will be shown here."); + icon-name: "emblem-music-symbolic"; + visible: true; + } + } + } +} diff --git a/src/app/components/library/library.rs b/src/app/components/library/library.rs new file mode 100644 index 0000000..d7adbf3 --- /dev/null +++ b/src/app/components/library/library.rs @@ -0,0 +1,158 @@ +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; +use std::rc::Rc; + +use super::LibraryModel; +use crate::app::components::utils::wrap_flowbox_item; +use crate::app::components::{AlbumWidget, Component, EventListener}; +use crate::app::dispatch::Worker; +use crate::app::models::AlbumModel; +use crate::app::state::LoginEvent; +use crate::app::{AppEvent, BrowserEvent, ListStore}; + +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/library.ui")] + pub struct LibraryWidget { + #[template_child] + pub scrolled_window: TemplateChild, + + #[template_child] + pub flowbox: TemplateChild, + + #[template_child] + pub status_page: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for LibraryWidget { + const NAME: &'static str = "LibraryWidget"; + type Type = super::LibraryWidget; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for LibraryWidget {} + impl WidgetImpl for LibraryWidget {} + impl BoxImpl for LibraryWidget {} +} + +glib::wrapper! { + pub struct LibraryWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; +} + +impl Default for LibraryWidget { + fn default() -> Self { + Self::new() + } +} + +impl LibraryWidget { + pub fn new() -> Self { + glib::Object::new() + } + + fn connect_bottom_edge(&self, f: F) + where + F: Fn() + 'static, + { + self.imp() + .scrolled_window + .connect_edge_reached(move |_, pos| { + if let gtk::PositionType::Bottom = pos { + f() + } + }); + } + + fn bind_albums(&self, worker: Worker, store: &ListStore, on_album_pressed: F) + where + F: Fn(String) + Clone + 'static, + { + self.imp() + .flowbox + .bind_model(Some(store.unsafe_store()), move |item| { + wrap_flowbox_item(item, |album_model| { + let f = on_album_pressed.clone(); + let album = AlbumWidget::for_model(album_model, worker.clone()); + album.connect_album_pressed(clone!(@weak album_model => move |_| { + f(album_model.uri()); + })); + album + }) + }); + } + + pub fn status_page(&self) -> &libadwaita::StatusPage { + &self.imp().status_page + } +} + +pub struct Library { + widget: LibraryWidget, + worker: Worker, + model: Rc, +} + +impl Library { + pub fn new(worker: Worker, model: LibraryModel) -> Self { + let model = Rc::new(model); + let widget = LibraryWidget::new(); + widget.connect_bottom_edge(clone!(@weak model => move || { + model.load_more_albums(); + })); + + Self { + widget, + worker, + model, + } + } + + fn bind_flowbox(&self) { + self.widget.bind_albums( + self.worker.clone(), + &self.model.get_list_store().unwrap(), + clone!(@weak self.model as model => move |id| { + model.open_album(id); + }), + ); + } +} + +impl EventListener for Library { + fn on_event(&mut self, event: &AppEvent) { + match event { + AppEvent::Started => { + let _ = self.model.refresh_saved_albums(); + self.bind_flowbox(); + } + AppEvent::LoginEvent(LoginEvent::LoginCompleted(_)) => { + let _ = self.model.refresh_saved_albums(); + } + AppEvent::BrowserEvent(BrowserEvent::LibraryUpdated) => { + self.widget + .status_page() + .set_visible(!self.model.has_albums()); + } + _ => {} + } + } +} + +impl Component for Library { + fn get_root_widget(&self) -> >k::Widget { + self.widget.as_ref() + } +} diff --git a/src/app/components/library/library_model.rs b/src/app/components/library/library_model.rs new file mode 100644 index 0000000..de1fda8 --- /dev/null +++ b/src/app/components/library/library_model.rs @@ -0,0 +1,70 @@ +use std::cell::Ref; +use std::ops::Deref; +use std::rc::Rc; + +use crate::app::models::*; +use crate::app::state::HomeState; +use crate::app::{ActionDispatcher, AppAction, AppModel, BrowserAction, ListStore}; + +pub struct LibraryModel { + app_model: Rc, + dispatcher: Box, +} + +impl LibraryModel { + pub fn new(app_model: Rc, dispatcher: Box) -> Self { + Self { + app_model, + dispatcher, + } + } + + fn state(&self) -> Option> { + self.app_model.map_state_opt(|s| s.browser.home_state()) + } + + pub fn get_list_store(&self) -> Option> + '_> { + Some(Ref::map(self.state()?, |s| &s.albums)) + } + + pub fn refresh_saved_albums(&self) -> Option<()> { + let api = self.app_model.get_spotify(); + let batch_size = self.state()?.next_albums_page.batch_size; + + self.dispatcher + .call_spotify_and_dispatch(move || async move { + api.get_saved_albums(0, batch_size) + .await + .map(|albums| BrowserAction::SetLibraryContent(albums).into()) + }); + + Some(()) + } + + pub fn has_albums(&self) -> bool { + self.get_list_store() + .map(|list| list.len() > 0) + .unwrap_or(false) + } + + pub fn load_more_albums(&self) -> Option<()> { + let api = self.app_model.get_spotify(); + + let next_page = &self.state()?.next_albums_page; + let batch_size = next_page.batch_size; + let offset = next_page.next_offset?; + + self.dispatcher + .call_spotify_and_dispatch(move || async move { + api.get_saved_albums(offset, batch_size) + .await + .map(|albums| BrowserAction::AppendLibraryContent(albums).into()) + }); + + Some(()) + } + + pub fn open_album(&self, album_id: String) { + self.dispatcher.dispatch(AppAction::ViewAlbum(album_id)); + } +} diff --git a/src/app/components/library/mod.rs b/src/app/components/library/mod.rs new file mode 100644 index 0000000..ad8bb31 --- /dev/null +++ b/src/app/components/library/mod.rs @@ -0,0 +1,6 @@ +#[allow(clippy::module_inception)] +mod library; +mod library_model; + +pub use library::*; +pub use library_model::*; diff --git a/src/app/components/login/login.blp b/src/app/components/login/login.blp new file mode 100644 index 0000000..c4a5a9a --- /dev/null +++ b/src/app/components/login/login.blp @@ -0,0 +1,108 @@ +using Gtk 4.0; +using Adw 1; + + +template $LoginWindow : Adw.Window { + default-width: 360; + default-height: 100; + + Box { + hexpand: true; + margin-bottom: 24; + orientation: vertical; + + Adw.HeaderBar { + [title] + Label {} + + styles ["flat"] + } + + WindowHandle { + + Adw.Clamp { + maximum-size: 360; + tightening-threshold: 280; + + Box { + hexpand: true; + orientation: vertical; + margin-start: 16; + margin-end: 16; + spacing: 24; + + Image { + icon-name: "dev.alextren.Spot"; + pixel-size: 128; + margin-bottom: 20; + } + + Box{ + hexpand: true; + orientation: vertical; + spacing: 4; + + Label { + label: _("Welcome to Spot"); + halign: center; + styles ["title-1"] + } + + Label { + /* Translators: Login window title, must mention Premium (a premium account is required). */ + label: _("Log in with your Spotify Account. A Spotify Premium subscription is required to use the app."); + wrap: true; + wrap-mode: word; + halign: center; + justify: center; + styles ["body"] + } + } + + ListBox { + styles ["boxed-list"] + + Adw.EntryRow username { + /* Translators: Placeholder for the username field */ + title: _("Username or Email"); + } + + Adw.PasswordEntryRow password { + /* Translators: Placeholder for the password field */ + title: _("Password"); + } + } + + Revealer auth_error_container { + vexpand: true; + transition-type: slide_up; + + Label { + /* Translators: This error is shown when authentication fails. */ + label: _("Incorrect login credentials."); + halign: center; + justify: center; + wrap: true; + wrap-mode: word; + styles ["error"] + } + } + + Button login_button { + /* Translators: Log in button label */ + label: _("Log in"); + halign: center; + styles ["pill", "suggested-action"] + } + + Button login_with_spotify_button { + /* Translators: Log in button label */ + label: _("Log in with Spotify"); + halign: center; + styles ["pill", "suggested-action"] + } + } + } + } + } +} diff --git a/src/app/components/login/login.rs b/src/app/components/login/login.rs new file mode 100644 index 0000000..95901c8 --- /dev/null +++ b/src/app/components/login/login.rs @@ -0,0 +1,247 @@ +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; +use std::rc::Rc; + +use crate::app::components::EventListener; +use crate::app::credentials::Credentials; +use crate::app::state::{LoginCompletedEvent, LoginEvent}; +use crate::app::AppEvent; + +use super::LoginModel; +mod imp { + + use libadwaita::subclass::prelude::AdwWindowImpl; + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/login.ui")] + pub struct LoginWindow { + #[template_child] + pub username: TemplateChild, + + #[template_child] + pub password: TemplateChild, + + #[template_child] + pub login_button: TemplateChild, + + #[template_child] + pub login_with_spotify_button: TemplateChild, + + #[template_child] + pub auth_error_container: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for LoginWindow { + const NAME: &'static str = "LoginWindow"; + type Type = super::LoginWindow; + type ParentType = libadwaita::Window; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for LoginWindow {} + impl WidgetImpl for LoginWindow {} + impl AdwWindowImpl for LoginWindow {} + impl WindowImpl for LoginWindow {} +} + +glib::wrapper! { + pub struct LoginWindow(ObjectSubclass) @extends gtk::Widget, libadwaita::Window; +} + +impl Default for LoginWindow { + fn default() -> Self { + Self::new() + } +} + +impl LoginWindow { + pub fn new() -> Self { + glib::Object::new() + } + + fn connect_close(&self, on_close: F) + where + F: Fn() + 'static, + { + let window = self.upcast_ref::(); + window.connect_close_request(move |_| { + on_close(); + gtk::Inhibit(false) + }); + } + + fn connect_submit(&self, on_submit: SubmitFn) + where + SubmitFn: Fn(&str, &str) + Clone + 'static, + { + let on_submit_clone = on_submit.clone(); + let controller = gtk::EventControllerKey::new(); + controller.set_propagation_phase(gtk::PropagationPhase::Capture); + controller.connect_key_pressed( + clone!(@weak self as _self => @default-return gtk::Inhibit(false), move |_, key, _, _| { + if key == gdk::Key::Return { + _self.submit(&on_submit_clone); + gtk::Inhibit(true) + } else { + gtk::Inhibit(false) + } + }), + ); + self.add_controller(controller); + + self.imp() + .login_button + .connect_clicked(clone!(@weak self as _self => move |_| { + _self.submit(&on_submit); + })); + } + + fn connect_login_oauth_spotify(&self, on_login_with_spotify_button: F) + where + F: Fn() + 'static, + { + self.imp().login_with_spotify_button.connect_clicked( + clone!(@weak self as _self => move |_| { + _self.login_with_spotify(&on_login_with_spotify_button); + }), + ); + } + + fn show_auth_error(&self, shown: bool) { + let error_class = "error"; + let widget = self.imp(); + if shown { + widget.username.add_css_class(error_class); + widget.password.add_css_class(error_class); + } else { + widget.username.remove_css_class(error_class); + widget.password.remove_css_class(error_class); + } + widget.auth_error_container.set_reveal_child(shown); + } + + fn submit(&self, on_submit: &SubmitFn) + where + SubmitFn: Fn(&str, &str), + { + let widget = self.imp(); + + self.show_auth_error(false); + + let username_text = widget.username.text(); + let password_text = widget.password.text(); + + if username_text.is_empty() { + widget.username.grab_focus(); + } else if password_text.is_empty() { + widget.password.grab_focus(); + } else { + on_submit(username_text.as_str(), password_text.as_str()); + } + } + + fn login_with_spotify(&self, on_login_with_spotify: &F) + where + F: Fn(), + { + self.show_auth_error(false); + on_login_with_spotify() + } +} + +pub struct Login { + parent: gtk::Window, + login_window: LoginWindow, + model: Rc, +} + +impl Login { + pub fn new(parent: gtk::Window, model: LoginModel) -> Self { + let model = Rc::new(model); + + let login_window = LoginWindow::new(); + + login_window.connect_close(clone!(@weak parent => move || { + if let Some(app) = parent.application().as_ref() { + app.quit(); + } + })); + + login_window.connect_submit(clone!(@weak model => move |username, password| { + model.login(username.to_string(), password.to_string()); + })); + + login_window.connect_login_oauth_spotify(clone!(@weak model => move || { + model.login_with_spotify(); + })); + + Self { + parent, + login_window, + model, + } + } + + fn window(&self) -> &libadwaita::Window { + self.login_window.upcast_ref::() + } + + fn show_self(&self) { + self.window().set_transient_for(Some(&self.parent)); + self.window().set_modal(true); + self.window().set_visible(true); + } + + fn hide_and_save_creds(&self, credentials: Credentials) { + self.window().set_visible(false); + self.model.save_for_autologin(credentials); + } + + fn reveal_error(&self) { + self.login_window.show_auth_error(true); + } +} + +impl EventListener for Login { + fn on_event(&mut self, event: &AppEvent) { + info!("received login event {:?}", event); + match event { + AppEvent::LoginEvent(LoginEvent::LoginCompleted(LoginCompletedEvent::Password( + creds, + ))) => { + self.hide_and_save_creds(creds.clone()); + } + AppEvent::LoginEvent(LoginEvent::LoginCompleted(LoginCompletedEvent::Token(token))) => { + self.hide_and_save_creds(token.clone()); + } + AppEvent::LoginEvent(LoginEvent::LoginFailed) => { + self.model.clear_saved_credentials(); + self.reveal_error(); + } + AppEvent::Started => { + self.model.try_autologin(); + } + AppEvent::LoginEvent(LoginEvent::LogoutCompleted | LoginEvent::LoginShown) => { + self.show_self(); + } + AppEvent::LoginEvent(LoginEvent::RefreshTokenCompleted { + token, + token_expiry_time, + }) => { + self.model.save_token(token.clone(), *token_expiry_time); + } + _ => {} + } + } +} diff --git a/src/app/components/login/login_model.rs b/src/app/components/login/login_model.rs new file mode 100644 index 0000000..2567541 --- /dev/null +++ b/src/app/components/login/login_model.rs @@ -0,0 +1,82 @@ +use std::time::SystemTime; + +use gettextrs::*; + +use crate::app::credentials::Credentials; +use crate::app::state::{LoginAction, TryLoginAction}; +use crate::app::{ActionDispatcher, AppAction, Worker}; + +pub struct LoginModel { + dispatcher: Box, + worker: Worker, +} + +impl LoginModel { + pub fn new(dispatcher: Box, worker: Worker) -> Self { + Self { dispatcher, worker } + } + + pub fn try_autologin(&self) { + self.dispatcher.dispatch_async(Box::pin(async { + let action = match Credentials::retrieve().await { + Ok(creds) => LoginAction::TryLogin(if creds.token_expired() { + TryLoginAction::Password { + username: creds.username, + password: creds.password, + } + } else { + TryLoginAction::Token { + username: creds.username, + token: creds.token, + } + }), + Err(err) => { + warn!("Could not retrieve credentials: {}", err); + LoginAction::ShowLogin + } + }; + Some(action.into()) + })); + } + + pub fn clear_saved_credentials(&self) { + self.worker.send_task(async { + let _ = Credentials::logout().await; + }); + } + + pub fn save_token(&self, token: String, token_expiry_time: SystemTime) { + self.worker.send_task(async move { + if let Ok(mut credentials) = Credentials::retrieve().await { + credentials.token = token; + credentials.token_expiry_time = Some(token_expiry_time); + if let Err(err) = credentials.save().await { + warn!("Could not save credentials: {}", err); + } + } + }); + } + + pub fn save_for_autologin(&self, credentials: Credentials) { + self.dispatcher.dispatch_async(Box::pin(async move { + let Err(err) = credentials.save().await else { + return None; + }; + warn!("Could not save credentials: {}", err); + Some(AppAction::ShowNotification(gettext( + // translators: This notification shows up right after login if the password could not be stored in the keyring (that is, GNOME's keyring aka seahorse, or any other libsecret compliant secret store). + "Could not save password. Make sure the session keyring is unlocked.", + ))) + })); + } + + pub fn login(&self, username: String, password: String) { + self.dispatcher + .dispatch(LoginAction::TryLogin(TryLoginAction::Password { username, password }).into()) + } + + pub fn login_with_spotify(&self) { + self.dispatcher + .dispatch(LoginAction::TryLogin(TryLoginAction::OAuthSpotify {}).into()) + } +} diff --git a/src/app/components/login/mod.rs b/src/app/components/login/mod.rs new file mode 100644 index 0000000..66a4bb2 --- /dev/null +++ b/src/app/components/login/mod.rs @@ -0,0 +1,6 @@ +#[allow(clippy::module_inception)] +mod login; +mod login_model; + +pub use login::*; +pub use login_model::*; diff --git a/src/app/components/mod.rs b/src/app/components/mod.rs new file mode 100644 index 0000000..175341c --- /dev/null +++ b/src/app/components/mod.rs @@ -0,0 +1,183 @@ +#[macro_export] +macro_rules! resource { + ($resource:expr) => { + concat!("/dev/alextren/Spot", $resource) + }; +} + +use gettextrs::*; +use std::cell::RefCell; +use std::collections::HashSet; +use std::future::Future; + +use crate::api::SpotifyApiError; +use crate::app::{state::LoginAction, ActionDispatcher, AppAction, AppEvent}; + +mod navigation; +pub use navigation::*; + +mod playback; +pub use playback::*; + +mod playlist; +pub use playlist::*; + +mod login; +pub use login::*; + +mod settings; +pub use settings::*; + +mod player_notifier; +pub use player_notifier::PlayerNotifier; + +mod library; +pub use library::*; + +mod details; +pub use details::*; + +mod search; +pub use search::*; + +mod album; +use album::*; + +mod artist; +use artist::*; + +mod artist_details; +pub use artist_details::*; + +mod user_details; +pub use user_details::*; + +mod now_playing; +pub use now_playing::*; + +mod device_selector; +pub use device_selector::*; + +mod saved_tracks; +pub use saved_tracks::*; + +mod user_menu; +pub use user_menu::*; + +mod notification; +pub use notification::*; + +mod saved_playlists; +pub use saved_playlists::*; + +mod playlist_details; +pub use playlist_details::*; + +mod window; +pub use window::*; + +mod selection; +pub use selection::*; + +mod headerbar; +pub use headerbar::*; + +mod scrolling_header; +pub use scrolling_header::*; + +pub mod utils; + +pub mod labels; + +pub mod sidebar; + +// without this the builder doesn't seen to know about the custom widgets +pub fn expose_custom_widgets() { + playback::expose_widgets(); + selection::expose_widgets(); + headerbar::expose_widgets(); + device_selector::expose_widgets(); + playlist_details::expose_widgets(); + scrolling_header::expose_widgets(); +} + +impl dyn ActionDispatcher { + fn call_spotify_and_dispatch(&self, call: C) + where + C: 'static + Send + Clone + FnOnce() -> F, + F: Send + Future>, + { + self.call_spotify_and_dispatch_many(move || async { call().await.map(|a| vec![a]) }) + } + + fn call_spotify_and_dispatch_many(&self, call: C) + where + C: 'static + Send + Clone + FnOnce() -> F, + F: Send + Future, SpotifyApiError>>, + { + self.dispatch_many_async(Box::pin(async move { + let first_call = call.clone(); + let result = first_call().await; + match result { + Ok(actions) => actions, + Err(SpotifyApiError::NoToken) => vec![], + Err(SpotifyApiError::InvalidToken) => { + let mut retried = call().await.unwrap_or_else(|_| Vec::new()); + retried.insert(0, LoginAction::RefreshToken.into()); + retried + } + Err(err) => { + error!("Spotify API error: {}", err); + vec![AppAction::ShowNotification(gettext( + // translators: This notification is the default message for unhandled errors. Logs refer to console output. + "An error occured. Check logs for details!", + ))] + } + } + })) + } +} + +thread_local!(static CSS_ADDED: RefCell> = RefCell::new(HashSet::new())); + +pub fn display_add_css_provider(resource: &'static str) { + CSS_ADDED.with(|set| { + if set.borrow().contains(resource) { + return; + } + + set.borrow_mut().insert(resource); + + let provider = gtk::CssProvider::new(); + provider.load_from_resource(resource); + + gtk::style_context_add_provider_for_display( + &gdk::Display::default().unwrap(), + &provider, + gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); + }); +} + +pub trait EventListener { + fn on_event(&mut self, _: &AppEvent) {} +} + +pub trait Component { + fn get_root_widget(&self) -> >k::Widget; + + fn get_children(&mut self) -> Option<&mut Vec>> { + None + } + + fn broadcast_event(&mut self, event: &AppEvent) { + if let Some(children) = self.get_children() { + for child in children.iter_mut() { + child.on_event(event); + } + } + } +} + +pub trait ListenerComponent: Component + EventListener {} +impl ListenerComponent for T where T: Component + EventListener {} diff --git a/src/app/components/navigation/factory.rs b/src/app/components/navigation/factory.rs new file mode 100644 index 0000000..c1196f0 --- /dev/null +++ b/src/app/components/navigation/factory.rs @@ -0,0 +1,149 @@ +use std::rc::Rc; + +use crate::app::components::sidebar::{Sidebar, SidebarModel}; +use crate::app::components::*; +use crate::app::state::SelectionContext; +use crate::app::{ActionDispatcher, AppModel, Worker}; + +pub struct ScreenFactory { + app_model: Rc, + dispatcher: Box, + worker: Worker, + leaflet: libadwaita::Leaflet, +} + +impl ScreenFactory { + pub fn new( + app_model: Rc, + dispatcher: Box, + worker: Worker, + leaflet: libadwaita::Leaflet, + ) -> Self { + Self { + app_model, + dispatcher, + worker, + leaflet, + } + } + + pub fn make_library(&self) -> impl ListenerComponent { + let model = LibraryModel::new(Rc::clone(&self.app_model), self.dispatcher.box_clone()); + let screen_model = DefaultHeaderBarModel::new( + Some(gettext("Library")), + None, + Rc::clone(&self.app_model), + self.dispatcher.box_clone(), + ); + StandardScreen::new( + Library::new(self.worker.clone(), model), + &self.leaflet, + Rc::new(screen_model), + ) + } + + pub fn make_sidebar(&self, listbox: gtk::ListBox) -> impl ListenerComponent { + let model = SidebarModel::new(Rc::clone(&self.app_model), self.dispatcher.box_clone()); + Sidebar::new(listbox, Rc::new(model)) + } + + pub fn make_saved_playlists(&self) -> impl ListenerComponent { + let model = + SavedPlaylistsModel::new(Rc::clone(&self.app_model), self.dispatcher.box_clone()); + let screen_model = DefaultHeaderBarModel::new( + Some(gettext("Playlists")), + None, + Rc::clone(&self.app_model), + self.dispatcher.box_clone(), + ); + StandardScreen::new( + SavedPlaylists::new(self.worker.clone(), model), + &self.leaflet, + Rc::new(screen_model), + ) + } + + pub fn make_now_playing(&self) -> impl ListenerComponent { + let model = Rc::new(NowPlayingModel::new( + Rc::clone(&self.app_model), + self.dispatcher.box_clone(), + )); + NowPlaying::new(model, self.worker.clone(), &self.leaflet) + } + + pub fn make_saved_tracks(&self) -> impl ListenerComponent { + let screen_model = DefaultHeaderBarModel::new( + Some(gettext("Saved tracks")), + Some(SelectionContext::SavedTracks), + Rc::clone(&self.app_model), + self.dispatcher.box_clone(), + ); + let model = Rc::new(SavedTracksModel::new( + Rc::clone(&self.app_model), + self.dispatcher.box_clone(), + )); + StandardScreen::new( + SavedTracks::new(model, self.worker.clone()), + &self.leaflet, + Rc::new(screen_model), + ) + } + + pub fn make_album_details(&self, id: String) -> impl ListenerComponent { + let model = Rc::new(DetailsModel::new( + id, + Rc::clone(&self.app_model), + self.dispatcher.box_clone(), + )); + Details::new(model, self.worker.clone(), &self.leaflet) + } + + pub fn make_search_results(&self) -> impl ListenerComponent { + let model = + SearchResultsModel::new(Rc::clone(&self.app_model), self.dispatcher.box_clone()); + SearchResults::new(model, self.worker.clone(), &self.leaflet) + } + + pub fn make_artist_details(&self, id: String) -> impl ListenerComponent { + let model = Rc::new(ArtistDetailsModel::new( + id, + Rc::clone(&self.app_model), + self.dispatcher.box_clone(), + )); + let screen_model = SimpleHeaderBarModelWrapper::new( + Rc::clone(&model), + Rc::clone(&self.app_model), + self.dispatcher.box_clone(), + ); + StandardScreen::new( + ArtistDetails::new(model, self.worker.clone()), + &self.leaflet, + Rc::new(screen_model), + ) + } + + pub fn make_playlist_details(&self, id: String) -> impl ListenerComponent { + let model = Rc::new(PlaylistDetailsModel::new( + id, + Rc::clone(&self.app_model), + self.dispatcher.box_clone(), + )); + PlaylistDetails::new(model, self.worker.clone()) + } + + pub fn make_user_details(&self, id: String) -> impl ListenerComponent { + let screen_model = DefaultHeaderBarModel::new( + None, + None, + Rc::clone(&self.app_model), + self.dispatcher.box_clone(), + ); + let model = + UserDetailsModel::new(id, Rc::clone(&self.app_model), self.dispatcher.box_clone()); + StandardScreen::new( + UserDetails::new(model, self.worker.clone()), + &self.leaflet, + Rc::new(screen_model), + ) + } +} diff --git a/src/app/components/navigation/home.rs b/src/app/components/navigation/home.rs new file mode 100644 index 0000000..1c2e62f --- /dev/null +++ b/src/app/components/navigation/home.rs @@ -0,0 +1,88 @@ +use gtk::prelude::*; + +use crate::app::components::sidebar::SidebarDestination; +use crate::app::components::{Component, EventListener, ScreenFactory}; +use crate::app::{AppEvent, BrowserEvent}; + +pub struct HomePane { + stack: gtk::Stack, + components: Vec>, +} + +impl HomePane { + pub fn new(listbox: gtk::ListBox, screen_factory: &ScreenFactory) -> Self { + let library = screen_factory.make_library(); + let saved_playlists = screen_factory.make_saved_playlists(); + let saved_tracks = screen_factory.make_saved_tracks(); + let now_playing = screen_factory.make_now_playing(); + let sidebar = screen_factory.make_sidebar(listbox); + + let stack = gtk::Stack::new(); + stack.set_transition_type(gtk::StackTransitionType::Crossfade); + + let dest = SidebarDestination::Library; + stack.add_titled( + library.get_root_widget(), + Option::from(dest.id()), + &dest.title(), + ); + + let dest = SidebarDestination::SavedTracks; + stack.add_titled( + saved_tracks.get_root_widget(), + Option::from(dest.id()), + &dest.title(), + ); + + let dest = SidebarDestination::SavedPlaylists; + stack.add_titled( + saved_playlists.get_root_widget(), + Option::from(dest.id()), + &dest.title(), + ); + + let dest = SidebarDestination::NowPlaying; + stack.add_titled( + now_playing.get_root_widget(), + Option::from(dest.id()), + &dest.title(), + ); + + Self { + stack, + components: vec![ + Box::new(sidebar), + Box::new(library), + Box::new(saved_playlists), + Box::new(saved_tracks), + Box::new(now_playing), + ], + } + } +} + +impl Component for HomePane { + fn get_root_widget(&self) -> >k::Widget { + self.stack.upcast_ref() + } + + fn get_children(&mut self) -> Option<&mut Vec>> { + Some(&mut self.components) + } +} + +impl EventListener for HomePane { + fn on_event(&mut self, event: &AppEvent) { + match event { + AppEvent::NowPlayingShown => { + self.stack + .set_visible_child_name(SidebarDestination::NowPlaying.id()); + } + AppEvent::BrowserEvent(BrowserEvent::HomeVisiblePageChanged(page)) => { + self.stack.set_visible_child_name(page); + } + _ => {} + } + self.broadcast_event(event); + } +} diff --git a/src/app/components/navigation/mod.rs b/src/app/components/navigation/mod.rs new file mode 100644 index 0000000..b6d9d54 --- /dev/null +++ b/src/app/components/navigation/mod.rs @@ -0,0 +1,11 @@ +#[allow(clippy::module_inception)] +mod navigation; +pub use navigation::*; + +mod navigation_model; +pub use navigation_model::*; + +mod home; + +mod factory; +pub use factory::*; diff --git a/src/app/components/navigation/navigation.rs b/src/app/components/navigation/navigation.rs new file mode 100644 index 0000000..deff472 --- /dev/null +++ b/src/app/components/navigation/navigation.rs @@ -0,0 +1,147 @@ +use gtk::traits::WidgetExt; +use libadwaita::NavigationDirection; +use std::rc::Rc; + +use crate::app::components::{EventListener, ListenerComponent}; +use crate::app::state::ScreenName; +use crate::app::{AppEvent, BrowserEvent}; + +use super::{factory::ScreenFactory, home::HomePane, NavigationModel}; + +pub struct Navigation { + model: Rc, + leaflet: libadwaita::Leaflet, + navigation_stack: gtk::Stack, + home_listbox: gtk::ListBox, + screen_factory: ScreenFactory, + children: Vec>, +} + +impl Navigation { + pub fn new( + model: NavigationModel, + leaflet: libadwaita::Leaflet, + navigation_stack: gtk::Stack, + home_listbox: gtk::ListBox, + screen_factory: ScreenFactory, + ) -> Self { + let model = Rc::new(model); + + leaflet.connect_folded_notify( + clone!(@weak model => move |leaflet| { + let is_main = leaflet.visible_child_name().map(|s| s.as_str() == "main").unwrap_or(false); + let folded = leaflet.is_folded(); + model.set_nav_hidden(folded && is_main); + }) + ); + + leaflet.connect_visible_child_name_notify( + clone!(@weak model => move |leaflet| { + let is_main = leaflet.visible_child_name().map(|s| s.as_str() == "main").unwrap_or(false); + let folded = leaflet.is_folded(); + model.set_nav_hidden(folded && is_main); + }) + ); + + Self { + model, + leaflet, + navigation_stack, + home_listbox, + screen_factory, + children: vec![], + } + } + + fn make_home(&self) -> Box { + Box::new(HomePane::new( + self.home_listbox.clone(), + &self.screen_factory, + )) + } + + fn show_navigation(&self) { + self.leaflet.navigate(NavigationDirection::Back); + } + + fn push_screen(&mut self, name: &ScreenName) { + let component: Box = match name { + ScreenName::Home => self.make_home(), + ScreenName::AlbumDetails(id) => { + Box::new(self.screen_factory.make_album_details(id.to_owned())) + } + ScreenName::Search => Box::new(self.screen_factory.make_search_results()), + ScreenName::Artist(id) => { + Box::new(self.screen_factory.make_artist_details(id.to_owned())) + } + ScreenName::PlaylistDetails(id) => { + Box::new(self.screen_factory.make_playlist_details(id.to_owned())) + } + ScreenName::User(id) => Box::new(self.screen_factory.make_user_details(id.to_owned())), + }; + + let widget = component.get_root_widget().clone(); + self.children.push(component); + + self.leaflet.navigate(NavigationDirection::Forward); + self.navigation_stack + .add_named(&widget, Some(name.identifier().as_ref())); + self.navigation_stack + .set_visible_child_name(name.identifier().as_ref()); + + glib::source::idle_add_local_once(move || { + widget.grab_focus(); + }); + } + + fn pop(&mut self) { + let children = &mut self.children; + let popped = children.pop(); + + let name = self.model.visible_child_name(); + self.navigation_stack + .set_visible_child_name(name.identifier().as_ref()); + + if let Some(child) = popped { + self.navigation_stack.remove(child.get_root_widget()); + } + } + + fn pop_to(&mut self, screen: &ScreenName) { + self.navigation_stack + .set_visible_child_name(screen.identifier().as_ref()); + let remainder = self.children.split_off(self.model.children_count()); + for widget in remainder { + self.navigation_stack.remove(widget.get_root_widget()); + } + } +} + +impl EventListener for Navigation { + fn on_event(&mut self, event: &AppEvent) { + match event { + AppEvent::Started => { + self.push_screen(&ScreenName::Home); + } + AppEvent::BrowserEvent(BrowserEvent::NavigationPushed(name)) => { + self.push_screen(name); + } + AppEvent::BrowserEvent(BrowserEvent::NavigationHidden(false)) => { + self.show_navigation(); + } + AppEvent::BrowserEvent(BrowserEvent::NavigationPopped) => { + self.pop(); + } + AppEvent::BrowserEvent(BrowserEvent::NavigationPoppedTo(name)) => { + self.pop_to(name); + } + AppEvent::BrowserEvent(BrowserEvent::HomeVisiblePageChanged(_)) => { + self.leaflet.navigate(NavigationDirection::Forward); + } + _ => {} + }; + for child in self.children.iter_mut() { + child.on_event(event); + } + } +} diff --git a/src/app/components/navigation/navigation_model.rs b/src/app/components/navigation/navigation_model.rs new file mode 100644 index 0000000..0051787 --- /dev/null +++ b/src/app/components/navigation/navigation_model.rs @@ -0,0 +1,31 @@ +use crate::app::state::ScreenName; +use crate::app::{ActionDispatcher, AppModel, BrowserAction}; +use std::ops::Deref; +use std::rc::Rc; + +pub struct NavigationModel { + app_model: Rc, + dispatcher: Box, +} + +impl NavigationModel { + pub fn new(app_model: Rc, dispatcher: Box) -> Self { + Self { + app_model, + dispatcher, + } + } + + pub fn visible_child_name(&self) -> impl Deref + '_ { + self.app_model.map_state(|s| s.browser.current_screen()) + } + + pub fn set_nav_hidden(&self, hidden: bool) { + self.dispatcher + .dispatch(BrowserAction::SetNavigationHidden(hidden).into()); + } + + pub fn children_count(&self) -> usize { + self.app_model.get_state().browser.count() + } +} diff --git a/src/app/components/notification/mod.rs b/src/app/components/notification/mod.rs new file mode 100644 index 0000000..a0f67bb --- /dev/null +++ b/src/app/components/notification/mod.rs @@ -0,0 +1,47 @@ +use crate::app::components::EventListener; +use crate::app::AppEvent; +use gettextrs::*; +use glib::ToVariant; + +pub struct Notification { + toast_overlay: libadwaita::ToastOverlay, +} + +impl Notification { + pub fn new(toast_overlay: libadwaita::ToastOverlay) -> Self { + Self { toast_overlay } + } + + fn show(&self, content: &str) { + let toast = libadwaita::Toast::builder() + .title(content) + .timeout(4) + .build(); + self.toast_overlay.add_toast(toast); + } + + fn show_playlist_created(&self, id: &str) { + // translators: This is a notification that pop ups when a new playlist is created. It includes the name of that playlist. + let message = gettext("New playlist created."); + // translators: This is a label in the notification shown after creating a new playlist. If it is clicked, the new playlist will be opened. + let label = gettext("View"); + let toast = libadwaita::Toast::builder() + .title(message) + .timeout(4) + .action_name("app.open_playlist") + .button_label(label) + .action_target(&id.to_variant()) + .build(); + self.toast_overlay.add_toast(toast); + } +} + +impl EventListener for Notification { + fn on_event(&mut self, event: &AppEvent) { + if let AppEvent::NotificationShown(content) = event { + self.show(content) + } else if let AppEvent::PlaylistCreatedNotificationShown(id) = event { + self.show_playlist_created(id) + } + } +} diff --git a/src/app/components/now_playing/mod.rs b/src/app/components/now_playing/mod.rs new file mode 100644 index 0000000..d4472f4 --- /dev/null +++ b/src/app/components/now_playing/mod.rs @@ -0,0 +1,6 @@ +#[allow(clippy::module_inception)] +mod now_playing; +pub use now_playing::*; + +mod now_playing_model; +pub use now_playing_model::*; diff --git a/src/app/components/now_playing/now_playing.blp b/src/app/components/now_playing/now_playing.blp new file mode 100644 index 0000000..30b4c38 --- /dev/null +++ b/src/app/components/now_playing/now_playing.blp @@ -0,0 +1,23 @@ +using Gtk 4.0; +using Adw 1; + +template $NowPlayingWidget : Box { + orientation: vertical; + vexpand: true; + hexpand: true; + + $HeaderBarWidget headerbar { + $DeviceSelectorWidget device_selector {} + } + + ScrolledWindow scrolled_window { + vexpand: true; + + Adw.ClampScrollable { + maximum-size: 900; + + ListView song_list { + } + } + } +} diff --git a/src/app/components/now_playing/now_playing.rs b/src/app/components/now_playing/now_playing.rs new file mode 100644 index 0000000..9980e1a --- /dev/null +++ b/src/app/components/now_playing/now_playing.rs @@ -0,0 +1,151 @@ +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; +use std::rc::Rc; + +use super::NowPlayingModel; +use crate::app::components::{ + Component, DeviceSelector, DeviceSelectorWidget, EventListener, HeaderBarComponent, + HeaderBarWidget, Playlist, +}; +use crate::app::state::PlaybackEvent; +use crate::app::{AppEvent, Worker}; + +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/now_playing.ui")] + pub struct NowPlayingWidget { + #[template_child] + pub song_list: TemplateChild, + + #[template_child] + pub headerbar: TemplateChild, + + #[template_child] + pub device_selector: TemplateChild, + + #[template_child] + pub scrolled_window: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for NowPlayingWidget { + const NAME: &'static str = "NowPlayingWidget"; + type Type = super::NowPlayingWidget; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for NowPlayingWidget { + fn constructed(&self) { + self.parent_constructed(); + } + } + + impl WidgetImpl for NowPlayingWidget {} + impl BoxImpl for NowPlayingWidget {} +} + +glib::wrapper! { + pub struct NowPlayingWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; +} + +impl NowPlayingWidget { + fn new() -> Self { + glib::Object::new() + } + + fn connect_bottom_edge(&self, f: F) + where + F: Fn() + 'static, + { + self.imp() + .scrolled_window + .connect_edge_reached(move |_, pos| { + if let gtk::PositionType::Bottom = pos { + f() + } + }); + } + + fn song_list_widget(&self) -> >k::ListView { + self.imp().song_list.as_ref() + } + + fn headerbar_widget(&self) -> &HeaderBarWidget { + self.imp().headerbar.as_ref() + } + + fn device_selector_widget(&self) -> &DeviceSelectorWidget { + self.imp().device_selector.as_ref() + } +} + +pub struct NowPlaying { + widget: NowPlayingWidget, + model: Rc, + children: Vec>, +} + +impl NowPlaying { + pub fn new(model: Rc, worker: Worker, leaflet: &libadwaita::Leaflet) -> Self { + let widget = NowPlayingWidget::new(); + + widget.connect_bottom_edge(clone!(@weak model => move || { + model.load_more(); + })); + + let playlist = Box::new(Playlist::new( + widget.song_list_widget().clone(), + model.clone(), + worker, + )); + + let headerbar_widget = widget.headerbar_widget(); + headerbar_widget.bind_to_leaflet(leaflet); + let headerbar = Box::new(HeaderBarComponent::new( + headerbar_widget.clone(), + model.to_headerbar_model(), + )); + + let device_selector = Box::new(DeviceSelector::new( + widget.device_selector_widget().clone(), + model.device_selector_model(), + )); + + Self { + widget, + model, + children: vec![playlist, headerbar, device_selector], + } + } +} + +impl Component for NowPlaying { + fn get_root_widget(&self) -> >k::Widget { + self.widget.upcast_ref() + } + + fn get_children(&mut self) -> Option<&mut Vec>> { + Some(&mut self.children) + } +} + +impl EventListener for NowPlaying { + fn on_event(&mut self, event: &AppEvent) { + if let AppEvent::PlaybackEvent(PlaybackEvent::TrackChanged(_)) = event { + self.model.load_more(); + } + self.broadcast_event(event); + } +} diff --git a/src/app/components/now_playing/now_playing_model.rs b/src/app/components/now_playing/now_playing_model.rs new file mode 100644 index 0000000..c6a0139 --- /dev/null +++ b/src/app/components/now_playing/now_playing_model.rs @@ -0,0 +1,174 @@ +use gio::prelude::*; +use gio::SimpleActionGroup; +use std::ops::Deref; +use std::rc::Rc; + +use crate::app::components::{ + labels, DeviceSelectorModel, HeaderBarModel, PlaylistModel, SimpleHeaderBarModel, + SimpleHeaderBarModelWrapper, +}; +use crate::app::models::{SongDescription, SongListModel}; +use crate::app::state::Device; +use crate::app::state::{ + PlaybackAction, PlaybackState, SelectionAction, SelectionContext, SelectionState, +}; +use crate::app::{ActionDispatcher, AppAction, AppEvent, AppModel}; + +pub struct NowPlayingModel { + app_model: Rc, + dispatcher: Box, +} + +impl NowPlayingModel { + pub fn new(app_model: Rc, dispatcher: Box) -> Self { + Self { + app_model, + dispatcher, + } + } + + fn queue(&self) -> impl Deref + '_ { + self.app_model.map_state(|s| &s.playback) + } + + pub fn load_more(&self) -> Option<()> { + let queue = self.queue(); + let loader = self.app_model.get_batch_loader(); + let query = queue.next_query()?; + debug!("next_query = {:?}", &query); + + self.dispatcher.dispatch_async(Box::pin(async move { + loader + .query(query, |source, song_batch| { + PlaybackAction::LoadPagedSongs(source, song_batch).into() + }) + .await + })); + + Some(()) + } + + pub fn to_headerbar_model(self: &Rc) -> Rc { + Rc::new(SimpleHeaderBarModelWrapper::new( + self.clone(), + self.app_model.clone(), + self.dispatcher.box_clone(), + )) + } + + pub fn device_selector_model(&self) -> DeviceSelectorModel { + DeviceSelectorModel::new(self.app_model.clone(), self.dispatcher.box_clone()) + } + + fn current_selection_context(&self) -> SelectionContext { + let state = self.app_model.get_state(); + match state.playback.current_device() { + Device::Local => SelectionContext::Queue, + Device::Connect(_) => SelectionContext::ReadOnlyQueue, + } + } +} + +impl PlaylistModel for NowPlayingModel { + fn song_list_model(&self) -> SongListModel { + self.queue().songs().clone() + } + + fn is_paused(&self) -> bool { + !self.app_model.get_state().playback.is_playing() + } + + fn current_song_id(&self) -> Option { + self.queue().current_song_id() + } + + fn play_song_at(&self, _pos: usize, id: &str) { + self.dispatcher + .dispatch(PlaybackAction::Load(id.to_string()).into()); + } + + fn autoscroll_to_playing(&self) -> bool { + false // too buggy for now + } + + fn actions_for(&self, id: &str) -> Option { + let queue = self.queue(); + let song = queue.songs().get(id)?; + let song = song.description(); + let group = SimpleActionGroup::new(); + + for view_artist in song.make_artist_actions(self.dispatcher.box_clone(), None) { + group.add_action(&view_artist); + } + group.add_action(&song.make_album_action(self.dispatcher.box_clone(), None)); + group.add_action(&song.make_link_action(None)); + group.add_action(&song.make_dequeue_action(self.dispatcher.box_clone(), None)); + + Some(group.upcast()) + } + + fn menu_for(&self, id: &str) -> Option { + let queue = self.queue(); + let song = queue.songs().get(id)?; + let song = song.description(); + + let menu = gio::Menu::new(); + menu.append(Some(&*labels::VIEW_ALBUM), Some("song.view_album")); + for artist in song.artists.iter() { + menu.append( + Some(&labels::more_from_label(&artist.name)), + Some(&format!("song.view_artist_{}", artist.id)), + ); + } + + menu.append(Some(&*labels::COPY_LINK), Some("song.copy_link")); + menu.append(Some(&*labels::REMOVE_FROM_QUEUE), Some("song.dequeue")); + + Some(menu.upcast()) + } + + fn select_song(&self, id: &str) { + let queue = self.queue(); + if let Some(song) = queue.songs().get(id) { + let song = song.description().clone(); + self.dispatcher + .dispatch(SelectionAction::Select(vec![song]).into()); + } + } + + fn deselect_song(&self, id: &str) { + self.dispatcher + .dispatch(SelectionAction::Deselect(vec![id.to_string()]).into()); + } + + fn enable_selection(&self) -> bool { + self.dispatcher + .dispatch(AppAction::EnableSelection(self.current_selection_context())); + true + } + + fn selection(&self) -> Option + '_>> { + let selection = self.app_model.map_state(|s| &s.selection); + Some(Box::new(selection)) + } +} + +impl SimpleHeaderBarModel for NowPlayingModel { + fn title(&self) -> Option { + None + } + + fn title_updated(&self, _: &AppEvent) -> bool { + false + } + + fn selection_context(&self) -> Option { + Some(self.current_selection_context()) + } + + fn select_all(&self) { + let songs: Vec = self.queue().songs().collect(); + self.dispatcher + .dispatch(SelectionAction::Select(songs).into()); + } +} diff --git a/src/app/components/playback/component.rs b/src/app/components/playback/component.rs new file mode 100644 index 0000000..dbae5b4 --- /dev/null +++ b/src/app/components/playback/component.rs @@ -0,0 +1,162 @@ +use std::ops::Deref; +use std::rc::Rc; + +use crate::app::components::EventListener; +use crate::app::models::*; +use crate::app::state::{PlaybackAction, PlaybackEvent, ScreenName, SelectionEvent}; +use crate::app::{ + ActionDispatcher, AppAction, AppEvent, AppModel, AppState, BrowserAction, Worker, +}; + +use super::playback_widget::PlaybackWidget; + +pub struct PlaybackModel { + app_model: Rc, + dispatcher: Box, +} + +impl PlaybackModel { + pub fn new(app_model: Rc, dispatcher: Box) -> Self { + Self { + app_model, + dispatcher, + } + } + + fn state(&self) -> impl Deref + '_ { + self.app_model.get_state() + } + + fn go_home(&self) { + self.dispatcher.dispatch(AppAction::ViewNowPlaying); + self.dispatcher + .dispatch(BrowserAction::NavigationPopTo(ScreenName::Home).into()); + } + + fn is_playing(&self) -> bool { + self.state().playback.is_playing() + } + + fn is_shuffled(&self) -> bool { + self.state().playback.is_shuffled() + } + + fn current_song(&self) -> Option { + self.app_model.get_state().playback.current_song() + } + + fn play_next_song(&self) { + self.dispatcher.dispatch(PlaybackAction::Next.into()); + } + + fn play_prev_song(&self) { + self.dispatcher.dispatch(PlaybackAction::Previous.into()); + } + + fn toggle_playback(&self) { + self.dispatcher.dispatch(PlaybackAction::TogglePlay.into()); + } + + fn toggle_shuffle(&self) { + self.dispatcher + .dispatch(PlaybackAction::ToggleShuffle.into()); + } + + fn toggle_repeat(&self) { + self.dispatcher + .dispatch(PlaybackAction::ToggleRepeat.into()); + } + + fn seek_to(&self, position: u32) { + self.dispatcher + .dispatch(PlaybackAction::Seek(position).into()); + } +} + +pub struct PlaybackControl { + model: Rc, + widget: PlaybackWidget, + worker: Worker, +} + +impl PlaybackControl { + pub fn new(model: PlaybackModel, widget: PlaybackWidget, worker: Worker) -> Self { + let model = Rc::new(model); + + widget.connect_play_pause(clone!(@weak model => move || model.toggle_playback() )); + widget.connect_next(clone!(@weak model => move || model.play_next_song())); + widget.connect_prev(clone!(@weak model => move || model.play_prev_song())); + widget.connect_shuffle(clone!(@weak model => move || model.toggle_shuffle())); + widget.connect_repeat(clone!(@weak model => move || model.toggle_repeat())); + widget.connect_seek(clone!(@weak model => move |position| model.seek_to(position))); + widget.connect_now_playing_clicked(clone!(@weak model => move || model.go_home())); + + Self { + model, + widget, + worker, + } + } + + fn update_repeat(&self, mode: &RepeatMode) { + self.widget.set_repeat_mode(*mode); + } + + fn update_shuffled(&self) { + self.widget.set_shuffled(self.model.is_shuffled()); + } + + fn update_playing(&self) { + let is_playing = self.model.is_playing(); + self.widget.set_playing(is_playing); + } + + fn update_current_info(&self) { + if let Some(song) = self.model.current_song() { + self.widget + .set_title_and_artist(&song.title, &song.artists_name()); + self.widget.set_song_duration(Some(song.duration as f64)); + if let Some(url) = song.art { + self.widget.set_artwork_from_url(url, &self.worker); + } + } else { + self.widget.reset_info(); + } + } + + fn sync_seek(&self, pos: u32) { + self.widget.set_seek_position(pos as f64); + } +} + +impl EventListener for PlaybackControl { + fn on_event(&mut self, event: &AppEvent) { + match event { + AppEvent::PlaybackEvent(PlaybackEvent::PlaybackPaused) + | AppEvent::PlaybackEvent(PlaybackEvent::PlaybackResumed) => { + self.update_playing(); + } + AppEvent::PlaybackEvent(PlaybackEvent::RepeatModeChanged(mode)) => { + self.update_repeat(mode); + } + AppEvent::PlaybackEvent(PlaybackEvent::ShuffleChanged(_)) => { + self.update_shuffled(); + } + AppEvent::PlaybackEvent(PlaybackEvent::TrackChanged(_)) => { + self.update_current_info(); + } + AppEvent::PlaybackEvent(PlaybackEvent::PlaybackStopped) => { + self.update_playing(); + self.update_current_info(); + } + AppEvent::PlaybackEvent(PlaybackEvent::SeekSynced(pos)) + | AppEvent::PlaybackEvent(PlaybackEvent::TrackSeeked(pos)) => { + self.sync_seek(*pos); + } + AppEvent::SelectionEvent(SelectionEvent::SelectionModeChanged(active)) => { + self.widget.set_seekbar_visible(!active); + } + _ => {} + } + } +} diff --git a/src/app/components/playback/mod.rs b/src/app/components/playback/mod.rs new file mode 100644 index 0000000..2800d3f --- /dev/null +++ b/src/app/components/playback/mod.rs @@ -0,0 +1,11 @@ +mod component; +mod playback_controls; +mod playback_info; +mod playback_widget; +pub use component::*; + +use glib::prelude::*; + +pub fn expose_widgets() { + playback_widget::PlaybackWidget::static_type(); +} diff --git a/src/app/components/playback/playback.css b/src/app/components/playback/playback.css new file mode 100644 index 0000000..3446a42 --- /dev/null +++ b/src/app/components/playback/playback.css @@ -0,0 +1,31 @@ +.seek-bar { + padding: 0; + padding-bottom: 2px; + min-height: 1px; +} + +.seek-bar trough, .seek-bar highlight { + border-radius: 0; + border-left: none; + border-right: none; + min-height: 1px; + transition: min-height 100ms ease; +} + +.seek-bar--active trough, .seek-bar--active highlight { + min-height: 5px; +} + +.seek-bar--active:hover trough, .seek-bar--active:hover highlight { + min-height: 10px; +} + +.seek-bar highlight { + border-left: none; + border-right: none; +} + +.playback-button { + min-width: 40px; + min-height: 40px; +} diff --git a/src/app/components/playback/playback_controls.blp b/src/app/components/playback/playback_controls.blp new file mode 100644 index 0000000..4b00b50 --- /dev/null +++ b/src/app/components/playback/playback_controls.blp @@ -0,0 +1,57 @@ +using Gtk 4.0; + +template $PlaybackControlsWidget : Box { + halign: center; + hexpand: true; + spacing: 8; + homogeneous: true; + + ToggleButton shuffle { + receives-default: true; + halign: center; + valign: center; + has-frame: false; + icon-name: "media-playlist-shuffle-symbolic"; + tooltip-text: _("Shuffle"); + } + + Button prev { + receives-default: true; + halign: center; + valign: center; + has-frame: false; + icon-name: "media-skip-backward-symbolic"; + tooltip-text: _("Previous"); + } + + Button play_pause { + receives-default: true; + halign: center; + valign: center; + icon-name: "media-playback-start-symbolic"; + tooltip-text: "Play/Pause"; + + styles [ + "circular", + "playback-button", + ] + } + + Button next { + receives-default: true; + halign: center; + valign: center; + has-frame: false; + icon-name: "media-skip-forward-symbolic"; + tooltip-text: _("Next"); + } + + Button repeat { + receives-default: true; + halign: center; + valign: center; + has-frame: false; + icon-name: "media-playlist-consecutive-symbolic"; + tooltip-text: _("Repeat"); + } +} diff --git a/src/app/components/playback/playback_controls.rs b/src/app/components/playback/playback_controls.rs new file mode 100644 index 0000000..d656a60 --- /dev/null +++ b/src/app/components/playback/playback_controls.rs @@ -0,0 +1,123 @@ +use gettextrs::gettext; +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::{glib, CompositeTemplate}; + +use crate::app::models::RepeatMode; + +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/playback_controls.ui")] + pub struct PlaybackControlsWidget { + #[template_child] + pub play_pause: TemplateChild, + + #[template_child] + pub next: TemplateChild, + + #[template_child] + pub prev: TemplateChild, + + #[template_child] + pub shuffle: TemplateChild, + + #[template_child] + pub repeat: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for PlaybackControlsWidget { + const NAME: &'static str = "PlaybackControlsWidget"; + type Type = super::PlaybackControlsWidget; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for PlaybackControlsWidget {} + impl WidgetImpl for PlaybackControlsWidget {} + impl BoxImpl for PlaybackControlsWidget {} +} + +glib::wrapper! { + pub struct PlaybackControlsWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; +} + +impl PlaybackControlsWidget { + pub fn set_playing(&self, is_playing: bool) { + let playback_icon = if is_playing { + "media-playback-pause-symbolic" + } else { + "media-playback-start-symbolic" + }; + + let translated_tooltip = if is_playing { + gettext("Pause") + } else { + gettext("Play") + }; + let tooltip_text = Some(translated_tooltip.as_str()); + + let playback_control = self.imp(); + playback_control.play_pause.set_icon_name(playback_icon); + playback_control.play_pause.set_tooltip_text(tooltip_text); + } + + pub fn set_shuffled(&self, shuffled: bool) { + self.imp().shuffle.set_active(shuffled); + } + + pub fn set_repeat_mode(&self, mode: RepeatMode) { + let repeat_mode_icon = match mode { + RepeatMode::Song => "media-playlist-repeat-song-symbolic", + RepeatMode::Playlist => "media-playlist-repeat-symbolic", + RepeatMode::None => "media-playlist-consecutive-symbolic", + }; + + self.imp().repeat.set_icon_name(repeat_mode_icon); + } + + pub fn connect_play_pause(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().play_pause.connect_clicked(move |_| f()); + } + + pub fn connect_prev(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().prev.connect_clicked(move |_| f()); + } + + pub fn connect_next(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().next.connect_clicked(move |_| f()); + } + + pub fn connect_shuffle(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().shuffle.connect_clicked(move |_| f()); + } + + pub fn connect_repeat(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().repeat.connect_clicked(move |_| f()); + } +} diff --git a/src/app/components/playback/playback_info.blp b/src/app/components/playback/playback_info.blp new file mode 100644 index 0000000..5c4445c --- /dev/null +++ b/src/app/components/playback/playback_info.blp @@ -0,0 +1,43 @@ +using Gtk 4.0; + +template $PlaybackInfoWidget : Button { + receives-default: true; + halign: start; + valign: center; + has-frame: false; + + layout { + column-span: "1"; + column: "0"; + row: "0"; + } + + Box { + halign: center; + + Image playing_image { + width-request: 40; + height-request: 40; + icon-name: "emblem-music-symbolic"; + } + + Label current_song_info { + visible: false; + halign: start; + hexpand: true; + margin-start: 12; + margin-end: 12; + + /* Translators: Short text displayed instead of a song title when nothing plays */ + + label: _("No song playing"); + use-markup: true; + ellipsize: middle; + lines: 1; + } + } + + styles [ + "body", + ] +} diff --git a/src/app/components/playback/playback_info.rs b/src/app/components/playback/playback_info.rs new file mode 100644 index 0000000..f18e881 --- /dev/null +++ b/src/app/components/playback/playback_info.rs @@ -0,0 +1,74 @@ +use gettextrs::gettext; +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::{glib, CompositeTemplate}; + +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/playback_info.ui")] + pub struct PlaybackInfoWidget { + #[template_child] + pub playing_image: TemplateChild, + + #[template_child] + pub current_song_info: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for PlaybackInfoWidget { + const NAME: &'static str = "PlaybackInfoWidget"; + type Type = super::PlaybackInfoWidget; + type ParentType = gtk::Button; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for PlaybackInfoWidget {} + impl WidgetImpl for PlaybackInfoWidget {} + impl ButtonImpl for PlaybackInfoWidget {} +} + +glib::wrapper! { + pub struct PlaybackInfoWidget(ObjectSubclass) @extends gtk::Widget, gtk::Button; +} + +impl PlaybackInfoWidget { + pub fn set_title_and_artist(&self, title: &str, artist: &str) { + let widget = self.imp(); + let title = glib::markup_escape_text(title); + let artist = glib::markup_escape_text(artist); + let label = format!("{}\n{}", title.as_str(), artist.as_str()); + widget.current_song_info.set_label(&label[..]); + } + + pub fn reset_info(&self) { + let widget = self.imp(); + widget + .current_song_info + // translators: Short text displayed instead of a song title when nothing plays + .set_label(&gettext("No song playing")); + widget + .playing_image + .set_from_icon_name(Some("emblem-music-symbolic")); + widget + .playing_image + .set_from_icon_name(Some("emblem-music-symbolic")); + } + + pub fn set_info_visible(&self, visible: bool) { + self.imp().current_song_info.set_visible(visible); + } + + pub fn set_artwork(&self, art: &gdk_pixbuf::Pixbuf) { + self.imp().playing_image.set_from_pixbuf(Some(art)); + } +} diff --git a/src/app/components/playback/playback_widget.blp b/src/app/components/playback/playback_widget.blp new file mode 100644 index 0000000..d1a2fdd --- /dev/null +++ b/src/app/components/playback/playback_widget.blp @@ -0,0 +1,101 @@ +using Gtk 4.0; +using Adw 1; + +template $PlaybackWidget : Box { + orientation: vertical; + + Scale seek_bar { + show-fill-level: true; + restrict-to-fill-level: false; + fill-level: 0; + digits: 0; + value-pos: left; + + styles [ + "seek-bar", + ] + } + + Adw.Squeezer { + margin-top: 4; + margin-bottom: 4; + margin-start: 8; + margin-end: 8; + + Grid { + hexpand: true; + column-homogeneous: true; + + $PlaybackInfoWidget now_playing { + receives-default: "1"; + halign: "start"; + valign: "center"; + has-frame: "0"; + + layout { + column-span: "1"; + column: "0"; + row: "0"; + } + } + + $PlaybackControlsWidget controls { + layout { + column-span: "1"; + column: "1"; + row: "0"; + } + } + + Box { + margin-top: 4; + margin-bottom: 4; + margin-start: 4; + margin-end: 4; + + layout { + column-span: "1"; + column: "2"; + row: "0"; + } + + Label track_position { + sensitive: false; + label: "0∶00"; + halign: end; + hexpand: true; + + styles [ + "numeric", + ] + } + + Label track_duration { + sensitive: false; + label: " / 0∶00"; + halign: end; + + styles [ + "numeric", + ] + } + } + } + + Box { + halign: fill; + hexpand: true; + + $PlaybackInfoWidget now_playing_mobile { + receives-default: "1"; + halign: "start"; + valign: "center"; + has-frame: "0"; + } + + $PlaybackControlsWidget controls_mobile { + halign: "center"; + } + } + } +} diff --git a/src/app/components/playback/playback_widget.rs b/src/app/components/playback/playback_widget.rs new file mode 100644 index 0000000..984f4ed --- /dev/null +++ b/src/app/components/playback/playback_widget.rs @@ -0,0 +1,242 @@ +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::{glib, CompositeTemplate}; + +use crate::app::components::display_add_css_provider; +use crate::app::components::utils::{format_duration, Clock, Debouncer}; +use crate::app::loader::ImageLoader; +use crate::app::models::RepeatMode; +use crate::app::Worker; + +use super::playback_controls::PlaybackControlsWidget; +use super::playback_info::PlaybackInfoWidget; + +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/playback_widget.ui")] + pub struct PlaybackWidget { + #[template_child] + pub controls: TemplateChild, + + #[template_child] + pub controls_mobile: TemplateChild, + + #[template_child] + pub now_playing: TemplateChild, + + #[template_child] + pub now_playing_mobile: TemplateChild, + + #[template_child] + pub seek_bar: TemplateChild, + + #[template_child] + pub track_position: TemplateChild, + + #[template_child] + pub track_duration: TemplateChild, + + pub clock: Clock, + } + + #[glib::object_subclass] + impl ObjectSubclass for PlaybackWidget { + const NAME: &'static str = "PlaybackWidget"; + type Type = super::PlaybackWidget; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for PlaybackWidget { + fn constructed(&self) { + self.parent_constructed(); + self.now_playing_mobile.set_info_visible(false); + self.now_playing.set_info_visible(true); + display_add_css_provider(resource!("/components/playback.css")); + } + } + + impl WidgetImpl for PlaybackWidget {} + impl BoxImpl for PlaybackWidget {} +} + +glib::wrapper! { + pub struct PlaybackWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; +} + +impl PlaybackWidget { + pub fn set_title_and_artist(&self, title: &str, artist: &str) { + let widget = self.imp(); + widget.now_playing.set_title_and_artist(title, artist); + } + + pub fn reset_info(&self) { + let widget = self.imp(); + widget.now_playing.reset_info(); + widget.now_playing_mobile.reset_info(); + self.set_song_duration(None); + } + + fn set_artwork(&self, image: &gdk_pixbuf::Pixbuf) { + let widget = self.imp(); + widget.now_playing.set_artwork(image); + widget.now_playing_mobile.set_artwork(image); + } + + pub fn set_artwork_from_url(&self, url: String, worker: &Worker) { + let weak_self = self.downgrade(); + worker.send_local_task(async move { + let loader = ImageLoader::new(); + let result = loader.load_remote(&url, "jpg", 48, 48).await; + if let (Some(ref _self), Some(ref result)) = (weak_self.upgrade(), result) { + _self.set_artwork(result); + } + }); + } + + pub fn set_song_duration(&self, duration: Option) { + let widget = self.imp(); + let class = "seek-bar--active"; + if let Some(duration) = duration { + self.add_css_class(class); + widget.seek_bar.set_range(0.0, duration); + widget.seek_bar.set_value(0.0); + widget.track_position.set_text("0∶00"); + widget + .track_duration + .set_text(&format!(" / {}", format_duration(duration))); + widget.track_position.set_visible(true); + widget.track_duration.set_visible(true); + } else { + self.remove_css_class(class); + widget.seek_bar.set_range(0.0, 0.0); + widget.track_position.set_visible(false); + widget.track_duration.set_visible(false); + } + } + + pub fn set_seek_position(&self, pos: f64) { + let widget = self.imp(); + widget.seek_bar.set_value(pos); + widget.track_position.set_text(&format_duration(pos)); + } + + pub fn increment_seek_position(&self) { + let value = self.imp().seek_bar.value() + 1_000.0; + self.set_seek_position(value); + } + + pub fn connect_now_playing_clicked(&self, f: F) + where + F: Fn() + Clone + 'static, + { + let widget = self.imp(); + let f_clone = f.clone(); + widget.now_playing.connect_clicked(move |_| f_clone()); + widget.now_playing_mobile.connect_clicked(move |_| f()); + } + + pub fn connect_seek(&self, seek: Seek) + where + Seek: Fn(u32) + Clone + 'static, + { + let debouncer = Debouncer::new(); + let widget = self.imp(); + widget.seek_bar.set_increments(5_000.0, 10_000.0); + widget.seek_bar.connect_change_value( + clone!(@weak self as _self => @default-return glib::signal::Inhibit(false), move |_, _, requested| { + _self.imp() + .track_position + .set_text(&format_duration(requested)); + let seek = seek.clone(); + debouncer.debounce(200, move || seek(requested as u32)); + glib::signal::Inhibit(false) + }), + ); + } + + pub fn set_playing(&self, is_playing: bool) { + let widget = self.imp(); + widget.controls.set_playing(is_playing); + widget.controls_mobile.set_playing(is_playing); + if is_playing { + widget + .clock + .start(clone!(@weak self as _self => move || _self.increment_seek_position())); + } else { + widget.clock.stop(); + } + } + + pub fn set_repeat_mode(&self, mode: RepeatMode) { + let widget = self.imp(); + widget.controls.set_repeat_mode(mode); + widget.controls_mobile.set_repeat_mode(mode); + } + + pub fn set_shuffled(&self, shuffled: bool) { + let widget = self.imp(); + widget.controls.set_shuffled(shuffled); + widget.controls_mobile.set_shuffled(shuffled); + } + + pub fn set_seekbar_visible(&self, visible: bool) { + let widget = self.imp(); + widget.seek_bar.set_visible(visible); + } + + pub fn connect_play_pause(&self, f: F) + where + F: Fn() + Clone + 'static, + { + let widget = self.imp(); + widget.controls.connect_play_pause(f.clone()); + widget.controls_mobile.connect_play_pause(f); + } + + pub fn connect_prev(&self, f: F) + where + F: Fn() + Clone + 'static, + { + let widget = self.imp(); + widget.controls.connect_prev(f.clone()); + widget.controls_mobile.connect_prev(f); + } + + pub fn connect_next(&self, f: F) + where + F: Fn() + Clone + 'static, + { + let widget = self.imp(); + widget.controls.connect_next(f.clone()); + widget.controls_mobile.connect_next(f); + } + + pub fn connect_shuffle(&self, f: F) + where + F: Fn() + Clone + 'static, + { + let widget = self.imp(); + widget.controls.connect_shuffle(f.clone()); + widget.controls_mobile.connect_shuffle(f); + } + + pub fn connect_repeat(&self, f: F) + where + F: Fn() + Clone + 'static, + { + let widget = self.imp(); + widget.controls.connect_repeat(f.clone()); + widget.controls_mobile.connect_repeat(f); + } +} diff --git a/src/app/components/player_notifier.rs b/src/app/components/player_notifier.rs new file mode 100644 index 0000000..5fbf8ce --- /dev/null +++ b/src/app/components/player_notifier.rs @@ -0,0 +1,239 @@ +use std::ops::Deref; +use std::rc::Rc; + +use futures::channel::mpsc::UnboundedSender; +use librespot::core::spotify_id::{SpotifyId, SpotifyItemType}; + +use crate::app::components::EventListener; +use crate::app::state::{ + Device, LoginAction, LoginEvent, LoginStartedEvent, PlaybackEvent, SettingsEvent, +}; +use crate::app::{ActionDispatcher, AppAction, AppEvent, AppModel, SongsSource}; +use crate::connect::ConnectCommand; +use crate::player::Command; + +enum CurrentlyPlaying { + WithSource { + source: SongsSource, + offset: usize, + song: String, + }, + Songs { + songs: Vec, + offset: usize, + }, +} + +impl CurrentlyPlaying { + fn song_id(&self) -> &String { + match self { + Self::WithSource { song, .. } => song, + Self::Songs { songs, offset } => &songs[*offset], + } + } +} + +pub struct PlayerNotifier { + app_model: Rc, + dispatcher: Box, + command_sender: UnboundedSender, + connect_command_sender: UnboundedSender, +} + +impl PlayerNotifier { + pub fn new( + app_model: Rc, + dispatcher: Box, + command_sender: UnboundedSender, + connect_command_sender: UnboundedSender, + ) -> Self { + Self { + app_model, + dispatcher, + command_sender, + connect_command_sender, + } + } + + fn is_playing(&self) -> bool { + self.app_model.get_state().playback.is_playing() + } + + fn currently_playing(&self) -> Option { + let state = self.app_model.get_state(); + let song = state.playback.current_song_id()?; + let offset = state.playback.current_song_index()?; + let source = state.playback.current_source().cloned(); + let result = match source { + Some(source) if source.has_spotify_uri() => CurrentlyPlaying::WithSource { + source, + offset, + song, + }, + _ => CurrentlyPlaying::Songs { + songs: state.playback.songs().map_collect(|s| s.id), + offset, + }, + }; + Some(result) + } + + fn device(&self) -> impl Deref + '_ { + self.app_model.map_state(|s| s.playback.current_device()) + } + + fn notify_login(&self, event: &LoginEvent) { + info!("notify_login: {:?}", event); + let command = match event { + LoginEvent::LoginStarted(LoginStartedEvent::Password { username, password }) => { + Some(Command::PasswordLogin { + username: username.to_owned(), + password: password.to_owned(), + }) + } + LoginEvent::LoginStarted(LoginStartedEvent::Token { username, token }) => { + Some(Command::TokenLogin { + username: username.to_owned(), + token: token.to_owned(), + }) + } + LoginEvent::LoginStarted(LoginStartedEvent::OAuthSpotify {}) => { + Some(Command::OAuthLogin) + } + LoginEvent::FreshTokenRequested => Some(Command::RefreshToken), + LoginEvent::LogoutCompleted => Some(Command::Logout), + _ => None, + }; + + if let Some(command) = command { + self.send_command_to_local_player(command); + } + } + + fn notify_connect_player(&self, event: &PlaybackEvent) { + let event = event.clone(); + let currently_playing = self.currently_playing(); + let command = match event { + PlaybackEvent::TrackChanged(_) | PlaybackEvent::SourceChanged => { + match currently_playing { + Some(CurrentlyPlaying::WithSource { + source, + offset, + song, + }) => Some(ConnectCommand::PlayerLoadInContext { + source, + offset, + song, + }), + Some(CurrentlyPlaying::Songs { songs, offset }) => { + Some(ConnectCommand::PlayerLoad { songs, offset }) + } + None => None, + } + } + PlaybackEvent::TrackSeeked(position) => { + Some(ConnectCommand::PlayerSeek(position as usize)) + } + PlaybackEvent::PlaybackPaused => Some(ConnectCommand::PlayerPause), + PlaybackEvent::PlaybackResumed => Some(ConnectCommand::PlayerResume), + PlaybackEvent::VolumeSet(volume) => Some(ConnectCommand::PlayerSetVolume( + (volume * 100f64).trunc() as u8, + )), + PlaybackEvent::RepeatModeChanged(mode) => Some(ConnectCommand::PlayerRepeat(mode)), + PlaybackEvent::ShuffleChanged(shuffled) => { + Some(ConnectCommand::PlayerShuffle(shuffled)) + } + _ => None, + }; + + if let Some(command) = command { + self.send_command_to_connect_player(command); + } + } + + fn notify_local_player(&self, event: &PlaybackEvent) { + let command = match event { + PlaybackEvent::PlaybackPaused => Some(Command::PlayerPause), + PlaybackEvent::PlaybackResumed => Some(Command::PlayerResume), + PlaybackEvent::PlaybackStopped => Some(Command::PlayerStop), + PlaybackEvent::VolumeSet(volume) => Some(Command::PlayerSetVolume(*volume)), + PlaybackEvent::TrackChanged(id) => { + info!("track changed: {}", id); + SpotifyId::from_base62(id).ok().map(|mut track| { + track.item_type = SpotifyItemType::Track; + Command::PlayerLoad { + track, + resume: true, + } + }) + } + PlaybackEvent::SourceChanged => { + let resume = self.is_playing(); + self.currently_playing() + .and_then(|c| SpotifyId::from_base62(c.song_id()).ok()) + .map(|mut track| { + track.item_type = SpotifyItemType::Track; + Command::PlayerLoad { track, resume } + }) + } + PlaybackEvent::TrackSeeked(position) => Some(Command::PlayerSeek(*position)), + PlaybackEvent::Preload(id) => SpotifyId::from_base62(id) + .ok() + .map(|mut track| { + track.item_type = SpotifyItemType::Track; + track + }) + .map(Command::PlayerPreload), + _ => None, + }; + + if let Some(command) = command { + self.send_command_to_local_player(command); + } + } + + fn send_command_to_connect_player(&self, command: ConnectCommand) { + self.connect_command_sender.unbounded_send(command).unwrap(); + } + + fn send_command_to_local_player(&self, command: Command) { + let dispatcher = &self.dispatcher; + self.command_sender + .unbounded_send(command) + .unwrap_or_else(|_| { + dispatcher.dispatch(AppAction::LoginAction(LoginAction::SetLoginFailure)); + }); + } + + fn switch_device(&mut self, device: &Device) { + match device { + Device::Connect(device) => { + self.send_command_to_local_player(Command::PlayerStop); + self.send_command_to_connect_player(ConnectCommand::SetDevice(device.id.clone())); + self.notify_connect_player(&PlaybackEvent::SourceChanged); + } + Device::Local => { + self.send_command_to_connect_player(ConnectCommand::PlayerStop); + self.notify_local_player(&PlaybackEvent::SourceChanged); + } + } + } +} + +impl EventListener for PlayerNotifier { + fn on_event(&mut self, event: &AppEvent) { + let device = self.device().clone(); + match (device, event) { + (_, AppEvent::LoginEvent(event)) => self.notify_login(event), + (_, AppEvent::PlaybackEvent(PlaybackEvent::SwitchedDevice(d))) => self.switch_device(d), + (Device::Local, AppEvent::PlaybackEvent(event)) => self.notify_local_player(event), + (Device::Local, AppEvent::SettingsEvent(SettingsEvent::PlayerSettingsChanged)) => { + self.send_command_to_local_player(Command::ReloadSettings) + } + (Device::Connect(_), AppEvent::PlaybackEvent(event)) => { + self.notify_connect_player(event) + } + _ => {} + } + } +} diff --git a/src/app/components/playlist/mod.rs b/src/app/components/playlist/mod.rs new file mode 100644 index 0000000..e319aa0 --- /dev/null +++ b/src/app/components/playlist/mod.rs @@ -0,0 +1,8 @@ +#[allow(clippy::module_inception)] +mod playlist; +pub use playlist::*; + +mod song; +pub use song::*; + +mod song_actions; diff --git a/src/app/components/playlist/playback-indicator/playback-0-symbolic.svg b/src/app/components/playlist/playback-indicator/playback-0-symbolic.svg new file mode 100644 index 0000000..c80127a --- /dev/null +++ b/src/app/components/playlist/playback-indicator/playback-0-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/components/playlist/playback-indicator/playback-1-symbolic.svg b/src/app/components/playlist/playback-indicator/playback-1-symbolic.svg new file mode 100644 index 0000000..1cdba19 --- /dev/null +++ b/src/app/components/playlist/playback-indicator/playback-1-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/components/playlist/playback-indicator/playback-10-symbolic.svg b/src/app/components/playlist/playback-indicator/playback-10-symbolic.svg new file mode 100644 index 0000000..c37ead2 --- /dev/null +++ b/src/app/components/playlist/playback-indicator/playback-10-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/components/playlist/playback-indicator/playback-11-symbolic.svg b/src/app/components/playlist/playback-indicator/playback-11-symbolic.svg new file mode 100644 index 0000000..a3e0176 --- /dev/null +++ b/src/app/components/playlist/playback-indicator/playback-11-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/components/playlist/playback-indicator/playback-12-symbolic.svg b/src/app/components/playlist/playback-indicator/playback-12-symbolic.svg new file mode 100644 index 0000000..2f1c0c2 --- /dev/null +++ b/src/app/components/playlist/playback-indicator/playback-12-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/components/playlist/playback-indicator/playback-13-symbolic.svg b/src/app/components/playlist/playback-indicator/playback-13-symbolic.svg new file mode 100644 index 0000000..581321f --- /dev/null +++ b/src/app/components/playlist/playback-indicator/playback-13-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/components/playlist/playback-indicator/playback-14-symbolic.svg b/src/app/components/playlist/playback-indicator/playback-14-symbolic.svg new file mode 100644 index 0000000..dc7255f --- /dev/null +++ b/src/app/components/playlist/playback-indicator/playback-14-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/components/playlist/playback-indicator/playback-15-symbolic.svg b/src/app/components/playlist/playback-indicator/playback-15-symbolic.svg new file mode 100644 index 0000000..5425e75 --- /dev/null +++ b/src/app/components/playlist/playback-indicator/playback-15-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/components/playlist/playback-indicator/playback-16-symbolic.svg b/src/app/components/playlist/playback-indicator/playback-16-symbolic.svg new file mode 100644 index 0000000..aa07e2d --- /dev/null +++ b/src/app/components/playlist/playback-indicator/playback-16-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/components/playlist/playback-indicator/playback-2-symbolic.svg b/src/app/components/playlist/playback-indicator/playback-2-symbolic.svg new file mode 100644 index 0000000..743ad3d --- /dev/null +++ b/src/app/components/playlist/playback-indicator/playback-2-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/components/playlist/playback-indicator/playback-3-symbolic.svg b/src/app/components/playlist/playback-indicator/playback-3-symbolic.svg new file mode 100644 index 0000000..7a6bc91 --- /dev/null +++ b/src/app/components/playlist/playback-indicator/playback-3-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/components/playlist/playback-indicator/playback-4-symbolic.svg b/src/app/components/playlist/playback-indicator/playback-4-symbolic.svg new file mode 100644 index 0000000..a61b085 --- /dev/null +++ b/src/app/components/playlist/playback-indicator/playback-4-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/components/playlist/playback-indicator/playback-5-symbolic.svg b/src/app/components/playlist/playback-indicator/playback-5-symbolic.svg new file mode 100644 index 0000000..fbc8614 --- /dev/null +++ b/src/app/components/playlist/playback-indicator/playback-5-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/components/playlist/playback-indicator/playback-6-symbolic.svg b/src/app/components/playlist/playback-indicator/playback-6-symbolic.svg new file mode 100644 index 0000000..018ac0d --- /dev/null +++ b/src/app/components/playlist/playback-indicator/playback-6-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/components/playlist/playback-indicator/playback-7-symbolic.svg b/src/app/components/playlist/playback-indicator/playback-7-symbolic.svg new file mode 100644 index 0000000..54d1b35 --- /dev/null +++ b/src/app/components/playlist/playback-indicator/playback-7-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/components/playlist/playback-indicator/playback-8-symbolic.svg b/src/app/components/playlist/playback-indicator/playback-8-symbolic.svg new file mode 100644 index 0000000..e70c5d3 --- /dev/null +++ b/src/app/components/playlist/playback-indicator/playback-8-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/components/playlist/playback-indicator/playback-9-symbolic.svg b/src/app/components/playlist/playback-indicator/playback-9-symbolic.svg new file mode 100644 index 0000000..df12667 --- /dev/null +++ b/src/app/components/playlist/playback-indicator/playback-9-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/components/playlist/playback-indicator/playback-paused-symbolic.svg b/src/app/components/playlist/playback-indicator/playback-paused-symbolic.svg new file mode 100644 index 0000000..97da731 --- /dev/null +++ b/src/app/components/playlist/playback-indicator/playback-paused-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/components/playlist/playlist.rs b/src/app/components/playlist/playlist.rs new file mode 100644 index 0000000..691f783 --- /dev/null +++ b/src/app/components/playlist/playlist.rs @@ -0,0 +1,248 @@ +use gio::prelude::*; +use gtk::prelude::*; +use std::ops::Deref; +use std::rc::Rc; + +use crate::app::components::utils::{ancestor, AnimatorDefault}; +use crate::app::components::{Component, EventListener, SongWidget}; +use crate::app::models::{SongListModel, SongModel, SongState}; +use crate::app::state::{PlaybackEvent, SelectionEvent, SelectionState}; +use crate::app::{AppEvent, Worker}; + +pub trait PlaylistModel { + fn is_paused(&self) -> bool; + + fn song_list_model(&self) -> SongListModel; + + fn current_song_id(&self) -> Option; + + fn play_song_at(&self, pos: usize, id: &str); + + fn autoscroll_to_playing(&self) -> bool { + true + } + + fn show_song_covers(&self) -> bool { + true + } + + fn actions_for(&self, _id: &str) -> Option { + None + } + fn menu_for(&self, _id: &str) -> Option { + None + } + + fn select_song(&self, _id: &str) {} + fn deselect_song(&self, _id: &str) {} + fn enable_selection(&self) -> bool { + false + } + + fn selection(&self) -> Option + '_>> { + None + } + + fn is_selection_enabled(&self) -> bool { + self.selection() + .map(|s| s.is_selection_enabled()) + .unwrap_or(false) + } + + fn song_state(&self, id: &str) -> SongState { + let is_playing = self.current_song_id().map(|s| s.eq(id)).unwrap_or(false); + let is_selected = self + .selection() + .map(|s| s.is_song_selected(id)) + .unwrap_or(false); + SongState { + is_selected, + is_playing, + } + } + + fn toggle_select(&self, id: &str) { + if let Some(selection) = self.selection() { + if selection.is_song_selected(id) { + self.deselect_song(id); + } else { + self.select_song(id); + } + } + } +} + +pub struct Playlist { + animator: AnimatorDefault, + listview: gtk::ListView, + model: Rc, +} + +impl Playlist +where + Model: PlaylistModel + 'static, +{ + pub fn new(listview: gtk::ListView, model: Rc, worker: Worker) -> Self { + let list_model = model.song_list_model(); + let selection_model = gtk::NoSelection::new(Some(list_model.clone())); + let factory = gtk::SignalListItemFactory::new(); + + listview.add_css_class("playlist"); + listview.set_show_separators(true); + listview.set_valign(gtk::Align::Start); + + listview.set_factory(Some(&factory)); + listview.set_single_click_activate(true); + listview.set_model(Some(&selection_model)); + Self::set_paused(&listview, model.is_paused()); + Self::set_selection_active(&listview, model.is_selection_enabled()); + + factory.connect_setup(|_, item| { + let item = item.downcast_ref::().unwrap(); + item.set_child(Some(&SongWidget::new())); + }); + + factory.connect_bind(clone!(@weak model => move |_, item| { + let item = item.downcast_ref::().unwrap(); + let song_model = item.item().unwrap().downcast::().unwrap(); + song_model.set_state(model.song_state(&song_model.get_id())); + + let widget = item.child().unwrap().downcast::().unwrap(); + widget.bind(&song_model, worker.clone(), model.show_song_covers()); + + let id = &song_model.get_id(); + widget.set_actions(model.actions_for(id).as_ref()); + widget.set_menu(model.menu_for(id).as_ref()); + })); + + factory.connect_unbind(|_, item| { + let item = item.downcast_ref::().unwrap(); + let song_model = item.item().unwrap().downcast::().unwrap(); + song_model.unbind_all(); + }); + + listview.connect_activate(clone!(@weak list_model, @weak model => move |_, position| { + let song = list_model.index_continuous(position as usize).expect("attempt to access invalid index"); + let song = song.description(); + let selection_enabled = model.is_selection_enabled(); + if selection_enabled { + model.toggle_select(&song.id); + } else { + model.play_song_at(position as usize, &song.id); + } + })); + + let press_gesture = gtk::GestureLongPress::new(); + press_gesture.set_touch_only(false); + press_gesture.set_propagation_phase(gtk::PropagationPhase::Capture); + press_gesture.connect_pressed(clone!(@weak model => move |_, _, _| { + model.enable_selection(); + })); + listview.add_controller(press_gesture); + + Self { + animator: AnimatorDefault::ease_in_out_animator(), + listview, + model, + } + } + + fn autoscroll_to_playing(&self, index: usize) { + let len = self.model.song_list_model().partial_len() as f64; + let scrolled_window: Option = ancestor(&self.listview); + let adj = scrolled_window.map(|w| w.vadjustment()); + if let Some(adj) = adj { + let v = adj.value(); + let v2 = v + 0.9 * adj.page_size(); + let pos = (index as f64) * adj.upper() / len; + debug!("estimated pos: {}", pos); + debug!("current window: {} -- {}", v, v2); + if pos < v || pos > v2 { + self.animator.animate( + 20, + clone!(@weak adj => @default-return false, move |p| { + let v = adj.value(); + adj.set_value(v + p * (pos - v)); + true + }), + ); + } + } + } + + fn update_list(&self) { + let autoscroll_to_playing = self.model.autoscroll_to_playing(); + let is_selection_enabled = self.model.is_selection_enabled(); + + self.model.song_list_model().for_each(|i, model_song| { + let state = self.model.song_state(&model_song.get_id()); + model_song.set_state(state); + if state.is_playing && autoscroll_to_playing && !is_selection_enabled { + self.autoscroll_to_playing(i); + } + }); + } + + fn set_selection_active(listview: >k::ListView, active: bool) { + let class_name = "playlist--selectable"; + if active { + listview.add_css_class(class_name); + } else { + listview.remove_css_class(class_name); + } + } + + fn set_paused(listview: >k::ListView, paused: bool) { + let class_name = "playlist--paused"; + if paused { + listview.add_css_class(class_name); + } else { + listview.remove_css_class(class_name); + } + } +} + +impl SongModel { + fn set_state( + &self, + SongState { + is_playing, + is_selected, + }: SongState, + ) { + self.set_playing(is_playing); + self.set_selected(is_selected); + } +} + +impl EventListener for Playlist +where + Model: PlaylistModel + 'static, +{ + fn on_event(&mut self, event: &AppEvent) { + match event { + AppEvent::SelectionEvent(SelectionEvent::SelectionChanged) => { + self.update_list(); + } + AppEvent::PlaybackEvent(PlaybackEvent::TrackChanged(_)) => { + self.update_list(); + } + AppEvent::PlaybackEvent( + PlaybackEvent::PlaybackResumed | PlaybackEvent::PlaybackPaused, + ) => { + Self::set_paused(&self.listview, self.model.is_paused()); + } + AppEvent::SelectionEvent(SelectionEvent::SelectionModeChanged(_)) => { + Self::set_selection_active(&self.listview, self.model.is_selection_enabled()); + self.update_list(); + } + _ => {} + } + } +} + +impl Component for Playlist { + fn get_root_widget(&self) -> >k::Widget { + self.listview.upcast_ref() + } +} diff --git a/src/app/components/playlist/song.blp b/src/app/components/playlist/song.blp new file mode 100644 index 0000000..a065080 --- /dev/null +++ b/src/app/components/playlist/song.blp @@ -0,0 +1,143 @@ +using Gtk 4.0; + +template $SongWidget : Grid { + margin-start: 6; + margin-end: 6; + margin-top: 6; + margin-bottom: 6; + column-spacing: 6; + row-spacing: 0; + + Overlay { + layout { + row-span: "2"; + column: "0"; + row: "0"; + } + + Label song_index { + label: "1"; + sensitive: false; + halign: center; + + styles [ + "song__index", + "numeric", + ] + } + + [overlay] + Image song_cover { + pixel-size: 30; + overflow: hidden; + halign: center; + valign: center; + + styles [ + "song__cover", + ] + } + + [overlay] + Spinner song_icon { + halign: center; + valign: center; + + styles [ + "song__icon", + ] + } + + [overlay] + CheckButton song_checkbox { + halign: center; + valign: center; + + styles [ + "song__checkbox", + ] + } + } + + Label song_title { + label: "Title"; + ellipsize: middle; + max-width-chars: 50; + xalign: 0; + yalign: 1; + hexpand: true; + + layout { + column-span: "2"; + column: "1"; + row: "0"; + } + + styles [ + "title", + ] + } + + Label song_artist { + label: "Artist"; + ellipsize: middle; + max-width-chars: 35; + xalign: 0; + hexpand: true; + + layout { + column-span: "1"; + column: "1"; + row: "1"; + } + + styles [ + "subtitle", + ] + } + + Label song_length { + sensitive: false; + label: "0∶00"; + justify: right; + max-width-chars: 7; + xalign: 1; + hexpand: false; + + layout { + row-span: "2"; + column: "3"; + row: "0"; + } + + styles [ + "numeric", + ] + } + + MenuButton menu_btn { + focus-on-click: false; + receives-default: true; + icon-name: "view-more-symbolic"; + has-frame: false; + hexpand: false; + halign: end; + valign: center; + tooltip-text: "Menu"; + + layout { + row-span: "2"; + column: "4"; + row: "0"; + } + + styles [ + "circular", + "flat", + ] + } + + styles [ + "song", + ] +} diff --git a/src/app/components/playlist/song.css b/src/app/components/playlist/song.css new file mode 100644 index 0000000..639cf74 --- /dev/null +++ b/src/app/components/playlist/song.css @@ -0,0 +1,186 @@ +.playlist .song__index { + transition: opacity 150ms ease; + margin: 6px 12px; + padding: 0; + opacity: 1; + min-width: 1.5em; +} + +.song__cover { + border-radius: 6px; + border: 1px solid @card_shade_color; +} + +.album__tracks .song__cover { + opacity: 0; +} + + +/* playback indicator */ + +.song--playing .song__icon { + opacity: 1; + animation: playing 1s linear infinite; + color: @accent_bg_color; + min-width: 16px; + -gtk-icon-source: -gtk-icontheme("playback-0-symbolic"); +} + +@keyframes playing { + 0% { + -gtk-icon-source: -gtk-icontheme("playback-0-symbolic"); + } + + 6% { + -gtk-icon-source: -gtk-icontheme("playback-1-symbolic"); + } + + 12% { + -gtk-icon-source: -gtk-icontheme("playback-2-symbolic"); + } + + 18% { + -gtk-icon-source: -gtk-icontheme("playback-3-symbolic"); + } + + 24% { + -gtk-icon-source: -gtk-icontheme("playback-4-symbolic"); + } + + 30% { + -gtk-icon-source: -gtk-icontheme("playback-5-symbolic"); + } + + 36% { + -gtk-icon-source: -gtk-icontheme("playback-6-symbolic"); + } + + 42% { + -gtk-icon-source: -gtk-icontheme("playback-7-symbolic"); + } + + 49% { + -gtk-icon-source: -gtk-icontheme("playback-8-symbolic"); + } + + 54% { + -gtk-icon-source: -gtk-icontheme("playback-9-symbolic"); + } + + 60% { + -gtk-icon-source: -gtk-icontheme("playback-10-symbolic"); + } + + 66% { + -gtk-icon-source: -gtk-icontheme("playback-11-symbolic"); + } + + 72% { + -gtk-icon-source: -gtk-icontheme("playback-12-symbolic"); + } + + 79% { + -gtk-icon-source: -gtk-icontheme("playback-13-symbolic"); + } + + 85% { + -gtk-icon-source: -gtk-icontheme("playback-14-symbolic"); + } + + 90% { + -gtk-icon-source: -gtk-icontheme("playback-15-symbolic"); + } + + 96% { + -gtk-icon-source: -gtk-icontheme("playback-16-symbolic"); + } + + 100% { + -gtk-icon-source: -gtk-icontheme("playback-0-symbolic"); + } +} + +.playlist--paused .song--playing .song__icon { + animation: none; + -gtk-icon-source: -gtk-icontheme("playback-paused-symbolic"); +} + +.song__icon, +.song__checkbox, +.song--playing .song__index, +.song--playing .song__cover, +.playlist--selectable .song__index, +.playlist--selectable .song__cover, +.playlist--selectable .song__icon { + transition: opacity 150ms ease; + opacity: 0; +} + + +.playlist--selectable .song__checkbox, +.playlist--selectable .song__checkbox check { + opacity: 1; + filter: none; +} + + +row:hover .song__menu--enabled, .song__menu--enabled:checked { + opacity: 1; +} + + +/* Song Labels */ +.song--playing label.title { + font-weight: bold; +} + +/* "Context Menu" */ +.song__menu { + opacity: 0; +} + +.song__menu--enabled { + opacity: 0.2; +} + + +/* Song boxed list styling */ + +.playlist { + background: transparent; +} + +.playlist row { + background: @card_bg_color; + margin-left: 12px; + margin-right: 12px; + box-shadow: 1px 0px 3px rgba(0, 0, 0, 0.07), -1px 0px 3px rgba(0, 0, 0, 0.07); + transition: background-color 150ms ease; +} + +.playlist row:hover { + background-image: image(alpha(currentColor, 0.03)); +} + +.playlist row:active { + background-image: image(alpha(currentColor, 0.08)); +} + + +.playlist row:first-child { + margin-top: 12px; + border-radius: 12px 12px 0 0; +} + +.playlist row:last-child { + margin-bottom: 12px; + border-bottom-color: rgba(0, 0, 0, 0); + border-radius: 0 0 12px 12px; + box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.21); +} + +.playlist row:only-child { + margin-top: 12px; + margin-bottom: 12px; + border-radius: 12px 12px 12px 12px; +} \ No newline at end of file diff --git a/src/app/components/playlist/song.rs b/src/app/components/playlist/song.rs new file mode 100644 index 0000000..79b40b3 --- /dev/null +++ b/src/app/components/playlist/song.rs @@ -0,0 +1,189 @@ +use crate::app::components::display_add_css_provider; +use crate::app::loader::ImageLoader; +use crate::app::models::SongModel; +use crate::app::Worker; +use gio::MenuModel; +use glib::subclass::InitializingObject; + +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; + +mod imp { + + use super::*; + + const SONG_CLASS: &str = "song--playing"; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/song.ui")] + pub struct SongWidget { + #[template_child] + pub song_index: TemplateChild, + + #[template_child] + pub song_icon: TemplateChild, + + #[template_child] + pub song_checkbox: TemplateChild, + + #[template_child] + pub song_title: TemplateChild, + + #[template_child] + pub song_artist: TemplateChild, + + #[template_child] + pub song_length: TemplateChild, + + #[template_child] + pub menu_btn: TemplateChild, + + #[template_child] + pub song_cover: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for SongWidget { + const NAME: &'static str = "SongWidget"; + type Type = super::SongWidget; + type ParentType = gtk::Grid; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &InitializingObject) { + obj.init_template(); + } + } + + lazy_static! { + static ref PROPERTIES: [glib::ParamSpec; 2] = [ + glib::ParamSpecBoolean::builder("playing").build(), + glib::ParamSpecBoolean::builder("selected").build() + ]; + } + + impl ObjectImpl for SongWidget { + fn properties() -> &'static [glib::ParamSpec] { + &*PROPERTIES + } + + fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { + match pspec.name() { + "playing" => { + let is_playing = value + .get() + .expect("type conformity checked by `Object::set_property`"); + if is_playing { + self.obj().add_css_class(SONG_CLASS); + } else { + self.obj().remove_css_class(SONG_CLASS); + } + } + "selected" => { + let is_selected = value + .get() + .expect("type conformity checked by `Object::set_property`"); + self.song_checkbox.set_active(is_selected); + } + _ => unimplemented!(), + } + } + + fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + match pspec.name() { + "playing" => self.obj().has_css_class(SONG_CLASS).to_value(), + "selected" => self.song_checkbox.is_active().to_value(), + _ => unimplemented!(), + } + } + + fn constructed(&self) { + self.parent_constructed(); + self.song_checkbox.set_sensitive(false); + } + + fn dispose(&self) { + while let Some(child) = self.obj().first_child() { + child.unparent(); + } + } + } + + impl WidgetImpl for SongWidget {} + impl GridImpl for SongWidget {} +} + +glib::wrapper! { + pub struct SongWidget(ObjectSubclass) @extends gtk::Widget, gtk::Grid; +} + +impl Default for SongWidget { + fn default() -> Self { + Self::new() + } +} + +impl SongWidget { + pub fn new() -> Self { + display_add_css_provider(resource!("/components/song.css")); + glib::Object::new() + } + + pub fn set_actions(&self, actions: Option<&gio::ActionGroup>) { + self.insert_action_group("song", actions); + } + + pub fn set_menu(&self, menu: Option<&MenuModel>) { + if menu.is_some() { + let widget = self.imp(); + widget.menu_btn.set_menu_model(menu); + widget.menu_btn.add_css_class("song__menu--enabled"); + } + } + + fn set_show_cover(&self, show_cover: bool) { + let song_class = "song--cover"; + if show_cover { + self.add_css_class(song_class); + } else { + self.remove_css_class(song_class); + } + } + + fn set_image(&self, pixbuf: Option<&gdk_pixbuf::Pixbuf>) { + self.imp().song_cover.set_from_pixbuf(pixbuf); + } + + pub fn set_art(&self, model: &SongModel, worker: Worker) { + if let Some(url) = model.description().art.clone() { + let _self = self.downgrade(); + worker.send_local_task(async move { + if let Some(_self) = _self.upgrade() { + let loader = ImageLoader::new(); + let result = loader.load_remote(&url, "jpg", 100, 100).await; + _self.set_image(result.as_ref()); + } + }); + } + } + + pub fn bind(&self, model: &SongModel, worker: Worker, show_cover: bool) { + let widget = self.imp(); + + model.bind_title(&*widget.song_title, "label"); + model.bind_artist(&*widget.song_artist, "label"); + model.bind_duration(&*widget.song_length, "label"); + model.bind_playing(self, "playing"); + model.bind_selected(self, "selected"); + + self.set_show_cover(show_cover); + if show_cover { + self.set_art(model, worker); + } else { + model.bind_index(&*widget.song_index, "label"); + } + } +} diff --git a/src/app/components/playlist/song_actions.rs b/src/app/components/playlist/song_actions.rs new file mode 100644 index 0000000..8a35ac7 --- /dev/null +++ b/src/app/components/playlist/song_actions.rs @@ -0,0 +1,82 @@ +use gdk::prelude::*; +use gio::SimpleAction; + +use crate::app::models::SongDescription; +use crate::app::state::{AppAction, PlaybackAction}; +use crate::app::ActionDispatcher; + +impl SongDescription { + pub fn make_queue_action( + &self, + dispatcher: Box, + name: Option<&str>, + ) -> SimpleAction { + let queue = SimpleAction::new(name.unwrap_or("queue"), None); + let song = self.clone(); + queue.connect_activate(move |_, _| { + dispatcher.dispatch(PlaybackAction::Queue(vec![song.clone()]).into()); + }); + queue + } + + pub fn make_dequeue_action( + &self, + dispatcher: Box, + name: Option<&str>, + ) -> SimpleAction { + let dequeue = SimpleAction::new(name.unwrap_or("dequeue"), None); + let track_id = self.id.clone(); + dequeue.connect_activate(move |_, _| { + dispatcher.dispatch(PlaybackAction::Dequeue(track_id.clone()).into()); + }); + dequeue + } + + pub fn make_link_action(&self, name: Option<&str>) -> SimpleAction { + let track_id = self.id.clone(); + let copy_link = SimpleAction::new(name.unwrap_or("copy_link"), None); + copy_link.connect_activate(move |_, _| { + let link = format!("https://open.spotify.com/track/{track_id}"); + let clipboard = gdk::Display::default().unwrap().clipboard(); + clipboard + .set_content(Some(&gdk::ContentProvider::for_value(&link.to_value()))) + .expect("Failed to set clipboard content"); + }); + copy_link + } + + pub fn make_album_action( + &self, + dispatcher: Box, + name: Option<&str>, + ) -> SimpleAction { + let album_id = self.album.id.clone(); + let view_album = SimpleAction::new(name.unwrap_or("view_album"), None); + view_album.connect_activate(move |_, _| { + dispatcher.dispatch(AppAction::ViewAlbum(album_id.clone())); + }); + view_album + } + + pub fn make_artist_actions( + &self, + dispatcher: Box, + prefix: Option<&str>, + ) -> Vec { + self.artists + .iter() + .map(|artist| { + let id = artist.id.clone(); + let view_artist = SimpleAction::new( + &format!("{}_{}", prefix.unwrap_or("view_artist"), &id), + None, + ); + let dispatcher = dispatcher.box_clone(); + view_artist.connect_activate(move |_, _| { + dispatcher.dispatch(AppAction::ViewArtist(id.clone())); + }); + view_artist + }) + .collect() + } +} diff --git a/src/app/components/playlist_details/mod.rs b/src/app/components/playlist_details/mod.rs new file mode 100644 index 0000000..1bb90ba --- /dev/null +++ b/src/app/components/playlist_details/mod.rs @@ -0,0 +1,14 @@ +#[allow(clippy::module_inception)] +mod playlist_details; +mod playlist_details_model; +mod playlist_header; +mod playlist_headerbar; + +pub use playlist_details::*; +pub use playlist_details_model::*; + +use glib::StaticType; + +pub fn expose_widgets() { + playlist_headerbar::PlaylistHeaderBarWidget::static_type(); +} diff --git a/src/app/components/playlist_details/playlist_details.blp b/src/app/components/playlist_details/playlist_details.blp new file mode 100644 index 0000000..d30cb3f --- /dev/null +++ b/src/app/components/playlist_details/playlist_details.blp @@ -0,0 +1,52 @@ +using Gtk 4.0; +using Adw 1; + +template $PlaylistDetailsWidget : Adw.Bin { + Box { + orientation: vertical; + vexpand: true; + hexpand: true; + + $PlaylistHeaderBarWidget headerbar { + } + + $ScrollingHeaderWidget scrolling_header { + [header] + WindowHandle { + Adw.Clamp { + maximum-size: 900; + + Adw.Squeezer { + valign: center; + homogeneous: false; + transition-type: crossfade; + switch-threshold-policy: natural; + + $PlaylistHeaderWidget header_widget { + } + + $PlaylistHeaderWidget header_mobile { + orientation: "vertical"; + spacing: "12"; + } + } + + styles [ + "playlist_details__clamp", + ] + } + } + + Adw.ClampScrollable { + maximum-size: 900; + + ListView tracks { + } + } + + styles [ + "container", + ] + } + } +} diff --git a/src/app/components/playlist_details/playlist_details.rs b/src/app/components/playlist_details/playlist_details.rs new file mode 100644 index 0000000..0678986 --- /dev/null +++ b/src/app/components/playlist_details/playlist_details.rs @@ -0,0 +1,326 @@ +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; +use std::rc::Rc; + +use super::playlist_header::PlaylistHeaderWidget; +use super::playlist_headerbar::PlaylistHeaderBarWidget; +use super::PlaylistDetailsModel; + +use crate::app::components::{ + Component, EventListener, Playlist, PlaylistModel, ScrollingHeaderWidget, +}; +use crate::app::dispatch::Worker; +use crate::app::loader::ImageLoader; +use crate::app::state::{PlaybackEvent, SelectionEvent}; +use crate::app::{AppEvent, BrowserEvent}; +use libadwaita::subclass::prelude::BinImpl; + +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/playlist_details.ui")] + pub struct PlaylistDetailsWidget { + #[template_child] + pub headerbar: TemplateChild, + + #[template_child] + pub scrolling_header: TemplateChild, + + #[template_child] + pub header_widget: TemplateChild, + + #[template_child] + pub header_mobile: TemplateChild, + + #[template_child] + pub tracks: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for PlaylistDetailsWidget { + const NAME: &'static str = "PlaylistDetailsWidget"; + type Type = super::PlaylistDetailsWidget; + type ParentType = libadwaita::Bin; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for PlaylistDetailsWidget { + fn constructed(&self) { + self.parent_constructed(); + self.header_mobile.set_centered(); + self.header_widget.set_grows_automatically(); + self.header_widget + .entry() + .bind_property("text", self.header_mobile.entry(), "text") + .flags(glib::BindingFlags::BIDIRECTIONAL) + .build(); + } + } + + impl WidgetImpl for PlaylistDetailsWidget {} + impl BinImpl for PlaylistDetailsWidget {} +} + +glib::wrapper! { + pub struct PlaylistDetailsWidget(ObjectSubclass) @extends gtk::Widget, libadwaita::Bin; +} + +impl PlaylistDetailsWidget { + fn new() -> Self { + glib::Object::new() + } + + fn playlist_tracks_widget(&self) -> >k::ListView { + self.imp().tracks.as_ref() + } + + fn connect_bottom_edge(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().scrolling_header.connect_bottom_edge(f); + } + + fn set_header_visible(&self, visible: bool) { + let widget = self.imp(); + widget.headerbar.set_title_visible(true); + if visible { + widget.headerbar.add_classes(&["flat"]); + } else { + widget.headerbar.remove_classes(&["flat"]); + } + } + + fn connect_header(&self) { + self.set_header_visible(false); + self.imp().scrolling_header.connect_header_visibility( + clone!(@weak self as _self => move |visible| { + _self.set_header_visible(visible); + }), + ); + } + + fn set_loaded(&self) { + self.imp() + .scrolling_header + .add_css_class("container--loaded"); + } + + fn set_editing(&self, editing: bool) { + self.imp().header_widget.set_editing(editing); + self.imp().header_mobile.set_editing(editing); + self.imp().headerbar.set_editing(editing); + } + + fn set_editable(&self, editing: bool) { + self.imp().headerbar.set_editable(editing); + } + + fn set_info(&self, playlist: &str, owner: &str) { + self.imp().header_widget.set_info(playlist, owner); + self.imp().header_mobile.set_info(playlist, owner); + self.imp().headerbar.set_title(Some(playlist)); + } + + fn set_playing(&self, is_playing: bool) { + self.imp().header_widget.set_playing(is_playing); + self.imp().header_mobile.set_playing(is_playing); + } + + fn set_artwork(&self, art: &gdk_pixbuf::Pixbuf) { + self.imp().header_widget.set_artwork(art); + self.imp().header_mobile.set_artwork(art); + } + + fn connect_owner_clicked(&self, f: F) + where + F: Fn() + Clone + 'static, + { + self.imp().header_widget.connect_owner_clicked(f.clone()); + self.imp().header_mobile.connect_owner_clicked(f); + } + + pub fn connect_edit(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().headerbar.connect_edit(f); + } + + pub fn connect_cancel(&self, f: F) + where + F: Fn() + 'static, + { + self.imp() + .headerbar + .connect_cancel(clone!(@weak self as _self => move || { + _self.imp().header_widget.reset_playlist_name(); + _self.imp().header_mobile.reset_playlist_name(); + f(); + })); + } + + pub fn connect_play(&self, f: F) + where + F: Fn() + Clone + 'static, + { + self.imp().header_widget.connect_play(f.clone()); + self.imp().header_mobile.connect_play(f); + } + + pub fn connect_done(&self, f: F) + where + F: Fn(String) + 'static, + { + self.imp() + .headerbar + .connect_ok(clone!(@weak self as _self => move || { + let s = _self.imp().header_widget.get_edited_playlist_name(); + f(s); + })); + } + + pub fn connect_go_back(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().headerbar.connect_go_back(f); + } +} + +pub struct PlaylistDetails { + model: Rc, + worker: Worker, + widget: PlaylistDetailsWidget, + children: Vec>, +} + +impl PlaylistDetails { + pub fn new(model: Rc, worker: Worker) -> Self { + if model.get_playlist_info().is_none() { + model.load_playlist_info(); + } + + let widget = PlaylistDetailsWidget::new(); + let playlist = Box::new(Playlist::new( + widget.playlist_tracks_widget().clone(), + model.clone(), + worker.clone(), + )); + + widget.set_editable(model.is_playlist_editable()); + + widget.connect_header(); + + widget.connect_bottom_edge(clone!(@weak model => move || { + model.load_more_tracks(); + })); + + widget.connect_owner_clicked(clone!(@weak model => move || model.view_owner())); + + widget.connect_edit(clone!(@weak model => move || { + model.enable_selection(); + })); + + widget.connect_cancel(clone!(@weak model => move || model.disable_selection())); + widget.connect_done(clone!(@weak model => move |n| { + model.disable_selection(); + model.update_playlist_details(n); + })); + + widget.connect_play(clone!(@weak model => move || model.toggle_play_playlist())); + + widget.connect_go_back(clone!(@weak model => move || model.go_back())); + + Self { + model, + worker, + widget, + children: vec![playlist], + } + } + + fn update_details(&self) { + if let Some(info) = self.model.get_playlist_info() { + let title = &info.title[..]; + let owner = &info.owner.display_name[..]; + let art_url = info.art.as_ref(); + + self.widget.set_info(title, owner); + + if let Some(art_url) = art_url.cloned() { + let widget = self.widget.downgrade(); + self.worker.send_local_task(async move { + let pixbuf = ImageLoader::new() + .load_remote(&art_url[..], "jpg", 320, 320) + .await; + if let (Some(widget), Some(ref pixbuf)) = (widget.upgrade(), pixbuf) { + widget.set_artwork(pixbuf); + widget.set_loaded(); + } + }); + } else { + self.widget.set_loaded(); + } + } + } + + fn update_playing(&self, is_playing: bool) { + if !self.model.playlist_is_playing() || !self.model.is_playing() { + self.widget.set_playing(false); + return; + } + self.widget.set_playing(is_playing); + } + + fn set_editing(&self, editable: bool) { + if !self.model.is_playlist_editable() { + return; + } + self.widget.set_editing(editable); + } +} + +impl Component for PlaylistDetails { + fn get_root_widget(&self) -> >k::Widget { + self.widget.upcast_ref() + } + + fn get_children(&mut self) -> Option<&mut Vec>> { + Some(&mut self.children) + } +} + +impl EventListener for PlaylistDetails { + fn on_event(&mut self, event: &AppEvent) { + match event { + AppEvent::BrowserEvent(BrowserEvent::PlaylistDetailsLoaded(id)) + if id == &self.model.id => + { + self.update_details(); + self.update_playing(true); + } + AppEvent::SelectionEvent(SelectionEvent::SelectionModeChanged(editing)) => { + self.set_editing(*editing); + } + AppEvent::PlaybackEvent(PlaybackEvent::PlaybackPaused) => { + self.update_playing(false); + } + AppEvent::PlaybackEvent(PlaybackEvent::PlaybackResumed) => { + self.update_playing(true); + } + _ => {} + } + self.broadcast_event(event); + } +} diff --git a/src/app/components/playlist_details/playlist_details_model.rs b/src/app/components/playlist_details/playlist_details_model.rs new file mode 100644 index 0000000..231ced9 --- /dev/null +++ b/src/app/components/playlist_details/playlist_details_model.rs @@ -0,0 +1,242 @@ +use gio::prelude::*; +use gio::SimpleActionGroup; +use std::cell::Ref; +use std::ops::Deref; +use std::rc::Rc; + +use crate::api::SpotifyApiError; +use crate::app::components::{labels, PlaylistModel}; +use crate::app::models::*; +use crate::app::state::SelectionContext; +use crate::app::state::{BrowserAction, PlaybackAction, SelectionAction, SelectionState}; +use crate::app::AppState; +use crate::app::{ActionDispatcher, AppAction, AppModel, BatchQuery, SongsSource}; + +pub struct PlaylistDetailsModel { + pub id: String, + app_model: Rc, + dispatcher: Box, +} + +impl PlaylistDetailsModel { + pub fn new(id: String, app_model: Rc, dispatcher: Box) -> Self { + Self { + id, + app_model, + dispatcher, + } + } + + pub fn state(&self) -> Ref<'_, AppState> { + self.app_model.get_state() + } + + pub fn is_playlist_editable(&self) -> bool { + let state = self.app_model.get_state(); + state.logged_user.playlists.iter().any(|p| p.id == self.id) + } + + pub fn get_playlist_info(&self) -> Option + '_> { + self.app_model.map_state_opt(|s| { + s.browser + .playlist_details_state(&self.id)? + .playlist + .as_ref() + }) + } + + pub fn is_playing(&self) -> bool { + self.state().playback.is_playing() + } + + pub fn playlist_is_playing(&self) -> bool { + matches!( + self.app_model.get_state().playback.current_source(), + Some(SongsSource::Playlist(ref id)) if id == &self.id) + } + + pub fn toggle_play_playlist(&self) { + if let Some(playlist) = self.get_playlist_info() { + if !self.playlist_is_playing() { + if self.state().playback.is_shuffled() { + self.dispatcher + .dispatch(AppAction::PlaybackAction(PlaybackAction::ToggleShuffle)); + } + let id_of_first_song = playlist.songs.songs[0].id.as_str(); + self.play_song_at(0, id_of_first_song); + return; + } + if self.state().playback.is_playing() { + self.dispatcher + .dispatch(AppAction::PlaybackAction(PlaybackAction::Pause)); + } else { + self.dispatcher + .dispatch(AppAction::PlaybackAction(PlaybackAction::Play)); + } + } + } + + pub fn load_playlist_info(&self) { + let api = self.app_model.get_spotify(); + let id = self.id.clone(); + self.dispatcher + .call_spotify_and_dispatch(move || async move { + let playlist = api.get_playlist(&id).await; + match playlist { + Ok(playlist) => { + Ok(BrowserAction::SetPlaylistDetails(Box::new(playlist)).into()) + } + Err(SpotifyApiError::BadStatus(400, _)) + | Err(SpotifyApiError::BadStatus(404, _)) => { + Ok(BrowserAction::NavigationPop.into()) + } + Err(e) => Err(e), + } + }); + } + + pub fn load_more_tracks(&self) -> Option<()> { + let last_batch = self.song_list_model().last_batch()?; + let query = BatchQuery { + source: SongsSource::Playlist(self.id.clone()), + batch: last_batch, + }; + + let id = self.id.clone(); + let next_query = query.next()?; + debug!("next_query = {:?}", &next_query); + let loader = self.app_model.get_batch_loader(); + + self.dispatcher.dispatch_async(Box::pin(async move { + loader + .query(next_query, |_s, song_batch| { + BrowserAction::AppendPlaylistTracks(id, Box::new(song_batch)).into() + }) + .await + })); + + Some(()) + } + + pub fn update_playlist_details(&self, title: String) { + let api = self.app_model.get_spotify(); + let id = self.id.clone(); + self.dispatcher + .call_spotify_and_dispatch(move || async move { + let playlist = api.update_playlist_details(&id, title.clone()).await; + match playlist { + Ok(_) => Ok(AppAction::UpdatePlaylistName(PlaylistSummary { id, title })), + Err(e) => Err(e), + } + }); + } + + pub fn view_owner(&self) { + if let Some(playlist) = self.get_playlist_info() { + let owner = &playlist.owner.id; + self.dispatcher + .dispatch(AppAction::ViewUser(owner.to_owned())); + } + } + + pub fn disable_selection(&self) { + self.dispatcher.dispatch(AppAction::CancelSelection); + } + + pub fn go_back(&self) { + self.dispatcher + .dispatch(BrowserAction::NavigationPop.into()); + } +} + +impl PlaylistModel for PlaylistDetailsModel { + fn song_list_model(&self) -> SongListModel { + self.state() + .browser + .playlist_details_state(&self.id) + .expect("illegal attempt to read playlist_details_state") + .songs + .clone() + } + + fn is_paused(&self) -> bool { + !self.state().playback.is_playing() + } + + fn current_song_id(&self) -> Option { + self.state().playback.current_song_id() + } + + fn play_song_at(&self, pos: usize, id: &str) { + let source = SongsSource::Playlist(self.id.clone()); + let batch = self.song_list_model().song_batch_for(pos); + if let Some(batch) = batch { + self.dispatcher + .dispatch(PlaybackAction::LoadPagedSongs(source, batch).into()); + self.dispatcher + .dispatch(PlaybackAction::Load(id.to_string()).into()); + } + } + + fn actions_for(&self, id: &str) -> Option { + let song = self.song_list_model().get(id)?; + let song = song.description(); + + let group = SimpleActionGroup::new(); + + for view_artist in song.make_artist_actions(self.dispatcher.box_clone(), None) { + group.add_action(&view_artist); + } + group.add_action(&song.make_album_action(self.dispatcher.box_clone(), None)); + group.add_action(&song.make_link_action(None)); + group.add_action(&song.make_queue_action(self.dispatcher.box_clone(), None)); + + Some(group.upcast()) + } + + fn menu_for(&self, id: &str) -> Option { + let song = self.song_list_model().get(id)?; + let song = song.description(); + + let menu = gio::Menu::new(); + menu.append(Some(&*labels::VIEW_ALBUM), Some("song.view_album")); + for artist in song.artists.iter() { + menu.append( + Some(&labels::more_from_label(&artist.name)), + Some(&format!("song.view_artist_{}", artist.id)), + ); + } + + menu.append(Some(&*labels::COPY_LINK), Some("song.copy_link")); + menu.append(Some(&*labels::ADD_TO_QUEUE), Some("song.queue")); + + Some(menu.upcast()) + } + + fn select_song(&self, id: &str) { + let song = self.song_list_model().get(id); + if let Some(song) = song { + self.dispatcher + .dispatch(SelectionAction::Select(vec![song.into_description()]).into()); + } + } + + fn deselect_song(&self, id: &str) { + self.dispatcher + .dispatch(SelectionAction::Deselect(vec![id.to_string()]).into()); + } + + fn enable_selection(&self) -> bool { + self.dispatcher + .dispatch(AppAction::EnableSelection(if self.is_playlist_editable() { + SelectionContext::EditablePlaylist(self.id.clone()) + } else { + SelectionContext::Playlist + })); + true + } + + fn selection(&self) -> Option + '_>> { + Some(Box::new(self.app_model.map_state(|s| &s.selection))) + } +} diff --git a/src/app/components/playlist_details/playlist_header.blp b/src/app/components/playlist_details/playlist_header.blp new file mode 100644 index 0000000..4c89453 --- /dev/null +++ b/src/app/components/playlist_details/playlist_header.blp @@ -0,0 +1,84 @@ +using Gtk 4.0; + +template $PlaylistHeaderWidget : Box { + valign: start; + vexpand: false; + margin-start: 6; + margin-end: 6; + margin-bottom: 6; + + Box playlist_image_box { + overflow: hidden; + halign: center; + margin-top: 18; + margin-start: 6; + margin-bottom: 6; + + Image playlist_art { + width-request: 160; + height-request: 160; + icon-name: "emblem-music-symbolic"; + } + + styles [ + "card", + ] + } + + Box playlist_info { + hexpand: true; + valign: center; + orientation: vertical; + spacing: 6; + margin-start: 18; + + Entry playlist_label_entry { + hexpand: false; + halign: start; + editable: false; + can-focus: false; + placeholder-text: "Playlist Title"; + + styles [ + "title-1", + "playlist__title-entry", + "playlist__title-entry--ro", + ] + } + + LinkButton author_button { + receives-default: true; + halign: start; + valign: center; + has-frame: false; + + Label author_button_label { + hexpand: true; + vexpand: true; + label: "Artist"; + ellipsize: middle; + } + + styles [ + "title-4", + ] + } + } + Button play_button { + margin-end: 6; + receives-default: true; + halign: center; + valign: center; + tooltip-text: "Play"; + icon-name: "media-playback-start-symbolic"; + + styles [ + "circular", + "play__button", + ] + } + + styles [ + "playlist__header", + ] +} diff --git a/src/app/components/playlist_details/playlist_header.css b/src/app/components/playlist_details/playlist_header.css new file mode 100644 index 0000000..ecd533f --- /dev/null +++ b/src/app/components/playlist_details/playlist_header.css @@ -0,0 +1,32 @@ +.playlist__header .title-4 label { + color: @window_fg_color; + font-weight: bold; + text-decoration: none; +} + +.playlist__header .title-4:hover { + border-radius: 6px; + background-image: image(alpha(currentColor, 0.08)); +} + +clamp.playlist_details__clamp { + background-color: @view_bg_color; + box-shadow: inset 0px -1px 0px @borders; +} + +headerbar.playlist_details__headerbar { + transition: background-color .3s ease; +} + +headerbar.flat.playlist_details__headerbar windowtitle { + opacity: 0; +} + +headerbar.playlist_details__headerbar windowtitle { + transition: opacity .3s ease; + opacity: 1; +} + +.playlist_details__headerbar.flat { + background-color: @view_bg_color; +} diff --git a/src/app/components/playlist_details/playlist_header.rs b/src/app/components/playlist_details/playlist_header.rs new file mode 100644 index 0000000..4db7e79 --- /dev/null +++ b/src/app/components/playlist_details/playlist_header.rs @@ -0,0 +1,190 @@ +use crate::app::components::display_add_css_provider; +use gettextrs::gettext; +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::{glib, CompositeTemplate}; + +const CSS_RO_ENTRY: &str = "playlist__title-entry--ro"; + +mod imp { + + use glib::{ParamSpec, Properties}; + use std::cell::RefCell; + + use super::*; + + #[derive(Debug, Default, CompositeTemplate, Properties)] + #[template(resource = "/dev/alextren/Spot/components/playlist_header.ui")] + #[properties(wrapper_type = super::PlaylistHeaderWidget)] + pub struct PlaylistHeaderWidget { + #[template_child] + pub playlist_label_entry: TemplateChild, + + #[template_child] + pub playlist_image_box: TemplateChild, + + #[template_child] + pub playlist_art: TemplateChild, + + #[template_child] + pub playlist_info: TemplateChild, + + #[template_child] + pub author_button: TemplateChild, + + #[template_child] + pub author_button_label: TemplateChild, + + #[template_child] + pub play_button: TemplateChild, + + #[property(get, set, name = "original-entry-text")] + pub original_entry_text: RefCell, + } + + #[glib::object_subclass] + impl ObjectSubclass for PlaylistHeaderWidget { + const NAME: &'static str = "PlaylistHeaderWidget"; + type Type = super::PlaylistHeaderWidget; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + display_add_css_provider(resource!("/components/playlist_header.css")); + obj.init_template(); + } + } + + impl ObjectImpl for PlaylistHeaderWidget { + fn properties() -> &'static [ParamSpec] { + Self::derived_properties() + } + + fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { + self.derived_set_property(id, value, pspec); + } + + fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { + self.derived_property(id, pspec) + } + + fn constructed(&self) { + self.parent_constructed(); + } + } + + impl WidgetImpl for PlaylistHeaderWidget {} + impl BoxImpl for PlaylistHeaderWidget {} +} + +glib::wrapper! { + pub struct PlaylistHeaderWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; +} + +impl Default for PlaylistHeaderWidget { + fn default() -> Self { + Self::new() + } +} + +impl PlaylistHeaderWidget { + pub fn new() -> Self { + glib::Object::new() + } + + pub fn connect_owner_clicked(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().author_button.connect_activate_link(move |_| { + f(); + glib::signal::Inhibit(true) + }); + } + + pub fn connect_play(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().play_button.connect_clicked(move |_| f()); + } + + pub fn reset_playlist_name(&self) { + self.imp() + .playlist_label_entry + .set_text(&self.original_entry_text()); + } + + pub fn get_edited_playlist_name(&self) -> String { + self.imp().playlist_label_entry.text().to_string() + } + + pub fn set_artwork(&self, art: &gdk_pixbuf::Pixbuf) { + self.imp().playlist_art.set_from_pixbuf(Some(art)); + } + + pub fn set_info(&self, playlist: &str, owner: &str) { + let widget = self.imp(); + self.set_original_entry_text(playlist); + widget.playlist_label_entry.set_text(playlist); + widget + .playlist_label_entry + .set_placeholder_text(Some(playlist)); + widget.author_button_label.set_label(owner); + } + + pub fn set_playing(&self, is_playing: bool) { + let playback_icon = if is_playing { + "media-playback-pause-symbolic" + } else { + "media-playback-start-symbolic" + }; + + let translated_tooltip = if is_playing { + gettext("Pause") + } else { + gettext("Play") + }; + let tooltip_text = Some(translated_tooltip.as_str()); + + self.imp().play_button.set_icon_name(playback_icon); + self.imp().play_button.set_tooltip_text(tooltip_text); + } + + pub fn set_centered(&self) { + let widget = self.imp(); + widget.playlist_info.set_halign(gtk::Align::Center); + widget.play_button.set_margin_end(0); + widget.playlist_info.set_margin_start(0); + widget.playlist_image_box.set_margin_start(0); + widget.playlist_label_entry.set_xalign(0.5); + widget.author_button.set_halign(gtk::Align::Center); + } + + pub fn set_editing(&self, editing: bool) { + let widget = self.imp(); + widget.playlist_label_entry.set_can_focus(editing); + widget.playlist_label_entry.set_editable(editing); + if editing { + widget.playlist_label_entry.remove_css_class(CSS_RO_ENTRY); + } else { + widget.playlist_label_entry.add_css_class(CSS_RO_ENTRY); + } + } + + pub fn entry(&self) -> >k::Entry { + self.imp().playlist_label_entry.as_ref() + } + + pub fn set_grows_automatically(&self) { + let entry: >k::Entry = &self.imp().playlist_label_entry; + entry + .bind_property("text", entry, "width-chars") + .transform_to(|_, text: &str| Some(text.len() as i32)) + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(); + } +} diff --git a/src/app/components/playlist_details/playlist_headerbar.blp b/src/app/components/playlist_details/playlist_headerbar.blp new file mode 100644 index 0000000..9a94068 --- /dev/null +++ b/src/app/components/playlist_details/playlist_headerbar.blp @@ -0,0 +1,77 @@ +using Gtk 4.0; +using Adw 1; + +template $PlaylistHeaderBarWidget : Adw.Bin { + [root] + Overlay overlay { + hexpand: true; + + Adw.HeaderBar main_header { + show-end-title-buttons: true; + + Button go_back { + receives-default: true; + halign: start; + valign: center; + icon-name: "go-previous-symbolic"; + has-frame: false; + } + + [title] + Adw.WindowTitle title { + visible: false; + title: "Spot"; + } + + [end] + Button edit { + icon-name: "document-edit-symbolic"; + } + + styles [ + "playlist_details__headerbar", + ] + } + + [overlay] + Adw.HeaderBar edition_header { + show-end-title-buttons: false; + show-start-title-buttons: false; + visible: false; + + styles [ + "selection-mode", + ] + + Button cancel { + receives-default: true; + halign: start; + valign: center; + + /* Translators: Exit playlist edition */ + + label: _("Cancel"); + } + + [title] + Separator { + styles [ + "spacer", + ] + } + + [end] + Button ok { + valign: center; + + /* Translators: Finish playlist edition */ + + label: _("Done"); + + styles [ + "suggested-action", + ] + } + } + } +} diff --git a/src/app/components/playlist_details/playlist_headerbar.rs b/src/app/components/playlist_details/playlist_headerbar.rs new file mode 100644 index 0000000..9e6fd49 --- /dev/null +++ b/src/app/components/playlist_details/playlist_headerbar.rs @@ -0,0 +1,164 @@ +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; +use libadwaita::subclass::prelude::BinImpl; + +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/playlist_headerbar.ui")] + pub struct PlaylistHeaderBarWidget { + #[template_child] + pub main_header: TemplateChild, + + #[template_child] + pub edition_header: TemplateChild, + + #[template_child] + pub go_back: TemplateChild, + + #[template_child] + pub title: TemplateChild, + + #[template_child] + pub edit: TemplateChild, + + #[template_child] + pub ok: TemplateChild, + + #[template_child] + pub cancel: TemplateChild, + + #[template_child] + pub overlay: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for PlaylistHeaderBarWidget { + const NAME: &'static str = "PlaylistHeaderBarWidget"; + type Type = super::PlaylistHeaderBarWidget; + type ParentType = libadwaita::Bin; + type Interfaces = (gtk::Buildable,); + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for PlaylistHeaderBarWidget {} + + impl BuildableImpl for PlaylistHeaderBarWidget { + fn add_child(&self, builder: >k::Builder, child: &glib::Object, type_: Option<&str>) { + if Some("root") == type_ { + self.parent_add_child(builder, child, type_); + } else { + self.main_header + .set_title_widget(child.downcast_ref::()); + } + } + } + + impl WidgetImpl for PlaylistHeaderBarWidget {} + impl BinImpl for PlaylistHeaderBarWidget {} + impl WindowImpl for PlaylistHeaderBarWidget {} +} + +glib::wrapper! { + pub struct PlaylistHeaderBarWidget(ObjectSubclass) @extends gtk::Widget, libadwaita::Bin; +} + +impl Default for PlaylistHeaderBarWidget { + fn default() -> Self { + Self::new() + } +} + +impl PlaylistHeaderBarWidget { + pub fn new() -> Self { + glib::Object::new() + } + + pub fn connect_edit(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().edit.connect_clicked(move |_| f()); + } + + pub fn connect_ok(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().ok.connect_clicked(move |_| f()); + } + + pub fn connect_cancel(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().cancel.connect_clicked(move |_| f()); + } + + pub fn connect_go_back(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().go_back.connect_clicked(move |_| f()); + } + + pub fn bind_to_leaflet(&self, leaflet: &libadwaita::Leaflet) { + leaflet + .bind_property( + "folded", + &*self.imp().main_header, + "show-start-title-buttons", + ) + .build(); + leaflet.notify("folded"); + } + + pub fn set_can_go_back(&self, can_go_back: bool) { + self.imp().go_back.set_visible(can_go_back); + } + + pub fn set_editable(&self, editable: bool) { + self.imp().edit.set_visible(editable); + } + + pub fn set_editing(&self, editing: bool) { + if editing { + self.imp().edition_header.set_visible(true); + } else { + self.imp().edition_header.set_visible(false); + } + } + + pub fn add_classes(&self, classes: &[&str]) { + for &class in classes { + self.add_css_class(class); + } + } + + pub fn remove_classes(&self, classes: &[&str]) { + for &class in classes { + self.remove_css_class(class); + } + } + + pub fn set_title_visible(&self, visible: bool) { + self.imp().title.set_visible(visible); + } + + pub fn set_title(&self, title: Option<&str>) { + self.imp().title.set_visible(title.is_some()); + if let Some(title) = title { + self.imp().title.set_title(title); + } + } +} diff --git a/src/app/components/saved_playlists/mod.rs b/src/app/components/saved_playlists/mod.rs new file mode 100644 index 0000000..771e4e1 --- /dev/null +++ b/src/app/components/saved_playlists/mod.rs @@ -0,0 +1,6 @@ +#[allow(clippy::module_inception)] +mod saved_playlists; +mod saved_playlists_model; + +pub use saved_playlists::*; +pub use saved_playlists_model::*; diff --git a/src/app/components/saved_playlists/saved_playlists.blp b/src/app/components/saved_playlists/saved_playlists.blp new file mode 100644 index 0000000..8f6ba12 --- /dev/null +++ b/src/app/components/saved_playlists/saved_playlists.blp @@ -0,0 +1,36 @@ +using Gtk 4.0; +using Adw 1; + +template $SavedPlaylistsWidget : Box { + ScrolledWindow scrolled_window { + hexpand: true; + vexpand: true; + vscrollbar-policy: always; + min-content-width: 250; + + Overlay overlay { + FlowBox flowbox { + margin-start: 8; + margin-end: 8; + margin-top: 8; + margin-bottom: 8; + min-children-per-line: 1; + selection-mode: none; + activate-on-single-click: false; + } + + [overlay] + Adw.StatusPage status_page { + /* Translators: A title that is shown when the user has not saved any playlists. */ + + title: _("You have no saved playlists."); + + /* Translators: A description of what happens when the user has saved playlists. */ + + description: _("Your playlists will be shown here."); + icon-name: "emblem-music-symbolic"; + visible: true; + } + } + } +} diff --git a/src/app/components/saved_playlists/saved_playlists.rs b/src/app/components/saved_playlists/saved_playlists.rs new file mode 100644 index 0000000..fd4aa58 --- /dev/null +++ b/src/app/components/saved_playlists/saved_playlists.rs @@ -0,0 +1,160 @@ +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; +use std::rc::Rc; + +use super::SavedPlaylistsModel; +use crate::app::components::{AlbumWidget, Component, EventListener}; +use crate::app::dispatch::Worker; +use crate::app::models::AlbumModel; +use crate::app::state::LoginEvent; +use crate::app::{AppEvent, BrowserEvent, ListStore}; + +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/saved_playlists.ui")] + pub struct SavedPlaylistsWidget { + #[template_child] + pub scrolled_window: TemplateChild, + + #[template_child] + pub flowbox: TemplateChild, + #[template_child] + pub status_page: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for SavedPlaylistsWidget { + const NAME: &'static str = "SavedPlaylistsWidget"; + type Type = super::SavedPlaylistsWidget; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for SavedPlaylistsWidget {} + impl WidgetImpl for SavedPlaylistsWidget {} + impl BoxImpl for SavedPlaylistsWidget {} +} + +glib::wrapper! { + pub struct SavedPlaylistsWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; +} + +impl Default for SavedPlaylistsWidget { + fn default() -> Self { + Self::new() + } +} + +impl SavedPlaylistsWidget { + pub fn new() -> Self { + glib::Object::new() + } + + fn connect_bottom_edge(&self, f: F) + where + F: Fn() + 'static, + { + self.imp() + .scrolled_window + .connect_edge_reached(move |_, pos| { + if let gtk::PositionType::Bottom = pos { + f() + } + }); + } + + fn bind_albums(&self, worker: Worker, store: &ListStore, on_album_pressed: F) + where + F: Fn(String) + Clone + 'static, + { + self.imp() + .flowbox + .bind_model(Some(store.unsafe_store()), move |item| { + let album_model = item.downcast_ref::().unwrap(); + let child = gtk::FlowBoxChild::new(); + let album = AlbumWidget::for_model(album_model, worker.clone()); + + let f = on_album_pressed.clone(); + album.connect_album_pressed(clone!(@weak album_model => move |_| { + f(album_model.uri()); + })); + + child.set_child(Some(&album)); + child.upcast::() + }); + } + pub fn get_status_page(&self) -> &libadwaita::StatusPage { + &self.imp().status_page + } +} + +pub struct SavedPlaylists { + widget: SavedPlaylistsWidget, + worker: Worker, + model: Rc, +} + +impl SavedPlaylists { + pub fn new(worker: Worker, model: SavedPlaylistsModel) -> Self { + let model = Rc::new(model); + + let widget = SavedPlaylistsWidget::new(); + + widget.connect_bottom_edge(clone!(@weak model => move || { + model.load_more_playlists(); + })); + + Self { + widget, + worker, + model, + } + } + + fn bind_flowbox(&self) { + self.widget.bind_albums( + self.worker.clone(), + &self.model.get_list_store().unwrap(), + clone!(@weak self.model as model => move |id| { + model.open_playlist(id); + }), + ); + } +} + +impl EventListener for SavedPlaylists { + fn on_event(&mut self, event: &AppEvent) { + match event { + AppEvent::Started => { + let _ = self.model.refresh_saved_playlists(); + self.bind_flowbox(); + } + AppEvent::LoginEvent(LoginEvent::LoginCompleted(_)) => { + let _ = self.model.refresh_saved_playlists(); + } + AppEvent::BrowserEvent(BrowserEvent::SavedPlaylistsUpdated) => { + self.widget + .get_status_page() + .set_visible(!self.model.has_playlists()); + } + _ => {} + } + } +} + +impl Component for SavedPlaylists { + fn get_root_widget(&self) -> >k::Widget { + self.widget.as_ref() + } +} diff --git a/src/app/components/saved_playlists/saved_playlists_model.rs b/src/app/components/saved_playlists/saved_playlists_model.rs new file mode 100644 index 0000000..062aadf --- /dev/null +++ b/src/app/components/saved_playlists/saved_playlists_model.rs @@ -0,0 +1,70 @@ +use std::cell::Ref; +use std::ops::Deref; +use std::rc::Rc; + +use crate::app::models::*; +use crate::app::state::HomeState; +use crate::app::{ActionDispatcher, AppAction, AppModel, BrowserAction, ListStore}; + +pub struct SavedPlaylistsModel { + app_model: Rc, + dispatcher: Box, +} + +impl SavedPlaylistsModel { + pub fn new(app_model: Rc, dispatcher: Box) -> Self { + Self { + app_model, + dispatcher, + } + } + + fn state(&self) -> Option> { + self.app_model.map_state_opt(|s| s.browser.home_state()) + } + + pub fn get_list_store(&self) -> Option> + '_> { + Some(Ref::map(self.state()?, |s| &s.playlists)) + } + + pub fn refresh_saved_playlists(&self) -> Option<()> { + let api = self.app_model.get_spotify(); + let batch_size = self.state()?.next_playlists_page.batch_size; + + self.dispatcher + .call_spotify_and_dispatch(move || async move { + api.get_saved_playlists(0, batch_size) + .await + .map(|playlists| BrowserAction::SetPlaylistsContent(playlists).into()) + }); + + Some(()) + } + + pub fn has_playlists(&self) -> bool { + self.get_list_store() + .map(|list| list.len() > 0) + .unwrap_or(false) + } + + pub fn load_more_playlists(&self) -> Option<()> { + let api = self.app_model.get_spotify(); + + let next_page = &self.state()?.next_playlists_page; + let batch_size = next_page.batch_size; + let offset = next_page.next_offset?; + + self.dispatcher + .call_spotify_and_dispatch(move || async move { + api.get_saved_playlists(offset, batch_size) + .await + .map(|playlists| BrowserAction::AppendPlaylistsContent(playlists).into()) + }); + + Some(()) + } + + pub fn open_playlist(&self, id: String) { + self.dispatcher.dispatch(AppAction::ViewPlaylist(id)); + } +} diff --git a/src/app/components/saved_tracks/mod.rs b/src/app/components/saved_tracks/mod.rs new file mode 100644 index 0000000..7fb2813 --- /dev/null +++ b/src/app/components/saved_tracks/mod.rs @@ -0,0 +1,6 @@ +#[allow(clippy::module_inception)] +mod saved_tracks; +pub use saved_tracks::*; + +mod saved_tracks_model; +pub use saved_tracks_model::*; diff --git a/src/app/components/saved_tracks/saved_tracks.blp b/src/app/components/saved_tracks/saved_tracks.blp new file mode 100644 index 0000000..c597279 --- /dev/null +++ b/src/app/components/saved_tracks/saved_tracks.blp @@ -0,0 +1,15 @@ +using Gtk 4.0; +using Adw 1; + +template $SavedTracksWidget : Adw.Bin { + ScrolledWindow scrolled_window { + vexpand: true; + + Adw.ClampScrollable { + maximum-size: 900; + + ListView song_list { + } + } + } +} diff --git a/src/app/components/saved_tracks/saved_tracks.rs b/src/app/components/saved_tracks/saved_tracks.rs new file mode 100644 index 0000000..715ac28 --- /dev/null +++ b/src/app/components/saved_tracks/saved_tracks.rs @@ -0,0 +1,117 @@ +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; +use std::rc::Rc; + +use super::SavedTracksModel; +use crate::app::components::{Component, EventListener, Playlist}; +use crate::app::state::LoginEvent; +use crate::app::{AppEvent, Worker}; +use libadwaita::subclass::prelude::BinImpl; + +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/saved_tracks.ui")] + pub struct SavedTracksWidget { + #[template_child] + pub song_list: TemplateChild, + + #[template_child] + pub scrolled_window: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for SavedTracksWidget { + const NAME: &'static str = "SavedTracksWidget"; + type Type = super::SavedTracksWidget; + type ParentType = libadwaita::Bin; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for SavedTracksWidget {} + impl WidgetImpl for SavedTracksWidget {} + impl BinImpl for SavedTracksWidget {} +} + +glib::wrapper! { + pub struct SavedTracksWidget(ObjectSubclass) @extends gtk::Widget, libadwaita::Bin; +} + +impl SavedTracksWidget { + fn new() -> Self { + glib::Object::new() + } + + fn connect_bottom_edge(&self, f: F) + where + F: Fn() + 'static, + { + self.imp() + .scrolled_window + .connect_edge_reached(move |_, pos| { + if let gtk::PositionType::Bottom = pos { + f() + } + }); + } + + fn song_list_widget(&self) -> >k::ListView { + self.imp().song_list.as_ref() + } +} + +pub struct SavedTracks { + widget: SavedTracksWidget, + model: Rc, + children: Vec>, +} + +impl SavedTracks { + pub fn new(model: Rc, worker: Worker) -> Self { + let widget = SavedTracksWidget::new(); + + widget.connect_bottom_edge(clone!(@weak model => move || { + model.load_more(); + })); + + let playlist = Playlist::new(widget.song_list_widget().clone(), model.clone(), worker); + + Self { + widget, + model, + children: vec![Box::new(playlist)], + } + } +} + +impl Component for SavedTracks { + fn get_root_widget(&self) -> >k::Widget { + self.widget.upcast_ref() + } + + fn get_children(&mut self) -> Option<&mut Vec>> { + Some(&mut self.children) + } +} + +impl EventListener for SavedTracks { + fn on_event(&mut self, event: &AppEvent) { + match event { + AppEvent::Started | AppEvent::LoginEvent(LoginEvent::LoginCompleted(_)) => { + self.model.load_initial(); + } + _ => {} + } + self.broadcast_event(event); + } +} diff --git a/src/app/components/saved_tracks/saved_tracks_model.rs b/src/app/components/saved_tracks/saved_tracks_model.rs new file mode 100644 index 0000000..7d0b0a2 --- /dev/null +++ b/src/app/components/saved_tracks/saved_tracks_model.rs @@ -0,0 +1,147 @@ +use gio::prelude::*; +use gio::SimpleActionGroup; +use std::ops::Deref; +use std::rc::Rc; + +use crate::app::components::{labels, PlaylistModel}; +use crate::app::models::*; +use crate::app::state::SelectionContext; +use crate::app::state::{PlaybackAction, SelectionAction, SelectionState}; +use crate::app::{ActionDispatcher, AppAction, AppModel, BatchQuery, BrowserAction, SongsSource}; + +pub struct SavedTracksModel { + app_model: Rc, + dispatcher: Box, +} + +impl SavedTracksModel { + pub fn new(app_model: Rc, dispatcher: Box) -> Self { + Self { + app_model, + dispatcher, + } + } + + pub fn load_initial(&self) { + let loader = self.app_model.get_batch_loader(); + let query = BatchQuery { + source: SongsSource::SavedTracks, + batch: Batch::first_of_size(50), + }; + self.dispatcher.dispatch_async(Box::pin(async move { + loader + .query(query, |_s, song_batch| { + BrowserAction::SetSavedTracks(Box::new(song_batch)).into() + }) + .await + })); + } + + pub fn load_more(&self) -> Option<()> { + let loader = self.app_model.get_batch_loader(); + let last_batch = self.song_list_model().last_batch()?.next()?; + let query = BatchQuery { + source: SongsSource::SavedTracks, + batch: last_batch, + }; + self.dispatcher.dispatch_async(Box::pin(async move { + loader + .query(query, |_s, song_batch| { + BrowserAction::AppendSavedTracks(Box::new(song_batch)).into() + }) + .await + })); + Some(()) + } +} + +impl PlaylistModel for SavedTracksModel { + fn song_list_model(&self) -> SongListModel { + self.app_model + .get_state() + .browser + .home_state() + .expect("illegal attempt to read home_state") + .saved_tracks + .clone() + } + + fn is_paused(&self) -> bool { + !self.app_model.get_state().playback.is_playing() + } + + fn current_song_id(&self) -> Option { + self.app_model.get_state().playback.current_song_id() + } + + fn play_song_at(&self, pos: usize, id: &str) { + let source = SongsSource::SavedTracks; + let batch = self.song_list_model().song_batch_for(pos); + if let Some(batch) = batch { + self.dispatcher + .dispatch(PlaybackAction::LoadPagedSongs(source, batch).into()); + self.dispatcher + .dispatch(PlaybackAction::Load(id.to_string()).into()); + } + } + fn autoscroll_to_playing(&self) -> bool { + true + } + + fn actions_for(&self, id: &str) -> Option { + let song = self.song_list_model().get(id)?; + let song = song.description(); + + let group = SimpleActionGroup::new(); + + for view_artist in song.make_artist_actions(self.dispatcher.box_clone(), None) { + group.add_action(&view_artist); + } + group.add_action(&song.make_album_action(self.dispatcher.box_clone(), None)); + group.add_action(&song.make_link_action(None)); + + Some(group.upcast()) + } + + fn menu_for(&self, id: &str) -> Option { + let song = self.song_list_model().get(id)?; + let song = song.description(); + + let menu = gio::Menu::new(); + menu.append(Some(&*labels::VIEW_ALBUM), Some("song.view_album")); + for artist in song.artists.iter() { + menu.append( + Some(&labels::more_from_label(&artist.name)), + Some(&format!("song.view_artist_{}", artist.id)), + ); + } + + menu.append(Some(&*labels::COPY_LINK), Some("song.copy_link")); + + Some(menu.upcast()) + } + + fn select_song(&self, id: &str) { + let song = self.song_list_model().get(id); + if let Some(song) = song { + self.dispatcher + .dispatch(SelectionAction::Select(vec![song.description().clone()]).into()); + } + } + + fn deselect_song(&self, id: &str) { + self.dispatcher + .dispatch(SelectionAction::Deselect(vec![id.to_string()]).into()); + } + + fn enable_selection(&self) -> bool { + self.dispatcher + .dispatch(AppAction::EnableSelection(SelectionContext::SavedTracks)); + true + } + + fn selection(&self) -> Option + '_>> { + let selection = self.app_model.map_state(|s| &s.selection); + Some(Box::new(selection)) + } +} diff --git a/src/app/components/scrolling_header/mod.rs b/src/app/components/scrolling_header/mod.rs new file mode 100644 index 0000000..1f8c390 --- /dev/null +++ b/src/app/components/scrolling_header/mod.rs @@ -0,0 +1,7 @@ +mod scrolling_header_widget; +use glib::StaticType; +pub use scrolling_header_widget::*; + +pub fn expose_widgets() { + scrolling_header_widget::ScrollingHeaderWidget::static_type(); +} diff --git a/src/app/components/scrolling_header/scrolling_header.blp b/src/app/components/scrolling_header/scrolling_header.blp new file mode 100644 index 0000000..05cfb88 --- /dev/null +++ b/src/app/components/scrolling_header/scrolling_header.blp @@ -0,0 +1,20 @@ +using Gtk 4.0; + +template $ScrollingHeaderWidget : Box { + orientation: vertical; + vexpand: true; + hexpand: true; + + [internal] + Revealer revealer { + transition-type: slide_up; + } + + [internal] + ScrolledWindow scrolled_window { + hscrollbar-policy: never; + propagate-natural-width: true; + hexpand: true; + vexpand: true; + } +} diff --git a/src/app/components/scrolling_header/scrolling_header_widget.rs b/src/app/components/scrolling_header/scrolling_header_widget.rs new file mode 100644 index 0000000..85be005 --- /dev/null +++ b/src/app/components/scrolling_header/scrolling_header_widget.rs @@ -0,0 +1,117 @@ +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; + +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/scrolling_header.ui")] + pub struct ScrollingHeaderWidget { + #[template_child] + pub scrolled_window: TemplateChild, + + #[template_child] + pub revealer: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for ScrollingHeaderWidget { + const NAME: &'static str = "ScrollingHeaderWidget"; + type Type = super::ScrollingHeaderWidget; + type ParentType = gtk::Box; + type Interfaces = (gtk::Buildable,); + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for ScrollingHeaderWidget { + fn constructed(&self) { + self.parent_constructed(); + } + } + + impl BuildableImpl for ScrollingHeaderWidget { + fn add_child(&self, builder: >k::Builder, child: &glib::Object, type_: Option<&str>) { + let child_widget = child.downcast_ref::(); + match type_ { + Some("internal") => self.parent_add_child(builder, child, type_), + Some("header") => self.revealer.set_child(child_widget), + _ => self.scrolled_window.set_child(child_widget), + } + } + } + + impl WidgetImpl for ScrollingHeaderWidget {} + impl BoxImpl for ScrollingHeaderWidget {} +} + +glib::wrapper! { + pub struct ScrollingHeaderWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; +} + +impl ScrollingHeaderWidget { + fn set_header_visible(&self, visible: bool) -> bool { + let widget = self.imp(); + let is_up_to_date = widget.revealer.reveals_child() == visible; + if !is_up_to_date { + widget.revealer.set_reveal_child(visible); + } + is_up_to_date + } + + fn is_scrolled_to_top(&self) -> bool { + self.imp().scrolled_window.vadjustment().value() <= f64::EPSILON + || self.imp().revealer.reveals_child() + } + + pub fn connect_header_visibility(&self, f: F) + where + F: Fn(bool) + Clone + 'static, + { + self.set_header_visible(true); + f(true); + + let scroll_controller = + gtk::EventControllerScroll::new(gtk::EventControllerScrollFlags::VERTICAL); + scroll_controller.connect_scroll( + clone!(@strong f, @weak self as _self => @default-return gtk::Inhibit(false), move |_, _, dy| { + let visible = dy < 0f64 && _self.is_scrolled_to_top(); + f(visible); + gtk::Inhibit(!_self.set_header_visible(visible)) + }), + ); + + let swipe_controller = gtk::GestureSwipe::new(); + swipe_controller.set_touch_only(true); + swipe_controller.set_propagation_phase(gtk::PropagationPhase::Capture); + swipe_controller.connect_swipe(clone!(@weak self as _self => move |_, _, dy| { + let visible = dy >= 0f64 && _self.is_scrolled_to_top(); + f(visible); + _self.set_header_visible(visible); + })); + + self.imp().scrolled_window.add_controller(scroll_controller); + self.add_controller(swipe_controller); + } + + pub fn connect_bottom_edge(&self, f: F) + where + F: Fn() + 'static, + { + self.imp() + .scrolled_window + .connect_edge_reached(move |_, pos| { + if let gtk::PositionType::Bottom = pos { + f() + } + }); + } +} diff --git a/src/app/components/search/mod.rs b/src/app/components/search/mod.rs new file mode 100644 index 0000000..4db3b9a --- /dev/null +++ b/src/app/components/search/mod.rs @@ -0,0 +1,9 @@ +#[allow(clippy::module_inception)] +mod search; +pub use search::*; + +mod search_model; +pub use search_model::*; + +mod search_button; +pub use search_button::*; diff --git a/src/app/components/search/search.blp b/src/app/components/search/search.blp new file mode 100644 index 0000000..9135431 --- /dev/null +++ b/src/app/components/search/search.blp @@ -0,0 +1,119 @@ +using Gtk 4.0; +using Adw 1; + +template $SearchResultsWidget : Box { + orientation: vertical; + can-focus: true; + + Adw.HeaderBar main_header { + show-end-title-buttons: true; + + Button go_back { + halign: start; + valign: center; + icon-name: "go-previous-symbolic"; + has-frame: false; + } + + [title] + SearchEntry search_entry { + receives-default: true; + can-focus: true; + } + } + + Overlay overlay { + hexpand: true; + vexpand: true; + + ScrolledWindow search_results { + visible: false; + hexpand: true; + vexpand: true; + hscrollbar-policy: never; + Box { + vexpand: false; + margin-start: 8; + margin-end: 8; + margin-top: 8; + margin-bottom: 8; + orientation: vertical; + spacing: 8; + + Expander { + margin-start: 4; + margin-end: 4; + expanded: true; + vexpand: false; + valign: start; + + ScrolledWindow { + vscrollbar-policy: never; + propagate-natural-height: false; + FlowBox albums_results { + halign: start; + hexpand: true; + vexpand: false; + valign: start; + orientation: vertical; + max-children-per-line: 1; + selection-mode: none; + activate-on-single-click: false; + } + } + + [label] + Label { + /* Translators: This is the title of a section of the search results */ + + label: _("Albums"); + } + } + + Expander { + margin-start: 4; + margin-end: 4; + margin-bottom: 4; + expanded: true; + vexpand: false; + valign: start; + + ScrolledWindow { + vscrollbar-policy: never; + propagate-natural-height: false; + FlowBox artist_results { + halign: start; + hexpand: true; + vexpand: false; + valign: start; + orientation: vertical; + max-children-per-line: 1; + selection-mode: none; + activate-on-single-click: false; + } + } + + [label] + Label { + /* Translators: This is the title of a section of the search results */ + + label: _("Artists"); + } + } + } + } + + [overlay] + Adw.StatusPage status_page { + /* Translators: Title for the empty search page (initial state). */ + + title: _("Search Spotify."); + + /* Translators: Subtitle for the empty search page (initial state). */ + + description: _("Type to search."); + icon-name: "system-search-symbolic"; + visible: true; + } + } +} diff --git a/src/app/components/search/search.rs b/src/app/components/search/search.rs new file mode 100644 index 0000000..990e4c5 --- /dev/null +++ b/src/app/components/search/search.rs @@ -0,0 +1,257 @@ +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; +use std::rc::Rc; + +use crate::app::components::utils::{wrap_flowbox_item, Debouncer}; +use crate::app::components::{AlbumWidget, ArtistWidget, Component, EventListener}; +use crate::app::dispatch::Worker; +use crate::app::models::{AlbumModel, ArtistModel}; +use crate::app::state::{AppEvent, BrowserEvent}; + +use super::SearchResultsModel; +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/search.ui")] + pub struct SearchResultsWidget { + #[template_child] + pub main_header: TemplateChild, + + #[template_child] + pub go_back: TemplateChild, + + #[template_child] + pub search_entry: TemplateChild, + + #[template_child] + pub status_page: TemplateChild, + + #[template_child] + pub search_results: TemplateChild, + + #[template_child] + pub albums_results: TemplateChild, + + #[template_child] + pub artist_results: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for SearchResultsWidget { + const NAME: &'static str = "SearchResultsWidget"; + type Type = super::SearchResultsWidget; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for SearchResultsWidget {} + impl BoxImpl for SearchResultsWidget {} + + impl WidgetImpl for SearchResultsWidget { + fn grab_focus(&self) -> bool { + self.search_entry.grab_focus() + } + } +} + +glib::wrapper! { + pub struct SearchResultsWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; +} + +impl Default for SearchResultsWidget { + fn default() -> Self { + Self::new() + } +} + +impl SearchResultsWidget { + pub fn new() -> Self { + glib::Object::new() + } + + pub fn bind_to_leaflet(&self, leaflet: &libadwaita::Leaflet) { + leaflet + .bind_property( + "folded", + &*self.imp().main_header, + "show-start-title-buttons", + ) + .build(); + leaflet.notify("folded"); + } + + pub fn connect_go_back(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().go_back.connect_clicked(move |_| f()); + } + + pub fn connect_search_updated(&self, f: F) + where + F: Fn(String) + 'static, + { + self.imp() + .search_entry + .connect_changed(clone!(@weak self as _self => move |s| { + let query = s.text(); + let query = query.as_str(); + _self.imp().status_page.set_visible(query.is_empty()); + _self.imp().search_results.set_visible(!query.is_empty()); + if !query.is_empty() { + f(query.to_string()); + } + })); + } + + fn bind_albums_results(&self, worker: Worker, store: &gio::ListStore, on_album_pressed: F) + where + F: Fn(String) + Clone + 'static, + { + self.imp() + .albums_results + .bind_model(Some(store), move |item| { + wrap_flowbox_item(item, |album_model| { + let f = on_album_pressed.clone(); + let album = AlbumWidget::for_model(album_model, worker.clone()); + album.connect_album_pressed(clone!(@weak album_model => move |_| { + f(album_model.uri()); + })); + album + }) + }); + } + + fn bind_artists_results(&self, worker: Worker, store: &gio::ListStore, on_artist_pressed: F) + where + F: Fn(String) + Clone + 'static, + { + self.imp() + .artist_results + .bind_model(Some(store), move |item| { + wrap_flowbox_item(item, |artist_model| { + let f = on_artist_pressed.clone(); + let artist = ArtistWidget::for_model(artist_model, worker.clone()); + artist.connect_artist_pressed(clone!(@weak artist_model => move |_| { + f(artist_model.id()); + })); + artist + }) + }); + } +} + +pub struct SearchResults { + widget: SearchResultsWidget, + model: Rc, + album_results_model: gio::ListStore, + artist_results_model: gio::ListStore, + debouncer: Debouncer, +} + +impl SearchResults { + pub fn new(model: SearchResultsModel, worker: Worker, leaflet: &libadwaita::Leaflet) -> Self { + let model = Rc::new(model); + let widget = SearchResultsWidget::new(); + + let album_results_model = gio::ListStore::new(AlbumModel::static_type()); + let artist_results_model = gio::ListStore::new(ArtistModel::static_type()); + + widget.bind_to_leaflet(leaflet); + + widget.connect_go_back(clone!(@weak model => move || { + model.go_back(); + })); + + widget.connect_search_updated(clone!(@weak model => move |q| { + model.search(q); + })); + + widget.bind_albums_results( + worker.clone(), + &album_results_model, + clone!(@weak model => move |uri| { + model.open_album(uri); + }), + ); + + widget.bind_artists_results( + worker, + &artist_results_model, + clone!(@weak model => move |id| { + model.open_artist(id); + }), + ); + + Self { + widget, + model, + album_results_model, + artist_results_model, + debouncer: Debouncer::new(), + } + } + + fn update_results(&self) { + if let Some(results) = self.model.get_album_results() { + self.album_results_model.remove_all(); + for album in results.iter() { + self.album_results_model.append(&AlbumModel::new( + &album.artists_name(), + &album.title, + album.year(), + album.art.as_ref(), + &album.id, + )); + } + } + if let Some(results) = self.model.get_artist_results() { + self.artist_results_model.remove_all(); + for artist in results.iter() { + self.artist_results_model.append(&ArtistModel::new( + &artist.name, + &artist.photo, + &artist.id, + )); + } + } + } + + fn update_search_query(&self) { + self.debouncer.debounce( + 600, + clone!(@weak self.model as model => move || model.fetch_results()), + ); + } +} + +impl Component for SearchResults { + fn get_root_widget(&self) -> >k::Widget { + self.widget.as_ref() + } +} + +impl EventListener for SearchResults { + fn on_event(&mut self, app_event: &AppEvent) { + match app_event { + AppEvent::BrowserEvent(BrowserEvent::SearchUpdated) => { + self.get_root_widget().grab_focus(); + self.update_search_query(); + } + AppEvent::BrowserEvent(BrowserEvent::SearchResultsUpdated) => { + self.update_results(); + } + _ => {} + } + } +} diff --git a/src/app/components/search/search_button.rs b/src/app/components/search/search_button.rs new file mode 100644 index 0000000..c320f7a --- /dev/null +++ b/src/app/components/search/search_button.rs @@ -0,0 +1,26 @@ +use gtk::prelude::*; + +use crate::app::components::EventListener; +use crate::app::{ActionDispatcher, AppAction}; + +pub struct SearchBarModel(pub Box); + +impl SearchBarModel { + pub fn navigate_to_search(&self) { + self.0.dispatch(AppAction::ViewSearch()); + } +} + +pub struct SearchButton; + +impl SearchButton { + pub fn new(model: SearchBarModel, search_button: gtk::Button) -> Self { + search_button.connect_clicked(move |_| { + model.navigate_to_search(); + }); + + Self + } +} + +impl EventListener for SearchButton {} diff --git a/src/app/components/search/search_model.rs b/src/app/components/search/search_model.rs new file mode 100644 index 0000000..f6289c6 --- /dev/null +++ b/src/app/components/search/search_model.rs @@ -0,0 +1,66 @@ +use std::ops::Deref; +use std::rc::Rc; + +use crate::app::dispatch::ActionDispatcher; +use crate::app::models::*; +use crate::app::state::{AppAction, AppModel, BrowserAction}; + +pub struct SearchResultsModel { + app_model: Rc, + dispatcher: Box, +} + +impl SearchResultsModel { + pub fn new(app_model: Rc, dispatcher: Box) -> Self { + Self { + app_model, + dispatcher, + } + } + + pub fn go_back(&self) { + self.dispatcher + .dispatch(BrowserAction::NavigationPop.into()); + } + + pub fn search(&self, query: String) { + self.dispatcher + .dispatch(BrowserAction::Search(query).into()); + } + + fn get_query(&self) -> Option + '_> { + self.app_model + .map_state_opt(|s| Some(&s.browser.search_state()?.query).filter(|s| !s.is_empty())) + } + + pub fn fetch_results(&self) { + let api = self.app_model.get_spotify(); + if let Some(query) = self.get_query() { + let query = query.to_owned(); + self.dispatcher + .call_spotify_and_dispatch(move || async move { + api.search(&query, 0, 5) + .await + .map(|results| BrowserAction::SetSearchResults(Box::new(results)).into()) + }); + } + } + + pub fn get_album_results(&self) -> Option> + '_> { + self.app_model + .map_state_opt(|s| Some(&s.browser.search_state()?.album_results)) + } + + pub fn get_artist_results(&self) -> Option> + '_> { + self.app_model + .map_state_opt(|s| Some(&s.browser.search_state()?.artist_results)) + } + + pub fn open_album(&self, id: String) { + self.dispatcher.dispatch(AppAction::ViewAlbum(id)); + } + + pub fn open_artist(&self, id: String) { + self.dispatcher.dispatch(AppAction::ViewArtist(id)); + } +} diff --git a/src/app/components/selection/component.rs b/src/app/components/selection/component.rs new file mode 100644 index 0000000..5737237 --- /dev/null +++ b/src/app/components/selection/component.rs @@ -0,0 +1,233 @@ +use gettextrs::gettext; +use gtk::prelude::*; +use std::ops::Deref; +use std::rc::Rc; + +use crate::app::components::{Component, EventListener}; +use crate::app::models::PlaylistSummary; +use crate::app::state::{ + LoginEvent, SelectionAction, SelectionContext, SelectionEvent, SelectionState, +}; +use crate::app::{ActionDispatcher, AppAction, AppEvent, AppModel, BrowserAction}; + +use super::widget::{SelectionToolState, SelectionToolbarWidget}; + +pub struct SelectionToolbarModel { + app_model: Rc, + dispatcher: Box, +} + +impl SelectionToolbarModel { + pub fn new(app_model: Rc, dispatcher: Box) -> Self { + Self { + app_model, + dispatcher, + } + } + + pub fn move_up_selection(&self) { + self.dispatcher.dispatch(AppAction::MoveUpSelection); + } + + pub fn move_down_selection(&self) { + self.dispatcher.dispatch(AppAction::MoveDownSelection); + } + + pub fn queue_selection(&self) { + self.dispatcher.dispatch(AppAction::QueueSelection); + } + + fn dequeue_selection(&self) { + self.dispatcher.dispatch(AppAction::DequeueSelection); + } + + pub fn remove_selection(&self) { + match &self.selection().context { + SelectionContext::SavedTracks => self.remove_saved_tracks(), + SelectionContext::Queue => self.dequeue_selection(), + SelectionContext::EditablePlaylist(id) => self.remove_from_playlist(id), + _ => {} + } + } + + pub fn save_selection(&self) { + let api = self.app_model.get_spotify(); + let ids: Vec = self + .selection() + .peek_selection() + .map(|s| &s.id) + .cloned() + .collect(); + self.dispatcher + .call_spotify_and_dispatch_many(move || async move { + api.save_tracks(ids).await?; + Ok(vec![ + AppAction::SaveSelection, + AppAction::ShowNotification(gettext("Tracks saved!")), + ]) + }) + } + + fn remove_saved_tracks(&self) { + let api = self.app_model.get_spotify(); + let ids: Vec = self + .selection() + .peek_selection() + .map(|s| &s.id) + .cloned() + .collect(); + self.dispatcher + .call_spotify_and_dispatch_many(move || async move { + api.remove_saved_tracks(ids).await?; + Ok(vec![AppAction::UnsaveSelection]) + }) + } + + fn selection(&self) -> impl Deref + '_ { + self.app_model.map_state(|s| &s.selection) + } + + fn selected_count(&self) -> usize { + self.selection().count() + } + + fn user_playlists(&self) -> impl Deref> + '_ { + self.app_model.map_state(|s| &s.logged_user.playlists) + } + + fn add_to_playlist(&self, id: &str) { + let id = id.to_string(); + let api = self.app_model.get_spotify(); + let uris: Vec = self + .selection() + .peek_selection() + .map(|s| &s.uri) + .cloned() + .collect(); + self.dispatcher + .call_spotify_and_dispatch(move || async move { + api.add_to_playlist(&id, uris).await?; + Ok(SelectionAction::Clear.into()) + }) + } + + fn remove_from_playlist(&self, id: &str) { + let api = self.app_model.get_spotify(); + let id = id.to_string(); + let uris: Vec = self + .selection() + .peek_selection() + .map(|s| &s.uri) + .cloned() + .collect(); + self.dispatcher + .call_spotify_and_dispatch_many(move || async move { + api.remove_from_playlist(&id, uris.clone()).await?; + Ok(vec![ + BrowserAction::RemoveTracksFromPlaylist(id, uris).into(), + SelectionAction::Clear.into(), + ]) + }) + } +} + +pub struct SelectionToolbar { + model: Rc, + widget: SelectionToolbarWidget, +} + +impl SelectionToolbar { + pub fn new(model: SelectionToolbarModel, widget: SelectionToolbarWidget) -> Self { + let model = Rc::new(model); + widget.connect_move_up(clone!(@weak model => move || model.move_up_selection())); + widget.connect_move_down(clone!(@weak model => move || model.move_down_selection())); + widget.connect_queue(clone!(@weak model => move || model.queue_selection())); + widget.connect_remove(clone!(@weak model => move || model.remove_selection())); + widget.connect_save(clone!(@weak model => move || model.save_selection())); + Self { model, widget } + } + + fn update_active_tools(&self) { + let count = self.model.selected_count(); + match self.model.selection().context { + SelectionContext::Default => { + self.widget.set_move(SelectionToolState::Hidden); + self.widget + .set_queue(SelectionToolState::Visible(count > 0)); + self.widget.set_add(SelectionToolState::Visible(count > 0)); + self.widget.set_remove(SelectionToolState::Hidden); + self.widget.set_save(SelectionToolState::Visible(count > 0)); + } + SelectionContext::SavedTracks => { + self.widget.set_move(SelectionToolState::Hidden); + self.widget + .set_queue(SelectionToolState::Visible(count > 0)); + self.widget.set_add(SelectionToolState::Visible(count > 0)); + self.widget + .set_remove(SelectionToolState::Visible(count > 0)); + self.widget.set_save(SelectionToolState::Hidden); + } + SelectionContext::ReadOnlyQueue => { + self.widget.set_move(SelectionToolState::Hidden); + self.widget.set_queue(SelectionToolState::Hidden); + self.widget.set_add(SelectionToolState::Hidden); + self.widget.set_remove(SelectionToolState::Hidden); + self.widget.set_save(SelectionToolState::Visible(count > 0)); + } + SelectionContext::Queue => { + self.widget + .set_move(SelectionToolState::Visible(count == 1)); + self.widget.set_queue(SelectionToolState::Hidden); + self.widget.set_add(SelectionToolState::Hidden); + self.widget + .set_remove(SelectionToolState::Visible(count > 0)); + self.widget.set_save(SelectionToolState::Visible(count > 0)); + } + SelectionContext::Playlist => { + self.widget.set_move(SelectionToolState::Hidden); + self.widget + .set_queue(SelectionToolState::Visible(count > 0)); + self.widget.set_add(SelectionToolState::Hidden); + self.widget.set_remove(SelectionToolState::Hidden); + self.widget.set_save(SelectionToolState::Hidden); + } + SelectionContext::EditablePlaylist(_) => { + self.widget.set_move(SelectionToolState::Hidden); + self.widget + .set_queue(SelectionToolState::Visible(count > 0)); + self.widget.set_add(SelectionToolState::Hidden); + self.widget + .set_remove(SelectionToolState::Visible(count > 0)); + self.widget.set_save(SelectionToolState::Hidden); + } + }; + } +} + +impl Component for SelectionToolbar { + fn get_root_widget(&self) -> >k::Widget { + self.widget.upcast_ref() + } +} + +impl EventListener for SelectionToolbar { + fn on_event(&mut self, event: &AppEvent) { + match event { + AppEvent::SelectionEvent(SelectionEvent::SelectionModeChanged(active)) => { + self.widget.set_visible(*active); + self.update_active_tools(); + } + AppEvent::SelectionEvent(SelectionEvent::SelectionChanged) => { + self.update_active_tools(); + } + AppEvent::LoginEvent(LoginEvent::UserPlaylistsLoaded) => { + let model = &self.model; + self.widget.connect_playlists( + &model.user_playlists(), + clone!(@weak model => move |s| model.add_to_playlist(s)), + ); + } + _ => {} + } + } +} diff --git a/src/app/components/selection/icons/music-queue-symbolic.svg b/src/app/components/selection/icons/music-queue-symbolic.svg new file mode 100644 index 0000000..1152a2d --- /dev/null +++ b/src/app/components/selection/icons/music-queue-symbolic.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/app/components/selection/icons/playlist2-symbolic.svg b/src/app/components/selection/icons/playlist2-symbolic.svg new file mode 100644 index 0000000..3d820c0 --- /dev/null +++ b/src/app/components/selection/icons/playlist2-symbolic.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/app/components/selection/mod.rs b/src/app/components/selection/mod.rs new file mode 100644 index 0000000..a737bac --- /dev/null +++ b/src/app/components/selection/mod.rs @@ -0,0 +1,10 @@ +mod widget; + +mod component; +pub use component::*; + +use glib::prelude::*; + +pub fn expose_widgets() { + widget::SelectionToolbarWidget::static_type(); +} diff --git a/src/app/components/selection/selection_toolbar.blp b/src/app/components/selection/selection_toolbar.blp new file mode 100644 index 0000000..31953cc --- /dev/null +++ b/src/app/components/selection/selection_toolbar.blp @@ -0,0 +1,91 @@ +using Gtk 4.0; +using Adw 1; + +template $SelectionToolbarWidget : Box { + hexpand: true; + visible: false; + + ActionBar action_bar { + hexpand: true; + revealed: true; + + Box { + valign: center; + + styles [ + "linked", + ] + + Button move_up { + icon-name: "go-up-symbolic"; + } + + Button move_down { + icon-name: "go-down-symbolic"; + } + } + + [end] + Button queue { + valign: center; + has-frame: false; + + Adw.Squeezer { + Adw.ButtonContent { + icon-name: "music-queue-symbolic"; + label: _("Add to queue"); + } + + Adw.ButtonContent { + icon-name: "music-queue-symbolic"; + } + } + } + + [end] + MenuButton add { + valign: center; + has-frame: false; + label: _("Add to playlist..."); + direction: up; + } + + [end] + Button remove { + valign: center; + has-frame: false; + + Adw.Squeezer { + Adw.ButtonContent { + icon-name: "user-trash-symbolic"; + label: _("Remove"); + } + + Adw.ButtonContent { + icon-name: "user-trash-symbolic"; + } + } + } + + [end] + Button save { + valign: center; + has-frame: false; + + Adw.Squeezer { + Adw.ButtonContent { + icon-name: "star-new-symbolic"; + label: _("Save to library"); + } + + Adw.ButtonContent { + icon-name: "star-new-symbolic"; + } + } + } + } + + styles [ + "selection_toolbar", + ] +} diff --git a/src/app/components/selection/selection_toolbar.css b/src/app/components/selection/selection_toolbar.css new file mode 100644 index 0000000..c580a23 --- /dev/null +++ b/src/app/components/selection/selection_toolbar.css @@ -0,0 +1,3 @@ +.selection_toolbar { + background: @theme_bg_color; +} diff --git a/src/app/components/selection/widget.rs b/src/app/components/selection/widget.rs new file mode 100644 index 0000000..d6ede9e --- /dev/null +++ b/src/app/components/selection/widget.rs @@ -0,0 +1,183 @@ +use gio::prelude::ActionMapExt; +use gio::{SimpleAction, SimpleActionGroup}; +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; + +use crate::app::components::{display_add_css_provider, labels}; +use crate::app::models::PlaylistSummary; + +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/selection_toolbar.ui")] + pub struct SelectionToolbarWidget { + #[template_child] + pub action_bar: TemplateChild, + + #[template_child] + pub move_up: TemplateChild, + + #[template_child] + pub move_down: TemplateChild, + + #[template_child] + pub add: TemplateChild, + + #[template_child] + pub remove: TemplateChild, + + #[template_child] + pub queue: TemplateChild, + + #[template_child] + pub save: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for SelectionToolbarWidget { + const NAME: &'static str = "SelectionToolbarWidget"; + type Type = super::SelectionToolbarWidget; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for SelectionToolbarWidget { + fn constructed(&self) { + self.parent_constructed(); + display_add_css_provider(resource!("/components/selection_toolbar.css")); + } + } + + impl WidgetImpl for SelectionToolbarWidget {} + impl BoxImpl for SelectionToolbarWidget {} +} + +glib::wrapper! { + pub struct SelectionToolbarWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; +} + +#[derive(Debug, Clone, Copy)] +pub enum SelectionToolState { + Hidden, + Visible(bool), +} + +impl SelectionToolState { + fn visible(self) -> bool { + matches!(self, SelectionToolState::Visible(_)) + } + + fn sensitive(self) -> bool { + matches!(self, SelectionToolState::Visible(true)) + } +} + +impl SelectionToolbarWidget { + pub fn connect_move_down(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().move_down.connect_clicked(move |_| f()); + } + + pub fn connect_move_up(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().move_up.connect_clicked(move |_| f()); + } + + pub fn connect_queue(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().queue.connect_clicked(move |_| f()); + } + + pub fn connect_save(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().save.connect_clicked(move |_| f()); + } + + pub fn connect_remove(&self, f: F) + where + F: Fn() + 'static, + { + self.imp().remove.connect_clicked(move |_| f()); + } + + pub fn set_move(&self, state: SelectionToolState) { + self.imp().move_up.set_sensitive(state.sensitive()); + self.imp().move_up.set_visible(state.visible()); + self.imp().move_down.set_sensitive(state.sensitive()); + self.imp().move_down.set_visible(state.visible()); + } + + pub fn set_queue(&self, state: SelectionToolState) { + self.imp().queue.set_sensitive(state.sensitive()); + self.imp().queue.set_visible(state.visible()); + } + + pub fn set_add(&self, state: SelectionToolState) { + self.imp().add.set_sensitive(state.sensitive()); + self.imp().add.set_visible(state.visible()); + } + + pub fn set_remove(&self, state: SelectionToolState) { + self.imp().remove.set_sensitive(state.sensitive()); + self.imp().remove.set_visible(state.visible()); + } + + pub fn set_save(&self, state: SelectionToolState) { + self.imp().save.set_sensitive(state.sensitive()); + self.imp().save.set_visible(state.visible()); + } + + pub fn set_visible(&self, visible: bool) { + gtk::Widget::set_visible(self.upcast_ref(), visible); + self.imp().action_bar.set_revealed(visible); + } + + pub fn connect_playlists(&self, playlists: &[PlaylistSummary], on_playlist_selected: F) + where + F: Fn(&str) + Clone + 'static, + { + let menu = gio::Menu::new(); + let action_group = SimpleActionGroup::new(); + + for PlaylistSummary { title, id } in playlists { + let action_name = format!("playlist_{id}"); + + action_group.add_action(&{ + let id = id.clone(); + let action = SimpleAction::new(&action_name, None); + let f = on_playlist_selected.clone(); + action.connect_activate(move |_, _| f(&id)); + action + }); + + menu.append( + Some(&labels::add_to_playlist_label(title)), + Some(&format!("add_to.{action_name}")), + ); + } + + let popover = gtk::PopoverMenu::from_model(Some(&menu)); + self.imp().add.set_popover(Some(&popover)); + self.imp() + .add + .insert_action_group("add_to", Some(&action_group)); + } +} diff --git a/src/app/components/settings/mod.rs b/src/app/components/settings/mod.rs new file mode 100644 index 0000000..d874084 --- /dev/null +++ b/src/app/components/settings/mod.rs @@ -0,0 +1,6 @@ +#[allow(clippy::module_inception)] +mod settings; +mod settings_model; + +pub use settings::*; +pub use settings_model::*; diff --git a/src/app/components/settings/settings.blp b/src/app/components/settings/settings.blp new file mode 100644 index 0000000..02ba6f1 --- /dev/null +++ b/src/app/components/settings/settings.blp @@ -0,0 +1,109 @@ +using Gtk 4.0; +using Adw 1; + +template $SettingsWindow : Adw.PreferencesWindow { + default-width: 600; + hide-on-close: true; + search-enabled: false; + + Adw.PreferencesPage { + Adw.PreferencesGroup { + /* Translators: Header for a group of preference items regarding audio */ + + title: _("Audio"); + + Adw.ComboRow audio_backend { + /* Translators: Title for an item in preferences */ + + title: _("Audio Backend"); + model: StringList { + strings [ + "PulseAudio", + "ALSA", + "Pipewire (GStreamer)" + ] + }; + } + + Adw.ActionRow alsa_device_row { + /* Translators: Title for an item in preferences */ + + title: _("ALSA Device"); + + /* Translators: Description for the item (ALSA Device) in preferences */ + + subtitle: _("Applied only if audio backend is ALSA"); + + Entry alsa_device { + valign: center; + } + } + + Adw.ComboRow player_bitrate { + /* Translators: Title for an item in preferences */ + + title: _("Audio Quality"); + model: StringList { + strings [ + _("Normal"), + _("High"), + _("Very high"), + ] + }; + } + + Adw.ActionRow gapless_playback { + /* Translators: Title for an item in preferences */ + + title: _("Gapless playback"); + name: "gapless_playback_row"; + activatable-widget: gapless_playback_switch; + visible: true; + + Switch gapless_playback_switch { + margin-top: 12; + margin-bottom: 12; + } + } + } + + Adw.PreferencesGroup { + /* Translators: Header for a group of preference items regarding the application's appearance */ + + title: _("Appearance"); + + Adw.ComboRow theme { + /* Translators: Title for an item in preferences */ + + title: _("Theme"); + model: StringList { + strings [ + _("Light"), + _("Dark"), + _("System") + ] + }; + } + } + + Adw.PreferencesGroup { + /* Translators: Header for a group of preference items regarding network */ + + title: _("Network"); + + Adw.ActionRow { + /* Translators: Title for an item in preferences */ + + title: _("Access Point Port"); + + /* Translators: Longer description for an item (Access Point Port) in preferences */ + + subtitle: _("Port used for connections to Spotify\'s Access Point. Set to 0 if any port is fine."); + + Entry ap_port { + valign: center; + } + } + } + } +} diff --git a/src/app/components/settings/settings.rs b/src/app/components/settings/settings.rs new file mode 100644 index 0000000..cbf4bcf --- /dev/null +++ b/src/app/components/settings/settings.rs @@ -0,0 +1,290 @@ +use crate::app::components::EventListener; +use crate::app::AppEvent; +use crate::settings::SpotSettings; + +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; +use libadwaita::prelude::*; + +use super::SettingsModel; + +const SETTINGS: &str = "dev.alextren.Spot"; + +mod imp { + + use super::*; + use libadwaita::subclass::prelude::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/settings.ui")] + pub struct SettingsWindow { + #[template_child] + pub player_bitrate: TemplateChild, + + #[template_child] + pub alsa_device: TemplateChild, + + #[template_child] + pub alsa_device_row: TemplateChild, + + #[template_child] + pub audio_backend: TemplateChild, + + #[template_child] + pub gapless_playback: TemplateChild, + + #[template_child] + pub ap_port: TemplateChild, + + #[template_child] + pub theme: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for SettingsWindow { + const NAME: &'static str = "SettingsWindow"; + type Type = super::SettingsWindow; + type ParentType = libadwaita::PreferencesWindow; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for SettingsWindow {} + impl WidgetImpl for SettingsWindow {} + impl WindowImpl for SettingsWindow {} + impl AdwWindowImpl for SettingsWindow {} + impl PreferencesWindowImpl for SettingsWindow {} +} + +glib::wrapper! { + pub struct SettingsWindow(ObjectSubclass) @extends gtk::Widget, gtk::Window, libadwaita::Window, libadwaita::PreferencesWindow; +} + +impl Default for SettingsWindow { + fn default() -> Self { + Self::new() + } +} + +impl SettingsWindow { + pub fn new() -> Self { + let window: Self = glib::Object::new(); + + window.bind_backend_and_device(); + window.bind_settings(); + window.connect_theme_select(); + window + } + + fn bind_backend_and_device(&self) { + let widget = self.imp(); + + let audio_backend = widget + .audio_backend + .downcast_ref::() + .unwrap(); + let alsa_device_row = widget + .alsa_device_row + .downcast_ref::() + .unwrap(); + + audio_backend + .bind_property("selected", alsa_device_row, "visible") + .transform_to(|_, value: u32| Some(value == 1)) + .build(); + + if audio_backend.selected() == 0 { + alsa_device_row.set_visible(false); + } + } + + fn bind_settings(&self) { + let widget = self.imp(); + let settings = gio::Settings::new(SETTINGS); + + let player_bitrate = widget + .player_bitrate + .downcast_ref::() + .unwrap(); + settings + .bind("player-bitrate", player_bitrate, "selected") + .mapping(|variant, _| { + variant.str().map(|s| { + match s { + "96" => 0, + "160" => 1, + "320" => 2, + _ => unreachable!(), + } + .to_value() + }) + }) + .set_mapping(|value, _| { + value.get::().ok().map(|u| { + match u { + 0 => "96", + 1 => "160", + 2 => "320", + _ => unreachable!(), + } + .to_variant() + }) + }) + .build(); + + let alsa_device = widget.alsa_device.downcast_ref::().unwrap(); + settings.bind("alsa-device", alsa_device, "text").build(); + + let audio_backend = widget + .audio_backend + .downcast_ref::() + .unwrap(); + settings + .bind("audio-backend", audio_backend, "selected") + .mapping(|variant, _| { + variant.str().map(|s| { + match s { + "pulseaudio" => 0, + "alsa" => 1, + "gstreamer" => 2, + _ => unreachable!(), + } + .to_value() + }) + }) + .set_mapping(|value, _| { + value.get::().ok().map(|u| { + match u { + 0 => "pulseaudio", + 1 => "alsa", + 2 => "gstreamer", + _ => unreachable!(), + } + .to_variant() + }) + }) + .build(); + + let gapless_playback = widget + .gapless_playback + .downcast_ref::() + .unwrap(); + settings + .bind( + "gapless-playback", + &gapless_playback.activatable_widget().unwrap(), + "active", + ) + .build(); + + let ap_port = widget.ap_port.downcast_ref::().unwrap(); + settings + .bind("ap-port", ap_port, "text") + .mapping(|variant, _| variant.get::().map(|s| s.to_value())) + .set_mapping(|value, _| value.get::().ok().map(|u| u.to_variant())) + .build(); + + let theme = widget.theme.downcast_ref::().unwrap(); + settings + .bind("theme-preference", theme, "selected") + .mapping(|variant, _| { + variant.str().map(|s| { + match s { + "light" => 0, + "dark" => 1, + "system" => 2, + _ => unreachable!(), + } + .to_value() + }) + }) + .set_mapping(|value, _| { + value.get::().ok().map(|u| { + match u { + 0 => "light", + 1 => "dark", + 2 => "system", + _ => unreachable!(), + } + .to_variant() + }) + }) + .build(); + } + + fn connect_theme_select(&self) { + let widget = self.imp(); + let theme = widget.theme.downcast_ref::().unwrap(); + theme.connect_selected_notify(|theme| { + debug!("Theme switched! --> value: {}", theme.selected()); + let manager = libadwaita::StyleManager::default(); + + let pref = match theme.selected() { + 0 => libadwaita::ColorScheme::ForceLight, + 1 => libadwaita::ColorScheme::ForceDark, + _ => libadwaita::ColorScheme::Default, + }; + + manager.set_color_scheme(pref); + }); + } + + fn connect_close(&self, on_close: F) + where + F: Fn() + 'static, + { + let window = self.upcast_ref::(); + + window.connect_close_request( + clone!(@weak self as _self => @default-return gtk::Inhibit(false), move |_| { + on_close(); + gtk::Inhibit(false) + }), + ); + } +} + +pub struct Settings { + parent: gtk::Window, + settings_window: SettingsWindow, +} + +impl Settings { + pub fn new(parent: gtk::Window, model: SettingsModel) -> Self { + let settings_window = SettingsWindow::new(); + + settings_window.connect_close(move || { + let new_settings = SpotSettings::new_from_gsettings().unwrap_or_default(); + if model.settings().player_settings != new_settings.player_settings { + model.stop_player(); + } + model.set_settings(); + }); + + Self { + parent, + settings_window, + } + } + + fn window(&self) -> &libadwaita::Window { + self.settings_window.upcast_ref::() + } + + pub fn show_self(&self) { + self.window().set_transient_for(Some(&self.parent)); + self.window().set_modal(true); + self.window().set_visible(true); + } +} + +impl EventListener for Settings { + fn on_event(&mut self, _: &AppEvent) {} +} diff --git a/src/app/components/settings/settings_model.rs b/src/app/components/settings/settings_model.rs new file mode 100644 index 0000000..4706497 --- /dev/null +++ b/src/app/components/settings/settings_model.rs @@ -0,0 +1,32 @@ +use crate::app::state::{PlaybackAction, SettingsAction}; +use crate::app::{ActionDispatcher, AppModel}; +use crate::settings::SpotSettings; +use std::rc::Rc; + +pub struct SettingsModel { + app_model: Rc, + dispatcher: Box, +} + +impl SettingsModel { + pub fn new(app_model: Rc, dispatcher: Box) -> Self { + Self { + app_model, + dispatcher, + } + } + + pub fn stop_player(&self) { + self.dispatcher.dispatch(PlaybackAction::Stop.into()); + } + + pub fn set_settings(&self) { + self.dispatcher + .dispatch(SettingsAction::ChangeSettings.into()); + } + + pub fn settings(&self) -> SpotSettings { + let state = self.app_model.get_state(); + state.settings.settings.clone() + } +} diff --git a/src/app/components/sidebar/create_playlist.blp b/src/app/components/sidebar/create_playlist.blp new file mode 100644 index 0000000..269577f --- /dev/null +++ b/src/app/components/sidebar/create_playlist.blp @@ -0,0 +1,40 @@ +using Gtk 4.0; + +template $CreatePlaylistPopover : Popover { + position: right; + + Box box { + Label label { + /* Translators: label for the entry containing the name of a new playlist */ + + label: _("Name"); + } + + Entry entry { + focusable: true; + margin-start: 12; + margin-end: 12; + } + + Revealer error_revealer { + Label error_label { + max-width-chars: 0; + wrap: true; + xalign: 0; + } + } + + Button button { + /* Translators: Button that creates a new playlist */ + + label: _("Create"); + focusable: true; + halign: end; + use-underline: true; + + styles [ + "suggested-action", + ] + } + } +} diff --git a/src/app/components/sidebar/create_playlist.rs b/src/app/components/sidebar/create_playlist.rs new file mode 100644 index 0000000..27d4238 --- /dev/null +++ b/src/app/components/sidebar/create_playlist.rs @@ -0,0 +1,67 @@ +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; + +mod imp { + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/create_playlist.ui")] + pub struct CreatePlaylistPopover { + #[template_child] + pub label: TemplateChild, + + #[template_child] + pub entry: TemplateChild, + + #[template_child] + pub button: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for CreatePlaylistPopover { + const NAME: &'static str = "CreatePlaylistPopover"; + type Type = super::CreatePlaylistPopover; + type ParentType = gtk::Popover; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for CreatePlaylistPopover {} + impl WidgetImpl for CreatePlaylistPopover {} + impl PopoverImpl for CreatePlaylistPopover {} +} + +glib::wrapper! { + pub struct CreatePlaylistPopover(ObjectSubclass) @extends gtk::Widget, gtk::Popover; +} + +impl Default for CreatePlaylistPopover { + fn default() -> Self { + Self::new() + } +} + +impl CreatePlaylistPopover { + pub fn new() -> Self { + glib::Object::new() + } + + pub fn connect_create(&self, create_fun: F) { + let entry = self.imp().entry.get(); + let closure = clone!(@weak self as popover, @weak entry, @strong create_fun => move || { + create_fun(entry.text().to_string()); + popover.popdown(); + entry.buffer().delete_text(0, None); + }); + let closure_clone = closure.clone(); + entry.connect_activate(move |_| closure()); + self.imp().button.connect_clicked(move |_| closure_clone()); + } +} diff --git a/src/app/components/sidebar/icons/library-music-symbolic.svg b/src/app/components/sidebar/icons/library-music-symbolic.svg new file mode 100644 index 0000000..0ef4ce1 --- /dev/null +++ b/src/app/components/sidebar/icons/library-music-symbolic.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/app/components/sidebar/mod.rs b/src/app/components/sidebar/mod.rs new file mode 100644 index 0000000..95a4e2d --- /dev/null +++ b/src/app/components/sidebar/mod.rs @@ -0,0 +1,9 @@ +#[allow(clippy::module_inception)] +mod sidebar; +pub use sidebar::*; + +mod sidebar_item; +pub use sidebar_item::*; + +mod create_playlist; +mod sidebar_row; diff --git a/src/app/components/sidebar/sidebar.rs b/src/app/components/sidebar/sidebar.rs new file mode 100644 index 0000000..19d96f2 --- /dev/null +++ b/src/app/components/sidebar/sidebar.rs @@ -0,0 +1,200 @@ +use gettextrs::gettext; +use gtk::prelude::*; +use std::rc::Rc; + +use super::create_playlist::CreatePlaylistPopover; +use super::{ + sidebar_row::SidebarRow, SidebarDestination, SidebarItem, CREATE_PLAYLIST_ITEM, + SAVED_PLAYLISTS_SECTION, +}; +use crate::app::models::{AlbumModel, PlaylistSummary}; +use crate::app::state::ScreenName; +use crate::app::{ + ActionDispatcher, AppAction, AppEvent, AppModel, BrowserAction, BrowserEvent, Component, + EventListener, +}; + +const NUM_FIXED_ENTRIES: u32 = 6; +const NUM_PLAYLISTS: usize = 20; + +pub struct SidebarModel { + app_model: Rc, + dispatcher: Box, +} + +impl SidebarModel { + pub fn new(app_model: Rc, dispatcher: Box) -> Self { + Self { + app_model, + dispatcher, + } + } + + fn get_playlists(&self) -> Vec { + self.app_model + .get_state() + .browser + .home_state() + .expect("expected HomeState to be available") + .playlists + .iter() + .take(NUM_PLAYLISTS) + .map(Self::map_to_destination) + .collect() + } + + fn map_to_destination(a: AlbumModel) -> SidebarDestination { + let title = Some(a.album()) + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| gettext("Unnamed playlist")); + let id = a.uri(); + SidebarDestination::Playlist(PlaylistSummary { id, title }) + } + + fn create_new_playlist(&self, name: String) { + let user_id = self.app_model.get_state().logged_user.user.clone().unwrap(); + let api = self.app_model.get_spotify(); + self.dispatcher + .call_spotify_and_dispatch(move || async move { + api.create_new_playlist(name.as_str(), user_id.as_str()) + .await + .map(AppAction::CreatePlaylist) + }) + } + + fn navigate(&self, dest: SidebarDestination) { + let actions = match dest { + SidebarDestination::Library + | SidebarDestination::SavedTracks + | SidebarDestination::NowPlaying + | SidebarDestination::SavedPlaylists => { + vec![ + BrowserAction::NavigationPopTo(ScreenName::Home).into(), + BrowserAction::SetHomeVisiblePage(dest.id()).into(), + ] + } + SidebarDestination::Playlist(PlaylistSummary { id, .. }) => { + vec![AppAction::ViewPlaylist(id)] + } + }; + self.dispatcher.dispatch_many(actions); + } +} + +pub struct Sidebar { + listbox: gtk::ListBox, + list_store: gio::ListStore, + model: Rc, +} + +impl Sidebar { + pub fn new(listbox: gtk::ListBox, model: Rc) -> Self { + let popover = CreatePlaylistPopover::new(); + popover.connect_create(clone!(@weak model => move |t| model.create_new_playlist(t))); + + let list_store = gio::ListStore::new(SidebarItem::static_type()); + + list_store.append(&SidebarItem::from_destination(SidebarDestination::Library)); + list_store.append(&SidebarItem::from_destination( + SidebarDestination::SavedTracks, + )); + list_store.append(&SidebarItem::from_destination( + SidebarDestination::NowPlaying, + )); + list_store.append(&SidebarItem::playlists_section()); + list_store.append(&SidebarItem::create_playlist_item()); + list_store.append(&SidebarItem::from_destination( + SidebarDestination::SavedPlaylists, + )); + + listbox.bind_model( + Some(&list_store), + clone!(@weak popover => @default-panic, move |obj| { + let item = obj.downcast_ref::().unwrap(); + if item.navigatable() { + Self::make_navigatable(item) + } else { + match item.id().as_str() { + SAVED_PLAYLISTS_SECTION => Self::make_section_label(item), + CREATE_PLAYLIST_ITEM => Self::make_create_playlist(item, popover), + _ => unimplemented!(), + } + } + }), + ); + + listbox.connect_row_activated(clone!(@weak popover, @weak model => move |_, row| { + if let Some(row) = row.downcast_ref::() { + if let Some(dest) = row.item().destination() { + model.navigate(dest); + } else { + match row.item().id().as_str() { + CREATE_PLAYLIST_ITEM => popover.popup(), + _ => unimplemented!() + } + } + } + })); + + Self { + listbox, + list_store, + model, + } + } + + fn make_navigatable(item: &SidebarItem) -> gtk::Widget { + let row = SidebarRow::new(item.clone()); + row.set_selectable(false); + row.upcast() + } + + fn make_section_label(item: &SidebarItem) -> gtk::Widget { + let label = gtk::Label::new(Some(item.title().as_str())); + label.add_css_class("caption-heading"); + let row = gtk::ListBoxRow::builder() + .activatable(false) + .selectable(false) + .sensitive(false) + .child(&label) + .build(); + row.upcast() + } + + fn make_create_playlist(item: &SidebarItem, popover: CreatePlaylistPopover) -> gtk::Widget { + let row = SidebarRow::new(item.clone()); + row.set_activatable(true); + row.set_selectable(false); + row.set_sensitive(true); + popover.set_parent(&row); + row.upcast() + } + + fn update_playlists_in_sidebar(&self) { + let playlists: Vec = self + .model + .get_playlists() + .into_iter() + .map(SidebarItem::from_destination) + .collect(); + self.list_store.splice( + NUM_FIXED_ENTRIES, + self.list_store.n_items() - NUM_FIXED_ENTRIES, + playlists.as_slice(), + ); + } +} + +impl Component for Sidebar { + fn get_root_widget(&self) -> >k::Widget { + self.listbox.upcast_ref() + } +} + +impl EventListener for Sidebar { + fn on_event(&mut self, event: &AppEvent) { + if let AppEvent::BrowserEvent(BrowserEvent::SavedPlaylistsUpdated) = event { + self.update_playlists_in_sidebar(); + } + } +} diff --git a/src/app/components/sidebar/sidebar_item.rs b/src/app/components/sidebar/sidebar_item.rs new file mode 100644 index 0000000..9dddf82 --- /dev/null +++ b/src/app/components/sidebar/sidebar_item.rs @@ -0,0 +1,167 @@ +use gettextrs::gettext; +use glib::Properties; +use gtk::prelude::*; +use gtk::subclass::prelude::*; + +use crate::app::models::PlaylistSummary; + +const LIBRARY: &str = "library"; +const SAVED_TRACKS: &str = "saved_tracks"; +const NOW_PLAYING: &str = "now_playing"; +const SAVED_PLAYLISTS: &str = "saved_playlists"; +const PLAYLIST: &str = "playlist"; +pub const SAVED_PLAYLISTS_SECTION: &str = "saved_playlists_section"; +pub const CREATE_PLAYLIST_ITEM: &str = "create_playlist"; + +#[derive(Debug)] +pub enum SidebarDestination { + Library, + SavedTracks, + NowPlaying, + SavedPlaylists, + Playlist(PlaylistSummary), +} + +impl SidebarDestination { + pub fn id(&self) -> &'static str { + match self { + Self::Library => LIBRARY, + Self::SavedTracks => SAVED_TRACKS, + Self::NowPlaying => NOW_PLAYING, + Self::SavedPlaylists => SAVED_PLAYLISTS, + Self::Playlist(_) => PLAYLIST, + } + } + + pub fn title(&self) -> String { + match self { + // translators: This is a sidebar entry to browse to saved albums. + Self::Library => gettext("Library"), + // translators: This is a sidebar entry to browse to saved tracks. + Self::SavedTracks => gettext("Saved tracks"), + // translators: This is a sidebar entry to browse to saved playlists. + Self::NowPlaying => gettext("Now playing"), + // translators: This is a sidebar entry that marks that the entries below are playlists. + Self::SavedPlaylists => gettext("Playlists"), + Self::Playlist(PlaylistSummary { title, .. }) => title.clone(), + } + } + + pub fn icon(&self) -> &'static str { + match self { + Self::Library => "library-music-symbolic", + Self::SavedTracks => "starred-symbolic", + Self::NowPlaying => "music-queue-symbolic", + Self::SavedPlaylists => "view-app-grid-symbolic", + Self::Playlist(_) => "playlist2-symbolic", + } + } +} + +impl SidebarItem { + pub fn from_destination(dest: SidebarDestination) -> Self { + let (id, data, title) = match dest { + SidebarDestination::Playlist(PlaylistSummary { id, title }) => { + (PLAYLIST, Some(id), title) + } + _ => (dest.id(), None, dest.title()), + }; + glib::Object::builder() + .property("id", id) + .property("data", data.unwrap_or_default()) + .property("title", &title) + .property("navigatable", true) + .build() + } + + pub fn playlists_section() -> Self { + glib::Object::builder() + .property("id", SAVED_PLAYLISTS_SECTION) + .property("data", String::new()) + .property("title", gettext("All Playlists")) + .property("navigatable", false) + .build() + } + + pub fn create_playlist_item() -> Self { + glib::Object::builder() + .property("id", CREATE_PLAYLIST_ITEM) + .property("data", String::new()) + .property("title", gettext("New Playlist")) + .property("navigatable", false) + .build() + } + + pub fn destination(&self) -> Option { + let navigatable = self.property::("navigatable"); + if navigatable { + let id = self.id(); + let data = self.property::("data"); + let title = self.title(); + match id.as_str() { + LIBRARY => Some(SidebarDestination::Library), + SAVED_TRACKS => Some(SidebarDestination::SavedTracks), + NOW_PLAYING => Some(SidebarDestination::NowPlaying), + SAVED_PLAYLISTS => Some(SidebarDestination::SavedPlaylists), + PLAYLIST => Some(SidebarDestination::Playlist(PlaylistSummary { + id: data, + title, + })), + _ => None, + } + } else { + None + } + } + + pub fn icon(&self) -> Option<&str> { + match self.id().as_str() { + CREATE_PLAYLIST_ITEM => Some("list-add-symbolic"), + _ => self.destination().map(|d| d.icon()), + } + } +} + +mod imp { + use super::*; + use gdk::cairo::glib::ParamSpec; + use std::cell::{Cell, RefCell}; + + #[derive(Debug, Default, Properties)] + #[properties(wrapper_type = super::SidebarItem)] + pub struct SidebarItem { + #[property(get, set)] + pub id: RefCell, + #[property(get, set)] + pub data: RefCell, + #[property(get, set)] + pub title: RefCell, + #[property(get, set)] + pub navigatable: Cell, + } + + #[glib::object_subclass] + impl ObjectSubclass for SidebarItem { + const NAME: &'static str = "SideBarItem"; + type Type = super::SidebarItem; + type ParentType = glib::Object; + } + + impl ObjectImpl for SidebarItem { + fn properties() -> &'static [ParamSpec] { + Self::derived_properties() + } + + fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { + self.derived_set_property(id, value, pspec); + } + + fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { + self.derived_property(id, pspec) + } + } +} + +glib::wrapper! { + pub struct SidebarItem(ObjectSubclass); +} diff --git a/src/app/components/sidebar/sidebar_row.blp b/src/app/components/sidebar/sidebar_row.blp new file mode 100644 index 0000000..6326471 --- /dev/null +++ b/src/app/components/sidebar/sidebar_row.blp @@ -0,0 +1,17 @@ +using Gtk 4.0; + +template $SidebarRow : ListBoxRow { + Box { + visible: true; + spacing: 12; + + Image icon { + } + + Label title { + width-chars: 20; + ellipsize: end; + xalign: 0; + } + } +} diff --git a/src/app/components/sidebar/sidebar_row.rs b/src/app/components/sidebar/sidebar_row.rs new file mode 100644 index 0000000..f86451f --- /dev/null +++ b/src/app/components/sidebar/sidebar_row.rs @@ -0,0 +1,84 @@ +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; + +use super::SidebarItem; + +impl SidebarRow { + pub fn new(item: SidebarItem) -> Self { + glib::Object::builder().property("item", item).build() + } +} + +mod imp { + use super::*; + use glib::{ParamSpec, Properties}; + use std::cell::RefCell; + + #[derive(Debug, CompositeTemplate, Properties)] + #[template(resource = "/dev/alextren/Spot/sidebar/sidebar_row.ui")] + #[properties(wrapper_type = super::SidebarRow)] + pub struct SidebarRow { + #[template_child] + pub icon: TemplateChild, + + #[template_child] + pub title: TemplateChild, + + #[property(get, set = Self::set_item)] + pub item: RefCell, + } + + impl SidebarRow { + fn set_item(&self, item: SidebarItem) { + self.title.set_text(item.title().as_str()); + self.icon.set_icon_name(item.icon()); + self.obj().set_tooltip_text(Some(item.title().as_str())); + self.item.replace(item); + } + } + + #[glib::object_subclass] + impl ObjectSubclass for SidebarRow { + const NAME: &'static str = "SidebarRow"; + type Type = super::SidebarRow; + type ParentType = gtk::ListBoxRow; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + + fn new() -> Self { + Self { + icon: Default::default(), + title: Default::default(), + item: RefCell::new(glib::Object::new()), + } + } + } + + impl ObjectImpl for SidebarRow { + fn properties() -> &'static [ParamSpec] { + Self::derived_properties() + } + + fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { + self.derived_set_property(id, value, pspec); + } + + fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { + self.derived_property(id, pspec) + } + } + + impl WidgetImpl for SidebarRow {} + impl ListBoxRowImpl for SidebarRow {} +} + +glib::wrapper! { + pub struct SidebarRow(ObjectSubclass) @extends gtk::Widget, gtk::ListBoxRow; +} diff --git a/src/app/components/user_details/mod.rs b/src/app/components/user_details/mod.rs new file mode 100644 index 0000000..e648930 --- /dev/null +++ b/src/app/components/user_details/mod.rs @@ -0,0 +1,6 @@ +#[allow(clippy::module_inception)] +mod user_details; +pub use user_details::*; + +mod user_details_model; +pub use user_details_model::*; diff --git a/src/app/components/user_details/user_details.blp b/src/app/components/user_details/user_details.blp new file mode 100644 index 0000000..a913ffc --- /dev/null +++ b/src/app/components/user_details/user_details.blp @@ -0,0 +1,44 @@ +using Gtk 4.0; + +template $UserDetailsWidget : Box { + ScrolledWindow scrolled_window { + hscrollbar-policy: never; + hexpand: true; + vexpand: true; + Box { + margin-start: 8; + margin-end: 8; + margin-top: 8; + margin-bottom: 8; + orientation: vertical; + spacing: 10; + + Label user_name { + halign: start; + margin-start: 8; + margin-end: 8; + label: "User"; + wrap: true; + xalign: 0; + + styles [ + "user_details--name", + "large-title", + ] + } + + FlowBox user_playlists { + height-request: 100; + valign: start; + hexpand: true; + min-children-per-line: 1; + selection-mode: none; + activate-on-single-click: false; + } + } + } + + styles [ + "user", + ] +} diff --git a/src/app/components/user_details/user_details.css b/src/app/components/user_details/user_details.css new file mode 100644 index 0000000..50f4d01 --- /dev/null +++ b/src/app/components/user_details/user_details.css @@ -0,0 +1,8 @@ +.user { + transition: opacity .3s ease; + opacity: 0; +} + +.user__loaded { + opacity: 1; +} diff --git a/src/app/components/user_details/user_details.rs b/src/app/components/user_details/user_details.rs new file mode 100644 index 0000000..d34c20e --- /dev/null +++ b/src/app/components/user_details/user_details.rs @@ -0,0 +1,150 @@ +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; +use std::rc::Rc; + +use crate::app::components::utils::wrap_flowbox_item; +use crate::app::components::{display_add_css_provider, AlbumWidget, Component, EventListener}; +use crate::app::{models::*, ListStore}; +use crate::app::{AppEvent, BrowserEvent, Worker}; + +use super::UserDetailsModel; + +mod imp { + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/dev/alextren/Spot/components/user_details.ui")] + pub struct UserDetailsWidget { + #[template_child] + pub scrolled_window: TemplateChild, + + #[template_child] + pub user_name: TemplateChild, + + #[template_child] + pub user_playlists: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for UserDetailsWidget { + const NAME: &'static str = "UserDetailsWidget"; + type Type = super::UserDetailsWidget; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for UserDetailsWidget {} + impl WidgetImpl for UserDetailsWidget {} + impl BoxImpl for UserDetailsWidget {} +} + +glib::wrapper! { + pub struct UserDetailsWidget(ObjectSubclass) @extends gtk::Widget, gtk::Box; +} + +impl UserDetailsWidget { + fn new() -> Self { + display_add_css_provider(resource!("/components/user_details.css")); + glib::Object::new() + } + + fn set_user_name(&self, name: &str) { + self.add_css_class("user__loaded"); + self.imp().user_name.set_text(name); + } + + fn connect_bottom_edge(&self, f: F) + where + F: Fn() + 'static, + { + self.imp() + .scrolled_window + .connect_edge_reached(move |_, pos| { + if let gtk::PositionType::Bottom = pos { + f() + } + }); + } + + fn bind_user_playlists(&self, worker: Worker, store: &ListStore, on_pressed: F) + where + F: Fn(String) + Clone + 'static, + { + self.imp() + .user_playlists + .bind_model(Some(store.unsafe_store()), move |item| { + wrap_flowbox_item(item, |item: &AlbumModel| { + let f = on_pressed.clone(); + let album = AlbumWidget::for_model(item, worker.clone()); + album.connect_album_pressed(clone!(@weak item => move |_| { + f(item.uri()); + })); + album + }) + }); + } +} + +pub struct UserDetails { + model: Rc, + widget: UserDetailsWidget, +} + +impl UserDetails { + pub fn new(model: UserDetailsModel, worker: Worker) -> Self { + model.load_user_details(model.id.clone()); + + let widget = UserDetailsWidget::new(); + let model = Rc::new(model); + + widget.connect_bottom_edge(clone!(@weak model => move || { + model.load_more(); + })); + + if let Some(store) = model.get_list_store() { + widget.bind_user_playlists( + worker, + &store, + clone!(@weak model => move |uri| { + model.open_playlist(uri); + }), + ); + } + + Self { model, widget } + } + + fn update_details(&self) { + if let Some(name) = self.model.get_user_name() { + self.widget.set_user_name(&name); + } + } +} + +impl Component for UserDetails { + fn get_root_widget(&self) -> >k::Widget { + self.widget.as_ref() + } +} + +impl EventListener for UserDetails { + fn on_event(&mut self, event: &AppEvent) { + match event { + AppEvent::BrowserEvent(BrowserEvent::UserDetailsUpdated(id)) + if id == &self.model.id => + { + self.update_details(); + } + _ => {} + } + } +} diff --git a/src/app/components/user_details/user_details_model.rs b/src/app/components/user_details/user_details_model.rs new file mode 100644 index 0000000..45c63f0 --- /dev/null +++ b/src/app/components/user_details/user_details_model.rs @@ -0,0 +1,63 @@ +use std::ops::Deref; +use std::rc::Rc; + +use crate::app::models::*; +use crate::app::state::BrowserAction; +use crate::app::{ActionDispatcher, AppAction, AppModel, ListStore}; + +pub struct UserDetailsModel { + pub id: String, + app_model: Rc, + dispatcher: Box, +} + +impl UserDetailsModel { + pub fn new(id: String, app_model: Rc, dispatcher: Box) -> Self { + Self { + id, + app_model, + dispatcher, + } + } + pub fn get_user_name(&self) -> Option + '_> { + self.app_model + .map_state_opt(|s| s.browser.user_state(&self.id)?.user.as_ref()) + } + + pub fn get_list_store(&self) -> Option> + '_> { + self.app_model + .map_state_opt(|s| Some(&s.browser.user_state(&self.id)?.playlists)) + } + + pub fn load_user_details(&self, id: String) { + let api = self.app_model.get_spotify(); + self.dispatcher + .call_spotify_and_dispatch(move || async move { + api.get_user(&id) + .await + .map(|user| BrowserAction::SetUserDetails(Box::new(user)).into()) + }); + } + + pub fn open_playlist(&self, id: String) { + self.dispatcher.dispatch(AppAction::ViewPlaylist(id)); + } + + pub fn load_more(&self) -> Option<()> { + let api = self.app_model.get_spotify(); + let state = self.app_model.get_state(); + let next_page = &state.browser.user_state(&self.id)?.next_page; + + let id = next_page.data.clone(); + let batch_size = next_page.batch_size; + let offset = next_page.next_offset?; + self.dispatcher + .call_spotify_and_dispatch(move || async move { + api.get_user_playlists(&id, offset, batch_size) + .await + .map(|playlists| BrowserAction::AppendUserPlaylists(id, playlists).into()) + }); + + Some(()) + } +} diff --git a/src/app/components/user_menu/mod.rs b/src/app/components/user_menu/mod.rs new file mode 100644 index 0000000..bf8b1a6 --- /dev/null +++ b/src/app/components/user_menu/mod.rs @@ -0,0 +1,6 @@ +#[allow(clippy::module_inception)] +mod user_menu; +pub use user_menu::*; + +mod user_menu_model; +pub use user_menu_model::*; diff --git a/src/app/components/user_menu/user_menu.rs b/src/app/components/user_menu/user_menu.rs new file mode 100644 index 0000000..dc31beb --- /dev/null +++ b/src/app/components/user_menu/user_menu.rs @@ -0,0 +1,93 @@ +use gettextrs::*; +use gio::{prelude::ActionMapExt, SimpleAction, SimpleActionGroup}; +use gtk::prelude::*; +use std::rc::Rc; + +use super::UserMenuModel; +use crate::app::components::{EventListener, Settings}; +use crate::app::{state::LoginEvent, AppEvent}; + +pub struct UserMenu { + user_button: gtk::MenuButton, + model: Rc, +} + +impl UserMenu { + pub fn new( + user_button: gtk::MenuButton, + settings: Settings, + about: libadwaita::AboutWindow, + model: UserMenuModel, + ) -> Self { + let model = Rc::new(model); + + about.connect_close_request( + clone!(@weak about => @default-return gtk::Inhibit(false), move |_| { + about.set_visible(false); + gtk::Inhibit(true) + }), + ); + + let action_group = SimpleActionGroup::new(); + + action_group.add_action(&{ + let logout = SimpleAction::new("logout", None); + logout.connect_activate(clone!(@weak model => move |_, _| { + model.logout(); + })); + logout + }); + + action_group.add_action(&{ + let settings_action = SimpleAction::new("settings", None); + settings_action.connect_activate(clone!(@weak model => move |_, _| { + settings.show_self(); + })); + settings_action + }); + + action_group.add_action(&{ + let about_action = SimpleAction::new("about", None); + about_action.connect_activate(clone!(@weak about => move |_, _| { + about.present(); + })); + about_action + }); + + user_button.insert_action_group("menu", Some(&action_group)); + + Self { user_button, model } + } + + fn update_menu(&self) { + let menu = gio::Menu::new(); + // translators: This is a menu entry. + menu.append(Some(&gettext("Preferences")), Some("menu.settings")); + // translators: This is a menu entry. + menu.append(Some(&gettext("About")), Some("menu.about")); + // translators: This is a menu entry. + menu.append(Some(&gettext("Quit")), Some("app.quit")); + menu.append(Some(&gettext("Log out")), Some("menu.logout")); + + if let Some(username) = self.model.username() { + let user_menu = gio::Menu::new(); + // translators: This is a menu entry. + user_menu.append(Some(&gettext("Log out")), Some("menu.logout")); + menu.insert_section(0, Some(&username), &user_menu); + } + + self.user_button.set_menu_model(Some(&menu)); + } +} + +impl EventListener for UserMenu { + fn on_event(&mut self, event: &AppEvent) { + match event { + AppEvent::LoginEvent(LoginEvent::LoginCompleted(_)) | AppEvent::Started => { + self.update_menu(); + self.model.fetch_user_playlists(); + } + _ => {} + } + } +} diff --git a/src/app/components/user_menu/user_menu_model.rs b/src/app/components/user_menu/user_menu_model.rs new file mode 100644 index 0000000..99dd39e --- /dev/null +++ b/src/app/components/user_menu/user_menu_model.rs @@ -0,0 +1,52 @@ +use crate::api::clear_user_cache; +use crate::app::credentials::Credentials; +use crate::app::state::{LoginAction, PlaybackAction}; +use crate::app::{ActionDispatcher, AppModel}; +use std::ops::Deref; +use std::rc::Rc; + +pub struct UserMenuModel { + app_model: Rc, + dispatcher: Box, +} + +impl UserMenuModel { + pub fn new(app_model: Rc, dispatcher: Box) -> Self { + Self { + app_model, + dispatcher, + } + } + + pub fn username(&self) -> Option + '_> { + self.app_model + .map_state_opt(|s| s.logged_user.user.as_ref()) + } + + pub fn logout(&self) { + self.dispatcher.dispatch(PlaybackAction::Stop.into()); + self.dispatcher.dispatch_async(Box::pin(async { + let _ = Credentials::logout().await; + let _ = clear_user_cache().await; + Some(LoginAction::Logout.into()) + })); + } + + pub fn fetch_user_playlists(&self) { + let api = self.app_model.get_spotify(); + if let Some(current_user) = self.username() { + let current_user = current_user.clone(); + self.dispatcher + .call_spotify_and_dispatch(move || async move { + api.get_saved_playlists(0, 30).await.map(|playlists| { + let summaries = playlists + .into_iter() + .filter(|p| p.owner.id == current_user) + .map(|p| p.into()) + .collect(); + LoginAction::SetUserPlaylists(summaries).into() + }) + }); + } + } +} diff --git a/src/app/components/utils.rs b/src/app/components/utils.rs new file mode 100644 index 0000000..8d36629 --- /dev/null +++ b/src/app/components/utils.rs @@ -0,0 +1,165 @@ +use gtk::prelude::*; +use std::cell::Cell; +use std::rc::Rc; +use std::time::Duration; + +#[derive(Clone)] +pub struct Clock { + interval_ms: u32, + source: Rc>>, +} + +impl Default for Clock { + fn default() -> Self { + Self::new(1000) + } +} + +impl std::fmt::Debug for Clock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Clock") + .field("interval_ms", &self.interval_ms) + .finish() + } +} + +impl Clock { + pub fn new(interval_ms: u32) -> Self { + Self { + interval_ms, + source: Rc::new(Cell::new(None)), + } + } + + pub fn start(&self, tick: F) { + let new_source = Some(glib::timeout_add_local( + Duration::from_millis(self.interval_ms.into()), + move || { + tick(); + glib::Continue(true) + }, + )); + if let Some(previous_source) = self.source.replace(new_source) { + previous_source.remove(); + } + } + + pub fn stop(&self) { + let new_source = None; + if let Some(previous_source) = self.source.replace(new_source) { + previous_source.remove(); + } + } +} + +#[derive(Clone)] +pub struct Debouncer(Rc>>); + +impl Debouncer { + pub fn new() -> Self { + Self(Rc::new(Cell::new(None))) + } + + pub fn debounce(&self, interval_ms: u32, f: F) { + let source_clone = Rc::downgrade(&self.0); + let new_source = + glib::timeout_add_local(Duration::from_millis(interval_ms.into()), move || { + f(); + if let Some(cell) = source_clone.upgrade() { + cell.set(None); + } + glib::Continue(false) + }); + if let Some(previous_source) = self.0.replace(Some(new_source)) { + previous_source.remove(); + } + } +} + +pub struct Animator { + progress: Rc>, + ease_fn: EasingFn, +} + +pub type AnimatorDefault = Animator f64>; + +impl AnimatorDefault { + fn ease_in_out(x: f64) -> f64 { + match x { + x if x < 0.5 => 0.5 * 2f64.powf(20.0 * x - 10.0), + _ => 0.5 * (2.0 - 2f64.powf(-20.0 * x + 10.0)), + } + } + + pub fn ease_in_out_animator() -> AnimatorDefault { + Animator { + progress: Rc::new(Cell::new(0)), + ease_fn: Self::ease_in_out, + } + } +} + +impl Animator +where + EasingFn: 'static + Copy + Fn(f64) -> f64, +{ + pub fn animate bool + 'static>(&self, steps: u16, f: F) { + self.progress.set(0); + let ease_fn = self.ease_fn; + + let progress = Rc::downgrade(&self.progress); + glib::timeout_add_local(Duration::from_millis(16), move || { + let mut continue_ = false; + if let Some(progress) = progress.upgrade() { + let step = progress.get(); + continue_ = step < steps; + if continue_ { + progress.set(step + 1); + let p = ease_fn(step as f64 / steps as f64); + continue_ = f(p); + } + } + glib::Continue(continue_) + }); + } +} + +pub fn ancestor(widget: &Current) -> Option +where + Current: IsA, + Ancestor: IsA, +{ + widget.parent().and_then(|p| { + p.clone() + .downcast::() + .ok() + .or_else(|| ancestor(&p)) + }) +} + +pub fn wrap_flowbox_item< + Model: glib::IsA, + Widget: gtk::glib::IsA, + F: Fn(&Model) -> Widget, +>( + item: &glib::Object, + f: F, +) -> gtk::Widget { + let item = item.downcast_ref::().unwrap(); + let widget = f(item); + let child = gtk::FlowBoxChild::new(); + child.set_child(Some(&widget)); + child.upcast::() +} + +pub fn format_duration(duration: f64) -> String { + let seconds = (duration / 1000.0) as i32; + let hours = seconds.div_euclid(3600); + let minutes = seconds.div_euclid(60).rem_euclid(60); + let seconds = seconds.rem_euclid(60); + if hours > 0 { + format!("{hours}∶{minutes:02}∶{seconds:02}") + } else { + format!("{minutes}∶{seconds:02}") + } +} diff --git a/src/app/components/window/mod.rs b/src/app/components/window/mod.rs new file mode 100644 index 0000000..04538b4 --- /dev/null +++ b/src/app/components/window/mod.rs @@ -0,0 +1,90 @@ +use gtk::prelude::*; +use std::cell::RefCell; +use std::rc::Rc; + +use crate::app::components::EventListener; +use crate::app::{AppEvent, AppModel}; +use crate::settings::WindowGeometry; + +thread_local! { + static WINDOW_GEOMETRY: RefCell = const { RefCell::new(WindowGeometry { + width: 0, height: 0, is_maximized: false + }) }; +} + +pub struct MainWindow { + initial_window_geometry: WindowGeometry, + window: libadwaita::ApplicationWindow, +} + +impl MainWindow { + pub fn new( + initial_window_geometry: WindowGeometry, + app_model: Rc, + window: libadwaita::ApplicationWindow, + ) -> Self { + window.connect_close_request( + clone!(@weak app_model => @default-return gtk::Inhibit(false), move |window| { + let state = app_model.get_state(); + if state.playback.is_playing() { + window.set_visible(false); + gtk::Inhibit(true) + } else { + gtk::Inhibit(false) + } + }), + ); + + window.connect_default_height_notify(Self::save_window_geometry); + window.connect_default_width_notify(Self::save_window_geometry); + window.connect_maximized_notify(Self::save_window_geometry); + + window.connect_unrealize(|_| { + debug!("saving geometry"); + WINDOW_GEOMETRY.with(|g| g.borrow().save()); + }); + + Self { + initial_window_geometry, + window, + } + } + + fn start(&self) { + self.window.set_default_size( + self.initial_window_geometry.width, + self.initial_window_geometry.height, + ); + if self.initial_window_geometry.is_maximized { + self.window.maximize(); + } + self.window.present(); + } + + fn raise(&self) { + self.window.present(); + } + + fn save_window_geometry(window: &W) { + let (width, height) = window.default_size(); + let is_maximized = window.is_maximized(); + WINDOW_GEOMETRY.with(|g| { + let mut g = g.borrow_mut(); + g.is_maximized = is_maximized; + if !is_maximized { + g.width = width; + g.height = height; + } + }); + } +} + +impl EventListener for MainWindow { + fn on_event(&mut self, event: &AppEvent) { + match event { + AppEvent::Started => self.start(), + AppEvent::Raised => self.raise(), + _ => {} + } + } +} diff --git a/src/app/credentials.rs b/src/app/credentials.rs new file mode 100644 index 0000000..0d315b3 --- /dev/null +++ b/src/app/credentials.rs @@ -0,0 +1,77 @@ +use secret_service::{EncryptionType, Error, SecretService}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, time::SystemTime}; + +static SPOT_ATTR: &str = "spot_credentials"; + +// I'm not sure this is the right way to make credentials identifiable, but hey, it works +fn make_attributes() -> HashMap<&'static str, &'static str> { + let mut attributes = HashMap::new(); + attributes.insert(SPOT_ATTR, "yes"); + attributes +} + +// A (statically accessed) wrapper around the DBUS Secret Service +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct Credentials { + pub username: String, + pub password: String, + pub token: String, + pub token_expiry_time: Option, +} + +impl Credentials { + pub fn token_expired(&self) -> bool { + match self.token_expiry_time { + Some(v) => SystemTime::now() > v, + None => true, + } + } + + pub async fn retrieve() -> Result { + let service = SecretService::connect(EncryptionType::Dh).await?; + let collection = service.get_default_collection().await?; + if collection.is_locked().await? { + collection.unlock().await?; + } + let items = collection.search_items(make_attributes()).await?; + let item = items.first().ok_or(Error::NoResult)?.get_secret().await?; + serde_json::from_slice(&item).map_err(|_| Error::Unavailable) + } + + // Try to clear the credentials + pub async fn logout() -> Result<(), Error> { + let service = SecretService::connect(EncryptionType::Dh).await?; + let collection = service.get_default_collection().await?; + if !collection.is_locked().await? { + let result = collection.search_items(make_attributes()).await?; + let item = result.first().ok_or(Error::NoResult)?; + item.delete().await + } else { + warn!("Keyring is locked -- not clearing credentials"); + Ok(()) + } + } + + pub async fn save(&self) -> Result<(), Error> { + let service = SecretService::connect(EncryptionType::Dh).await?; + let collection = service.get_default_collection().await?; + if collection.is_locked().await? { + collection.unlock().await?; + } + // We simply write our stuct as JSON and send it + info!("Saving credentials"); + let encoded = serde_json::to_vec(&self).unwrap(); + collection + .create_item( + "Spotify Credentials", + make_attributes(), + &encoded, + true, + "text/plain", + ) + .await?; + info!("Saved credentials"); + Ok(()) + } +} diff --git a/src/app/dispatch.rs b/src/app/dispatch.rs new file mode 100644 index 0000000..fb349dd --- /dev/null +++ b/src/app/dispatch.rs @@ -0,0 +1,128 @@ +use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; +use futures::future::BoxFuture; +use futures::future::Future; +use futures::stream::StreamExt; +use std::pin::Pin; + +use super::AppAction; + +// A wrapper around an MPSC sender to send AppActions synchronously or asynchronously +// It is a trait because I guess I wanted to be able to stub it, but see how that went... +pub trait ActionDispatcher { + fn dispatch(&self, action: AppAction); + fn dispatch_many(&self, actions: Vec); + fn dispatch_async(&self, action: BoxFuture<'static, Option>); + fn dispatch_many_async(&self, actions: BoxFuture<'static, Vec>); + // Can't have impl Clone easily so there you go + fn box_clone(&self) -> Box; +} + +#[derive(Clone)] +pub struct ActionDispatcherImpl { + sender: UnboundedSender, + worker: Worker, +} + +impl ActionDispatcherImpl { + pub fn new(sender: UnboundedSender, worker: Worker) -> Self { + Self { sender, worker } + } +} + +impl ActionDispatcher for ActionDispatcherImpl { + fn dispatch(&self, action: AppAction) { + self.sender.unbounded_send(action).unwrap(); + } + + fn dispatch_many(&self, actions: Vec) { + for action in actions.into_iter() { + self.sender.unbounded_send(action).unwrap(); + } + } + + fn dispatch_async(&self, action: BoxFuture<'static, Option>) { + let clone = self.sender.clone(); + self.worker.send_task(async move { + if let Some(action) = action.await { + clone.unbounded_send(action).unwrap(); + } + }); + } + + fn dispatch_many_async(&self, actions: BoxFuture<'static, Vec>) { + let clone = self.sender.clone(); + self.worker.send_task(async move { + for action in actions.await.into_iter() { + clone.unbounded_send(action).unwrap(); + } + }); + } + + fn box_clone(&self) -> Box { + Box::new(self.clone()) + } +} + +// Funky name for a mere wrapper around an MPSC send/recv pair +pub struct DispatchLoop { + receiver: UnboundedReceiver, + sender: UnboundedSender, +} + +impl DispatchLoop { + pub fn new() -> Self { + let (sender, receiver) = unbounded::(); + Self { receiver, sender } + } + + pub fn make_dispatcher(&self) -> UnboundedSender { + self.sender.clone() + } + + pub async fn attach(self, mut handler: impl FnMut(AppAction)) { + self.receiver + .for_each(|action| { + handler(action); + async {} + }) + .await; + } +} + +pub type FutureTask = Pin + Send>>; +pub type FutureLocalTask = Pin>>; + +// The Worker (see below) is a glorified way to send an async task to the GLib(rs) future executor +pub fn spawn_task_handler(context: &glib::MainContext) -> Worker { + let (future_local_sender, future_local_receiver) = unbounded::(); + context.spawn_local_with_priority( + glib::source::PRIORITY_DEFAULT_IDLE, + future_local_receiver.for_each(|t| t), + ); + + let (future_sender, future_receiver) = unbounded::(); + context.spawn_with_priority( + glib::source::PRIORITY_DEFAULT_IDLE, + future_receiver.for_each(|t| t), + ); + + Worker(future_local_sender, future_sender) +} + +// Again, fancy name for an MPSC sender +// Actually two of them, in case you need to send local futures (no Send needed) +#[derive(Clone)] +pub struct Worker( + UnboundedSender, + UnboundedSender, +); + +impl Worker { + pub fn send_local_task + 'static>(&self, task: T) -> Option<()> { + self.0.unbounded_send(Box::pin(task)).ok() + } + + pub fn send_task + Send + 'static>(&self, task: T) -> Option<()> { + self.1.unbounded_send(Box::pin(task)).ok() + } +} diff --git a/src/app/list_store.rs b/src/app/list_store.rs new file mode 100644 index 0000000..93ad6b0 --- /dev/null +++ b/src/app/list_store.rs @@ -0,0 +1,113 @@ +use gio::prelude::*; +use glib::clone::{Downgrade, Upgrade}; +use std::iter::Iterator; +use std::marker::PhantomData; + +// A typed wrapper around a GIO ListStore +pub struct ListStore { + store: gio::ListStore, + _marker: PhantomData, +} + +pub struct WeakListStore { + store: ::Weak, + _marker: PhantomData, +} + +impl ListStore +where + GType: IsA, +{ + pub fn new() -> Self { + Self { + store: gio::ListStore::new(GType::static_type()), + _marker: PhantomData, + } + } + + pub fn unsafe_store(&self) -> &gio::ListStore { + &self.store + } + + pub fn prepend(&mut self, elements: impl Iterator) { + let upcast_vec: Vec = elements.map(|e| e.upcast::()).collect(); + self.store.splice(0, 0, &upcast_vec[..]); + } + + pub fn extend(&mut self, elements: impl Iterator) { + let upcast_vec: Vec = elements.map(|e| e.upcast::()).collect(); + self.store.splice(self.store.n_items(), 0, &upcast_vec[..]); + } + + pub fn replace_all(&mut self, elements: impl Iterator) { + let upcast_vec: Vec = elements.map(|e| e.upcast::()).collect(); + self.store.splice(0, self.store.n_items(), &upcast_vec[..]); + } + + pub fn insert(&mut self, position: u32, element: GType) { + self.store.insert(position, &element); + } + + pub fn remove(&mut self, position: u32) { + self.store.remove(position); + } + + pub fn get(&self, index: u32) -> GType { + self.store.item(index).unwrap().downcast::().unwrap() + } + + pub fn iter(&self) -> impl Iterator + '_ { + let store = &self.store; + let count = store.n_items(); + (0..count).map(move |i| self.get(i)) + } + + pub fn len(&self) -> usize { + self.store.n_items() as usize + } + + // Quick and dirty comparison between the list store and a slice of object that can be compared + // with the contents of the store using some function F. + // Not so great but eh + pub fn eq(&self, other: &[O], comparison: F) -> bool + where + F: Fn(>ype, &O) -> bool, + { + self.len() == other.len() + && self + .iter() + .zip(other.iter()) + .all(|(left, right)| comparison(&left, right)) + } +} + +impl Clone for ListStore { + fn clone(&self) -> Self { + Self { + store: self.store.clone(), + _marker: PhantomData, + } + } +} + +impl Downgrade for ListStore { + type Weak = WeakListStore; + + fn downgrade(&self) -> Self::Weak { + Self::Weak { + store: Downgrade::downgrade(&self.store), + _marker: PhantomData, + } + } +} + +impl Upgrade for WeakListStore { + type Strong = ListStore; + + fn upgrade(&self) -> Option { + Some(Self::Strong { + store: self.store.upgrade()?, + _marker: PhantomData, + }) + } +} diff --git a/src/app/loader.rs b/src/app/loader.rs new file mode 100644 index 0000000..eacaffa --- /dev/null +++ b/src/app/loader.rs @@ -0,0 +1,100 @@ +use crate::api::cache::*; +use gdk_pixbuf::traits::PixbufLoaderExt; +use gdk_pixbuf::{Pixbuf, PixbufLoader}; +use isahc::config::Configurable; +use isahc::{AsyncBody, AsyncReadResponseExt, HttpClient, Response}; +use std::collections::hash_map::DefaultHasher; +use std::hash::Hasher; +use std::io::{Error, ErrorKind, Write}; + +// A wrapper to be able to implement the Write trait on a PixbufLoader +struct LocalPixbufLoader<'a>(&'a PixbufLoader); + +impl<'a> Write for LocalPixbufLoader<'a> { + fn write(&mut self, buf: &[u8]) -> Result { + self.0 + .write(buf) + .map_err(|e| Error::new(ErrorKind::Other, format!("glib error: {e}")))?; + Ok(buf.len()) + } + + fn flush(&mut self) -> Result<(), Error> { + self.0 + .close() + .map_err(|e| Error::new(ErrorKind::Other, format!("glib error: {e}")))?; + Ok(()) + } +} + +// A helper to load remote images, with simple cache management +pub struct ImageLoader { + cache: CacheManager, +} + +impl ImageLoader { + pub fn new() -> Self { + Self { + cache: CacheManager::for_dir("spot/img").unwrap(), + } + } + + // Downloaded images are simply named [hash of url].[file extension] + fn resource_for(url: &str, ext: &str) -> String { + let mut hasher = DefaultHasher::new(); + hasher.write(url.as_bytes()); + let hashed = hasher.finish().to_string(); + hashed + "." + ext + } + + async fn get_image(url: &str) -> Option> { + let mut builder = HttpClient::builder(); + if cfg!(debug_assertions) { + builder = builder.ssl_options(isahc::config::SslOption::DANGER_ACCEPT_INVALID_CERTS); + } + let client = builder.build().unwrap(); + client.get_async(url).await.ok() + } + + pub async fn load_remote( + &self, + url: &str, + ext: &str, + width: i32, + height: i32, + ) -> Option { + let resource = Self::resource_for(url, ext); + let pixbuf_loader = PixbufLoader::new(); + pixbuf_loader.set_size(width, height); + let mut loader = LocalPixbufLoader(&pixbuf_loader); + + // Try to read from cache first, ignoring possible expiry + match self + .cache + .read_cache_file(&resource[..], CachePolicy::IgnoreExpiry) + .await + { + // Write content of cache file to the pixbuf loader if the cache contained something + Ok(CacheFile::Fresh(buffer)) => { + loader.write_all(&buffer[..]).ok()?; + } + // Otherwise, get image over HTTP + _ => { + if let Some(mut resp) = Self::get_image(url).await { + let mut buffer = vec![]; + // Copy the image to a buffer... + resp.copy_to(&mut buffer).await.ok()?; + // ... copy the buffer to the loader... + loader.write_all(&buffer[..]).ok()?; + // ... but also save that buffer to cache + self.cache + .write_cache_file(&resource[..], &buffer[..], CacheExpiry::Never) + .await + .ok()?; + } + } + }; + + pixbuf_loader.close().ok()?; + pixbuf_loader.pixbuf() + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs new file mode 100644 index 0000000..14e1fec --- /dev/null +++ b/src/app/mod.rs @@ -0,0 +1,271 @@ +use crate::api::CachedSpotifyClient; +use crate::settings::SpotSettings; +use futures::channel::mpsc::UnboundedSender; +use std::rc::Rc; +use std::sync::Arc; + +pub mod dispatch; +pub use dispatch::{ActionDispatcher, ActionDispatcherImpl, DispatchLoop, Worker}; + +pub mod components; +use components::*; + +pub mod models; + +mod list_store; +pub use list_store::*; + +pub mod state; +pub use state::{AppAction, AppEvent, AppModel, AppState, BrowserAction, BrowserEvent}; + +mod batch_loader; +pub use batch_loader::*; + +pub mod credentials; +pub mod loader; + +pub mod rng; +pub use rng::LazyRandomIndex; + +// Where all the app logic happens +pub struct App { + settings: SpotSettings, + // The builder instance used to properly configure all the widgets created at startup + builder: gtk::Builder, + // All the "components" that will be notified of things happening throughout the app + components: Vec>, + // Holds the app state + model: Rc, + // Allows sending actions that are handled by the model above + sender: UnboundedSender, + worker: Worker, +} + +impl App { + pub fn new( + settings: SpotSettings, + builder: gtk::Builder, + sender: UnboundedSender, + worker: Worker, + ) -> Self { + let state = AppState::new(); + let spotify_client = Arc::new(CachedSpotifyClient::new()); + let model = Rc::new(AppModel::new(state, spotify_client)); + + // Non widget components + let components: Vec> = vec![ + App::make_player_notifier( + Rc::clone(&model), + &settings, + Box::new(ActionDispatcherImpl::new(sender.clone(), worker.clone())), + sender.clone(), + ), + App::make_dbus(Rc::clone(&model), sender.clone()), + ]; + + Self { + settings, + builder, + components, + model, + sender, + worker, + } + } + + fn add_ui_components(&mut self) { + // Most components will need some or all of these to work + // ie some way to retrieve widgets + let builder = &self.builder; + // ...some way to read the app state + let model = &self.model; + // ...some way to handle various asynchronous tasks + let worker = &self.worker; + // ...some (basic) way to send actions that will change the app state + let sender = &self.sender; + // ...ALSO some way to send actions, but more conveniently + let dispatcher = Box::new(ActionDispatcherImpl::new(sender.clone(), worker.clone())); + + // All components that will be available initially + let mut components: Vec> = vec![ + App::make_window(&self.settings, builder, Rc::clone(model)), + App::make_selection_toolbar(builder, Rc::clone(model), dispatcher.box_clone()), + App::make_playback( + builder, + Rc::clone(model), + dispatcher.box_clone(), + worker.clone(), + ), + App::make_login(builder, dispatcher.box_clone(), worker.clone()), + App::make_navigation( + builder, + Rc::clone(model), + dispatcher.box_clone(), + worker.clone(), + ), + App::make_search_button(builder, dispatcher.box_clone()), + App::make_user_menu(builder, Rc::clone(model), dispatcher), + App::make_notification(builder), + ]; + + self.components.append(&mut components); + } + + // A component that listens to what's happening in the app, and translates it for the actual player + fn make_player_notifier( + app_model: Rc, + settings: &SpotSettings, + dispatcher: Box, + sender: UnboundedSender, + ) -> Box { + let api = app_model.get_spotify(); + Box::new(PlayerNotifier::new( + app_model, + dispatcher, + // Either communications with the librespot player + crate::player::start_player_service(settings.player_settings.clone(), sender.clone()), + // or with a Spotify Connect device + crate::connect::start_connect_server(api, sender), + )) + } + + // A component to handle anything DBUS related + fn make_dbus( + app_model: Rc, + sender: UnboundedSender, + ) -> Box { + Box::new(crate::dbus::start_dbus_server(app_model, sender)) + } + + fn make_window( + settings: &SpotSettings, + builder: >k::Builder, + app_model: Rc, + ) -> Box { + let window: libadwaita::ApplicationWindow = builder.object("window").unwrap(); + Box::new(MainWindow::new(settings.window.clone(), app_model, window)) + } + + fn make_navigation( + builder: >k::Builder, + app_model: Rc, + dispatcher: Box, + worker: Worker, + ) -> Box { + let leaflet: libadwaita::Leaflet = builder.object("leaflet").unwrap(); + let navigation_stack: gtk::Stack = builder.object("navigation_stack").unwrap(); + let home_listbox: gtk::ListBox = builder.object("home_listbox").unwrap(); + let model = NavigationModel::new(Rc::clone(&app_model), dispatcher.box_clone()); + // This is where components that are not created initially will be assembled + let screen_factory = ScreenFactory::new( + Rc::clone(&app_model), + dispatcher.box_clone(), + worker, + leaflet.clone(), + ); + Box::new(Navigation::new( + model, + leaflet, + navigation_stack, + home_listbox, + screen_factory, + )) + } + + fn make_login( + builder: >k::Builder, + dispatcher: Box, + worker: Worker, + ) -> Box { + let parent: gtk::Window = builder.object("window").unwrap(); + let model = LoginModel::new(dispatcher, worker); + Box::new(Login::new(parent, model)) + } + + fn make_selection_toolbar( + builder: >k::Builder, + app_model: Rc, + dispatcher: Box, + ) -> Box { + Box::new(SelectionToolbar::new( + SelectionToolbarModel::new(app_model, dispatcher), + builder.object("selection_toolbar").unwrap(), + )) + } + + fn make_playback( + builder: >k::Builder, + app_model: Rc, + dispatcher: Box, + worker: Worker, + ) -> Box { + let model = PlaybackModel::new(app_model, dispatcher); + Box::new(PlaybackControl::new( + model, + builder.object("playback").unwrap(), + worker, + )) + } + + fn make_search_button( + builder: >k::Builder, + dispatcher: Box, + ) -> Box { + let search_button: gtk::Button = builder.object("search_button").unwrap(); + let model = SearchBarModel(dispatcher); + Box::new(SearchButton::new(model, search_button)) + } + + fn make_user_menu( + builder: >k::Builder, + app_model: Rc, + dispatcher: Box, + ) -> Box { + let parent: gtk::Window = builder.object("window").unwrap(); + let settings_model = SettingsModel::new(app_model.clone(), dispatcher.box_clone()); + let settings = Settings::new(parent, settings_model); + + let button: gtk::MenuButton = builder.object("user").unwrap(); + let about: libadwaita::AboutWindow = builder.object("about").unwrap(); + let model = UserMenuModel::new(app_model, dispatcher); + let user_menu = UserMenu::new(button, settings, about, model); + Box::new(user_menu) + } + + fn make_notification(builder: >k::Builder) -> Box { + let toast_overlay: libadwaita::ToastOverlay = builder.object("main").unwrap(); + Box::new(Notification::new(toast_overlay)) + } + + // Main handler called in a loop + fn handle(&mut self, action: AppAction) { + let starting = matches!(&action, &AppAction::Start); + + // Update the state based on an incoming action + // and obtain events representing what that mutation entailed... + let events = self.model.update_state(action); + + // (AppAction::Start is special and is used to setup the initial components) + if !events.is_empty() && starting { + self.add_ui_components(); + } + + // ...and notify every component that we know. + // They'll be responsible for passing down these events, if they feel like it. + for event in events.iter() { + for component in self.components.iter_mut() { + component.on_event(event); + } + } + } + + // Here is the loop + pub async fn attach(mut self, dispatch_loop: DispatchLoop) { + let app = &mut self; + dispatch_loop + .attach(move |action| { + app.handle(action); + }) + .await; + } +} diff --git a/src/app/models/album_model.rs b/src/app/models/album_model.rs new file mode 100644 index 0000000..ce88606 --- /dev/null +++ b/src/app/models/album_model.rs @@ -0,0 +1,73 @@ +#![allow(clippy::all)] + +use gio::prelude::*; +use glib::subclass::prelude::*; +use glib::Properties; + +// UI model! +// Despite the name, it can represent a playlist as well +glib::wrapper! { + pub struct AlbumModel(ObjectSubclass); +} + +impl AlbumModel { + pub fn new( + artist: &String, + album: &String, + year: Option, + cover: Option<&String>, + uri: &String, + ) -> AlbumModel { + let year = &year.unwrap_or(0); + glib::Object::builder() + .property("artist", artist) + .property("album", album) + .property("year", year) + .property("cover", &cover) + .property("uri", uri) + .build() + } +} + +mod imp { + + use super::*; + + use std::cell::{Cell, RefCell}; + + #[derive(Default, Properties)] + #[properties(wrapper_type = super::AlbumModel)] + pub struct AlbumModel { + #[property(get, set)] + album: RefCell, + #[property(get, set)] + artist: RefCell, + #[property(get, set)] + year: Cell, + #[property(get, set)] + cover: RefCell>, + #[property(get, set)] + uri: RefCell, + } + + #[glib::object_subclass] + impl ObjectSubclass for AlbumModel { + const NAME: &'static str = "AlbumModel"; + type Type = super::AlbumModel; + type ParentType = glib::Object; + } + + impl ObjectImpl for AlbumModel { + fn properties() -> &'static [glib::ParamSpec] { + Self::derived_properties() + } + + fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { + self.derived_set_property(id, value, pspec) + } + + fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { + self.derived_property(id, pspec) + } + } +} diff --git a/src/app/models/artist_model.rs b/src/app/models/artist_model.rs new file mode 100644 index 0000000..b413fda --- /dev/null +++ b/src/app/models/artist_model.rs @@ -0,0 +1,58 @@ +#![allow(clippy::all)] + +use gio::prelude::*; +use glib::subclass::prelude::*; +use glib::Properties; + +// UI model! +glib::wrapper! { + pub struct ArtistModel(ObjectSubclass); +} + +impl ArtistModel { + pub fn new(artist: &str, image: &Option, id: &str) -> ArtistModel { + glib::Object::builder() + .property("artist", &artist) + .property("image", image) + .property("id", &id) + .build() + } +} + +mod imp { + + use super::*; + use std::cell::RefCell; + + #[derive(Default, Properties)] + #[properties(wrapper_type = super::ArtistModel)] + pub struct ArtistModel { + #[property(get, set)] + artist: RefCell, + #[property(get, set)] + image: RefCell>, + #[property(get, set)] + id: RefCell, + } + + #[glib::object_subclass] + impl ObjectSubclass for ArtistModel { + const NAME: &'static str = "ArtistModel"; + type Type = super::ArtistModel; + type ParentType = glib::Object; + } + + impl ObjectImpl for ArtistModel { + fn properties() -> &'static [glib::ParamSpec] { + Self::derived_properties() + } + + fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { + self.derived_set_property(id, value, pspec) + } + + fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { + self.derived_property(id, pspec) + } + } +} diff --git a/src/app/models/main.rs b/src/app/models/main.rs new file mode 100644 index 0000000..27da151 --- /dev/null +++ b/src/app/models/main.rs @@ -0,0 +1,313 @@ +use std::{ + hash::{Hash, Hasher}, + str::FromStr, +}; + +use crate::app::SongsSource; + +// A batch of whatever +#[derive(Clone, Copy, Debug)] +pub struct Batch { + // What offset does the batch start at + pub offset: usize, + // How many elements + pub batch_size: usize, + // Total number of elements if we had all batches + pub total: usize, +} + +impl Batch { + pub fn first_of_size(batch_size: usize) -> Self { + Self { + offset: 0, + batch_size, + total: 0, + } + } + + pub fn next(self) -> Option { + let Self { + offset, + batch_size, + total, + } = self; + + Some(Self { + offset: offset + batch_size, + batch_size, + total, + }) + .filter(|b| b.offset < total) + } +} + +// "Something"Ref models usually boil down to an ID/url + a display name + +#[derive(Clone, Debug)] +pub struct UserRef { + pub id: String, + pub display_name: String, +} + +#[derive(Clone, Debug)] +pub struct ArtistRef { + pub id: String, + pub name: String, +} + +#[derive(Clone, Debug)] +pub struct AlbumRef { + pub id: String, + pub name: String, +} + +#[derive(Clone, Debug)] +pub struct SearchResults { + pub albums: Vec, + pub artists: Vec, +} + +#[derive(Clone, Debug)] +pub struct AlbumDescription { + pub id: String, + pub title: String, + pub artists: Vec, + pub release_date: Option, + pub art: Option, + pub songs: SongBatch, + pub is_liked: bool, +} + +impl AlbumDescription { + pub fn artists_name(&self) -> String { + self.artists + .iter() + .map(|a| a.name.to_string()) + .collect::>() + .join(", ") + } + + pub fn year(&self) -> Option { + self.release_date + .as_ref() + .and_then(|date| date.split('-').next()) + .and_then(|y| u32::from_str(y).ok()) + } +} + +#[derive(Clone, Debug)] +pub struct AlbumFullDescription { + pub description: AlbumDescription, + pub release_details: AlbumReleaseDetails, +} + +#[derive(Clone, Debug)] +pub struct AlbumReleaseDetails { + pub label: String, + pub copyright_text: String, + pub total_tracks: usize, +} + +#[derive(Clone, Debug)] +pub struct PlaylistDescription { + pub id: String, + pub title: String, + pub art: Option, + pub songs: SongBatch, + pub owner: UserRef, +} + +#[derive(Clone, Copy, Debug)] +pub enum ConnectDeviceKind { + Phone, + Computer, + Speaker, + Other, +} + +#[derive(Clone, Debug)] +pub struct ConnectDevice { + pub id: String, + pub label: String, + pub kind: ConnectDeviceKind, +} + +#[derive(Clone, Debug)] +pub struct PlaylistSummary { + pub id: String, + pub title: String, +} + +#[derive(Clone, Debug)] +pub struct SongDescription { + pub id: String, + pub track_number: Option, + pub uri: String, + pub title: String, + pub artists: Vec, + pub album: AlbumRef, + pub duration: u32, + pub art: Option, +} + +impl SongDescription { + pub fn artists_name(&self) -> String { + self.artists + .iter() + .map(|a| a.name.to_string()) + .collect::>() + .join(", ") + } +} + +impl Hash for SongDescription { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +#[derive(Copy, Clone, Default)] +pub struct SongState { + pub is_playing: bool, + pub is_selected: bool, +} + +// A batch of SONGS +#[derive(Debug, Clone)] +pub struct SongBatch { + pub songs: Vec, + pub batch: Batch, +} + +impl SongBatch { + pub fn empty() -> Self { + Self { + songs: vec![], + batch: Batch::first_of_size(1), + } + } + + pub fn resize(self, batch_size: usize) -> Vec { + let SongBatch { mut songs, batch } = self; + // Growing a batch is easy... + if batch_size > batch.batch_size { + let new_batch = Batch { + batch_size, + ..batch + }; + vec![Self { + songs, + batch: new_batch, + }] + // Shrinking is not! + // We have to split the batch in multiple batches + } else { + let n = songs.len(); + let iter_count = (n + batch_size - 1) / batch_size; + (0..iter_count) + .map(|i| { + let offset = batch.offset + i * batch_size; + let new_batch = Batch { + offset, + total: batch.total, + batch_size, + }; + let drain_upper = usize::min(batch_size, songs.len()); + let new_songs = songs.drain(0..drain_upper).collect(); + Self { + songs: new_songs, + batch: new_batch, + } + }) + .collect() + } + } +} + +#[derive(Clone, Debug)] +pub struct ArtistDescription { + pub id: String, + pub name: String, + pub albums: Vec, + pub top_tracks: Vec, +} + +#[derive(Clone, Debug)] +pub struct ArtistSummary { + pub id: String, + pub name: String, + pub photo: Option, +} + +#[derive(Clone, Debug)] +pub struct UserDescription { + pub id: String, + pub name: String, + pub playlists: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RepeatMode { + Song, + Playlist, + None, +} + +#[derive(Clone, Debug)] +pub struct ConnectPlayerState { + pub is_playing: bool, + #[allow(dead_code)] + pub source: Option, + pub current_song_id: Option, + pub progress_ms: u32, + pub repeat: RepeatMode, + pub shuffle: bool, +} + +impl Default for ConnectPlayerState { + fn default() -> Self { + Self { + is_playing: false, + source: None, + current_song_id: None, + progress_ms: 0, + repeat: RepeatMode::None, + shuffle: false, + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + fn song(id: &str) -> SongDescription { + SongDescription { + id: id.to_string(), + uri: "".to_string(), + title: "Title".to_string(), + artists: vec![], + album: AlbumRef { + id: "".to_string(), + name: "".to_string(), + }, + duration: 1000, + art: None, + track_number: None, + } + } + + #[test] + fn resize_batch() { + let batch = SongBatch { + songs: vec![song("1"), song("2"), song("3"), song("4")], + batch: Batch::first_of_size(4), + }; + + let batches = batch.resize(2); + assert_eq!(batches.len(), 2); + assert_eq!(&batches.get(0).unwrap().songs.get(0).unwrap().id, "1"); + assert_eq!(&batches.get(1).unwrap().songs.get(0).unwrap().id, "3"); + } +} diff --git a/src/app/models/mod.rs b/src/app/models/mod.rs new file mode 100644 index 0000000..e213455 --- /dev/null +++ b/src/app/models/mod.rs @@ -0,0 +1,68 @@ +// Domain models +mod main; +pub use main::*; + +// UI models (GObject) +mod songs; +pub use songs::*; + +mod album_model; +pub use album_model::*; + +mod artist_model; +pub use artist_model::*; + +impl From<&AlbumDescription> for AlbumModel { + fn from(album: &AlbumDescription) -> Self { + AlbumModel::new( + &album.artists_name(), + &album.title, + album.year(), + album.art.as_ref(), + &album.id, + ) + } +} + +impl From for AlbumModel { + fn from(album: AlbumDescription) -> Self { + Self::from(&album) + } +} + +impl From<&PlaylistDescription> for AlbumModel { + fn from(playlist: &PlaylistDescription) -> Self { + AlbumModel::new( + &playlist.owner.display_name, + &playlist.title, + // Playlists do not have their released date since they are expected to be updated anytime. + None, + playlist.art.as_ref(), + &playlist.id, + ) + } +} + +impl From for PlaylistSummary { + fn from(PlaylistDescription { id, title, .. }: PlaylistDescription) -> Self { + Self { id, title } + } +} + +impl From for AlbumModel { + fn from(playlist: PlaylistDescription) -> Self { + Self::from(&playlist) + } +} + +impl From for SongModel { + fn from(song: SongDescription) -> Self { + SongModel::new(song) + } +} + +impl From<&SongDescription> for SongModel { + fn from(song: &SongDescription) -> Self { + SongModel::new(song.clone()) + } +} diff --git a/src/app/models/songs/mod.rs b/src/app/models/songs/mod.rs new file mode 100644 index 0000000..d80a182 --- /dev/null +++ b/src/app/models/songs/mod.rs @@ -0,0 +1,9 @@ +// The underlying data structure for a list of songs +mod support; + +// A GObject wrapper around that list +mod song_list_model; +pub use song_list_model::*; + +mod song_model; +pub use song_model::*; diff --git a/src/app/models/songs/song_list_model.rs b/src/app/models/songs/song_list_model.rs new file mode 100644 index 0000000..43fc132 --- /dev/null +++ b/src/app/models/songs/song_list_model.rs @@ -0,0 +1,255 @@ +use gio::prelude::*; +use gio::ListModel; +use glib::Properties; +use glib::StaticType; +use gtk::subclass::prelude::*; +use std::cell::{Cell, Ref, RefCell, RefMut}; + +use super::support::*; +use crate::app::models::*; + +// A struct to perform multiple mutations on a SongListModel +// Eventually commit() must be called to send an update signal with merged affected ranges +#[must_use] +pub struct SongListModelPending<'a> { + change: Option, + song_list_model: &'a mut SongListModel, +} + +impl<'a> SongListModelPending<'a> { + fn new(change: Option, song_list_model: &'a mut SongListModel) -> Self { + Self { + change, + song_list_model, + } + } + + pub fn and(self, op: Op) -> Self + where + Op: FnOnce(&mut SongListModel) -> SongListModelPending<'_> + 'static, + { + let Self { + change, + song_list_model, + } = self; + + let new_change = op(song_list_model).change; + + let merged_change = if let (Some(change), Some(new_change)) = (change, new_change) { + Some(change.merge(new_change)) + } else { + change.or(new_change) + }; + + Self { + change: merged_change, + song_list_model, + } + } + + pub fn commit(self) -> bool { + let Self { + change, + song_list_model, + } = self; + song_list_model.notify_changes(change); + change.is_some() + } +} + +// A GObject wrapper around the SongList +glib::wrapper! { + pub struct SongListModel(ObjectSubclass) @implements gio::ListModel; +} + +impl SongListModel { + pub fn new(batch_size: u32) -> Self { + glib::Object::builder() + .property("batch-size", batch_size) + .build() + } + + fn inner_mut(&mut self) -> RefMut { + self.imp().get_mut() + } + + fn inner(&self) -> Ref { + self.imp().get() + } + + fn notify_changes(&self, changes: impl IntoIterator + 'static) { + // Eh, not great but that works + if cfg!(not(test)) { + glib::source::idle_add_local_once(clone!(@weak self as s => move || { + for ListRangeUpdate(a, b, c) in changes.into_iter() { + debug!("pos {}, removed {}, added {}", a, b, c); + s.items_changed(a as u32, b as u32, c as u32); + } + })); + } + } + + pub fn for_each(&self, f: F) + where + F: Fn(usize, &SongModel), + { + for (i, song) in self.inner().iter().enumerate() { + f(i, song); + } + } + + pub fn collect(&self) -> Vec { + self.inner().iter().map(|s| s.into_description()).collect() + } + + pub fn map_collect(&self, map: impl Fn(SongDescription) -> T) -> Vec { + self.inner() + .iter() + .map(|s| map(s.into_description())) + .collect() + } + + pub fn add(&mut self, song_batch: SongBatch) -> SongListModelPending { + let range = self.inner_mut().add(song_batch); + SongListModelPending::new(range, self) + } + + pub fn get(&self, id: &str) -> Option { + self.inner().get(id).cloned() + } + + pub fn index(&self, i: usize) -> Option { + self.inner().index(i).cloned() + } + + pub fn index_continuous(&self, i: usize) -> Option { + self.inner().index_continuous(i).cloned() + } + + pub fn song_batch_for(&self, i: usize) -> Option { + self.inner().song_batch_for(i) + } + + pub fn last_batch(&self) -> Option { + self.inner().last_batch() + } + + pub fn needed_batch_for(&self, i: usize) -> Option { + self.inner().needed_batch_for(i) + } + + pub fn partial_len(&self) -> usize { + self.inner().partial_len() + } + + pub fn len(&self) -> usize { + self.inner().len() + } + + pub fn append(&mut self, songs: Vec) -> SongListModelPending { + let range = self.inner_mut().append(songs); + SongListModelPending::new(Some(range), self) + } + + pub fn prepend(&mut self, songs: Vec) -> SongListModelPending { + let range = self.inner_mut().prepend(songs); + SongListModelPending::new(Some(range), self) + } + + pub fn find_index(&self, song_id: &str) -> Option { + self.inner().find_index(song_id) + } + + pub fn remove(&mut self, ids: &[String]) -> SongListModelPending { + let change = self.inner_mut().remove(ids); + SongListModelPending::new(Some(change), self) + } + + pub fn move_down(&mut self, a: usize) -> SongListModelPending { + let swap = self.inner_mut().swap(a + 1, a); + SongListModelPending::new(swap, self) + } + + pub fn move_up(&mut self, a: usize) -> SongListModelPending { + let swap = self.inner_mut().swap(a - 1, a); + SongListModelPending::new(swap, self) + } + + pub fn clear(&mut self) -> SongListModelPending { + let removed = self.inner_mut().clear(); + SongListModelPending::new(Some(removed), self) + } +} + +mod imp { + + use super::*; + + #[derive(Default, Properties)] + #[properties(wrapper_type = super::SongListModel)] + pub struct SongListModel { + #[property(get, set = Self::set_batch_size, name = "batch-size")] + batch_size: Cell, + song_list: RefCell>, + } + + impl SongListModel { + fn set_batch_size(&self, batch_size: u32) { + self.batch_size.set(batch_size); + self.song_list + .replace(Some(SongList::new_sized(batch_size as usize))); + } + } + + #[glib::object_subclass] + impl ObjectSubclass for SongListModel { + const NAME: &'static str = "SongList"; + type Type = super::SongListModel; + type ParentType = glib::Object; + type Interfaces = (ListModel,); + } + + impl ObjectImpl for SongListModel { + fn properties() -> &'static [glib::ParamSpec] { + Self::derived_properties() + } + + fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { + self.derived_set_property(id, value, pspec); + } + + fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { + self.derived_property(id, pspec) + } + } + + impl ListModelImpl for SongListModel { + fn item_type(&self) -> glib::Type { + SongModel::static_type() + } + + fn n_items(&self) -> u32 { + self.get().partial_len() as u32 + } + + fn item(&self, position: u32) -> Option { + self.get() + .index_continuous(position as usize) + .map(|m| m.clone().upcast()) + } + } + + impl SongListModel { + pub fn get_mut(&self) -> RefMut { + RefMut::map(self.song_list.borrow_mut(), |s| { + s.as_mut().expect("set at construction") + }) + } + + pub fn get(&self) -> Ref { + Ref::map(self.song_list.borrow(), |s| { + s.as_ref().expect("set at construction") + }) + } + } +} diff --git a/src/app/models/songs/song_model.rs b/src/app/models/songs/song_model.rs new file mode 100644 index 0000000..3cb21e6 --- /dev/null +++ b/src/app/models/songs/song_model.rs @@ -0,0 +1,255 @@ +#![allow(clippy::all)] + +use gio::prelude::*; +use glib::{subclass::prelude::*, SignalHandlerId}; +use std::{cell::Ref, ops::Deref}; + +use crate::app::components::utils::format_duration; +use crate::app::models::*; + +// UI model for a song +glib::wrapper! { + pub struct SongModel(ObjectSubclass); +} + +impl SongModel { + pub fn new(song: SongDescription) -> Self { + let o: Self = glib::Object::new(); + o.imp().song.replace(Some(song)); + o + } + + pub fn set_playing(&self, is_playing: bool) { + self.set_property("playing", is_playing); + } + + pub fn set_selected(&self, is_selected: bool) { + self.set_property("selected", is_selected); + } + + pub fn get_playing(&self) -> bool { + self.property("playing") + } + + pub fn get_selected(&self) -> bool { + self.property("selected") + } + + pub fn get_id(&self) -> String { + self.property("id") + } + + pub fn bind_index(&self, o: &impl ObjectType, property: &str) { + self.imp().push_binding( + self.bind_property("index", o, property) + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(), + ); + } + + pub fn bind_artist(&self, o: &impl ObjectType, property: &str) { + self.imp().push_binding( + self.bind_property("artist", o, property) + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(), + ); + } + + pub fn bind_title(&self, o: &impl ObjectType, property: &str) { + self.imp().push_binding( + self.bind_property("title", o, property) + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(), + ); + } + + pub fn bind_duration(&self, o: &impl ObjectType, property: &str) { + self.imp().push_binding( + self.bind_property("duration", o, property) + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(), + ); + } + + pub fn bind_playing(&self, o: &impl ObjectType, property: &str) { + self.imp().push_binding( + self.bind_property("playing", o, property) + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(), + ); + } + + pub fn bind_selected(&self, o: &impl ObjectType, property: &str) { + self.imp().push_binding( + self.bind_property("selected", o, property) + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(), + ); + } + + pub fn unbind_all(&self) { + self.imp().unbind_all(self); + } + + pub fn description(&self) -> impl Deref + '_ { + Ref::map(self.imp().song.borrow(), |s| { + s.as_ref().expect("song set at constructor") + }) + } + + pub fn into_description(&self) -> SongDescription { + self.imp() + .song + .borrow() + .as_ref() + .cloned() + .expect("song set at constructor") + } +} + +mod imp { + + use super::*; + use std::cell::{Cell, RefCell}; + + // Keep track of signals and bindings targeting this song + #[derive(Default)] + struct BindingsInner { + pub signals: Vec, + pub bindings: Vec, + } + + #[derive(Default)] + pub struct SongModel { + pub song: RefCell>, + pub state: Cell, + bindings: RefCell, + } + + impl SongModel { + pub fn push_signal(&self, id: SignalHandlerId) { + self.bindings.borrow_mut().signals.push(id); + } + + pub fn push_binding(&self, binding: glib::Binding) { + self.bindings.borrow_mut().bindings.push(binding); + } + + pub fn unbind_all(&self, o: &O) { + let mut bindings = self.bindings.borrow_mut(); + bindings.signals.drain(..).for_each(|s| o.disconnect(s)); + bindings.bindings.drain(..).for_each(|b| b.unbind()); + } + } + + #[glib::object_subclass] + impl ObjectSubclass for SongModel { + const NAME: &'static str = "SongModel"; + type Type = super::SongModel; + type ParentType = glib::Object; + } + + lazy_static! { + static ref PROPERTIES: [glib::ParamSpec; 8] = [ + glib::ParamSpecString::builder("id").read_only().build(), + glib::ParamSpecUInt::builder("index").read_only().build(), + glib::ParamSpecString::builder("title").read_only().build(), + glib::ParamSpecString::builder("artist").read_only().build(), + glib::ParamSpecString::builder("duration") + .read_only() + .build(), + // URL + glib::ParamSpecString::builder("art").read_only().build(), + // Can be true when playback is paused; just means this is the current song + glib::ParamSpecBoolean::builder("playing") + .readwrite() + .build(), + glib::ParamSpecBoolean::builder("selected") + .readwrite() + .build(), + ]; + } + + impl ObjectImpl for SongModel { + fn properties() -> &'static [glib::ParamSpec] { + &*PROPERTIES + } + + fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { + match pspec.name() { + "playing" => { + let is_playing = value + .get() + .expect("type conformity checked by `Object::set_property`"); + let SongState { is_selected, .. } = self.state.get(); + self.state.set(SongState { + is_playing, + is_selected, + }); + } + "selected" => { + let is_selected = value + .get() + .expect("type conformity checked by `Object::set_property`"); + let SongState { is_playing, .. } = self.state.get(); + self.state.set(SongState { + is_playing, + is_selected, + }); + } + _ => unimplemented!(), + } + } + + fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + match pspec.name() { + "index" => self + .song + .borrow() + .as_ref() + .expect("song set at constructor") + .track_number + .unwrap_or(1) + .to_value(), + "title" => self + .song + .borrow() + .as_ref() + .expect("song set at constructor") + .title + .to_value(), + "artist" => self + .song + .borrow() + .as_ref() + .expect("song set at constructor") + .artists_name() + .to_value(), + "id" => self + .song + .borrow() + .as_ref() + .expect("song set at constructor") + .id + .to_value(), + "duration" => self + .song + .borrow() + .as_ref() + .map(|s| format_duration(s.duration.into())) + .expect("song set at constructor") + .to_value(), + "art" => self + .song + .borrow() + .as_ref() + .expect("song set at constructor") + .art + .to_value(), + "playing" => self.state.get().is_playing.to_value(), + "selected" => self.state.get().is_selected.to_value(), + _ => unimplemented!(), + } + } + } +} diff --git a/src/app/models/songs/support.rs b/src/app/models/songs/support.rs new file mode 100644 index 0000000..4b02600 --- /dev/null +++ b/src/app/models/songs/support.rs @@ -0,0 +1,686 @@ +use std::collections::HashMap; +use std::convert::{TryFrom, TryInto}; + +use crate::app::models::*; + +// A range of numbers [a, b], empty range is allowed as well +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Range { + Empty, + NotEmpty(u32, u32), +} + +impl Range { + // Create a range [a, b] if b >= a, or an empty range otherwise + fn of(a: impl TryInto, b: impl TryInto) -> Self { + match (a.try_into(), b.try_into()) { + (Ok(a), Ok(b)) if b >= a => Self::NotEmpty(a, b), + _ => Self::Empty, + } + } + + fn len(self) -> u32 { + match self { + Self::Empty => 0, + Self::NotEmpty(a, b) => b - a + 1, + } + } + + fn union(self, other: Self) -> Self { + match (self, other) { + (Self::NotEmpty(a0, b0), Self::NotEmpty(a1, b1)) => { + let start = u32::min(a0, a1); + let end = u32::max(b0, b1); + Self::NotEmpty(start, end) + } + (Self::Empty, r) | (r, Self::Empty) => r, + } + } + + fn offset_by(self, offset: i32) -> Self { + match self { + Self::Empty => Self::Empty, + Self::NotEmpty(a, b) => Self::of((a as i32) + offset, (b as i32) + offset), + } + } + + // Start index of the range, if not an empty range + fn start(self) -> Option + where + Target: TryFrom, + { + match self { + Self::Empty => None, + Self::NotEmpty(a, _) => Some(a.try_into().ok()?), + } + } +} + +// Represents the range affected by an operation on the list +// ListRangeUpdate(position, nb of elements added, nb of elements removed) +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ListRangeUpdate(pub i32, pub i32, pub i32); + +impl ListRangeUpdate { + pub fn inserted(position: impl TryInto, added: impl TryInto) -> Self { + Self( + position.try_into().unwrap_or_default(), + 0, + added.try_into().unwrap_or_default(), + ) + } + + pub fn removed(position: impl TryInto, removed: impl TryInto) -> Self { + Self( + position.try_into().unwrap_or_default(), + removed.try_into().unwrap_or_default(), + 0, + ) + } + + pub fn updated(position: impl TryInto) -> Self { + Self(position.try_into().unwrap_or_default(), 1, 1) + } + + // Merge two range updates + pub fn merge(self, other: Self) -> Self { + // reorder for simplicity + let (left, right) = if self.0 <= other.0 { + (self, other) + } else { + (other, self) + }; + + let Self(p0, r0, a0) = left; + let Self(p1, r1, a1) = right; + + // range [s, e] affected by first update + let ra0 = Range::of(p0, p0 + r0 - 1); + + // ...second update, but only the range affecting existing elements + let ra1 = { + let s1 = i32::max(p0 + a0, p1); + let e1 = i32::max(s1 - 1, p1 + r1 - 1); + Range::of(s1, e1) + }; + + // remap to original + let ra1 = ra1.offset_by(r0 - a0); + + // union + let rau = ra0.union(ra1); + + let removed = rau.len() as i32; + let position = rau.start().unwrap_or(p0); + let added = removed - (r0 - a0) - (r1 - a1); + Self(position, removed, added) + } +} + +// A list of songs that supports +// - batch loading (with non contiguous batches if songs are accessed in random order) +// - O(1) time access to a song by its id +// - manually adding content (not batched), when managing a queue for instance +// - tracking the affected range after a mutation +// +// Note: the mutated ranges are given in terms of LOADED tracks. The theoretical size of the list is not accounted for. +// This is to ease the work of updating the UI: we want to know what loaded/visible elements have moved around. +// +// Some operations are not very efficient. It might have been smarter to have different structures for our two use cases: +// - fixed, batched sources (an album, a playlist) +// - editable lists (queue) +#[derive(Clone, Debug)] +pub struct SongList { + total: usize, + total_loaded: usize, + batch_size: usize, + last_batch_key: usize, + // Here a batch has an index (key) and a list of associated song ids + // Why not a Vec? We could have batch 1, 2, NOT 3, then 4 + batches: HashMap>, + indexed_songs: HashMap, +} + +impl SongList { + pub fn new_sized(batch_size: usize) -> Self { + Self { + total: 0, + total_loaded: 0, + batch_size, + last_batch_key: 0, + batches: Default::default(), + indexed_songs: Default::default(), + } + } + + pub fn batch_size(&self) -> usize { + self.batch_size + } + + pub fn iter(&self) -> impl Iterator { + let indexed_songs = &self.indexed_songs; + self.iter_ids_from(0) + .filter_map(move |(_, id)| indexed_songs.get(id)) + } + + // How many songs we actually have at the moment + pub fn partial_len(&self) -> usize { + self.total_loaded + } + + // How many songs are loaded, up to a given batch index + fn estimated_len(&self, up_to_batch_index: usize) -> usize { + let batches = &self.batches; + let batch_size = self.batch_size; + let batch_count = (0..up_to_batch_index) + .filter(move |i| batches.contains_key(i)) + .count(); + batch_size * batch_count + } + + // The theoretical len of the playlist, if we had all songs + pub fn len(&self) -> usize { + self.total + } + + fn iter_ids_from(&self, i: usize) -> impl Iterator { + let batch_size = self.batch_size; + let index = i / batch_size; + self.iter_range(index, self.last_batch_key) + .skip(i % batch_size) + } + + // Find the position of a song in the list + pub fn find_index(&self, song_id: &str) -> Option { + self.iter_ids_from(0) + .find(|(_, id)| &id[..] == song_id) + .map(|(pos, _)| pos) + } + + // Iterate over batches (in a given batch range), returning a tuple with the index of a song and its id + fn iter_range(&self, a: usize, b: usize) -> impl Iterator { + let batch_size = self.batch_size; + let batches = &self.batches; + (a..=b) + .filter_map(move |i| batches.get_key_value(&i)) + .flat_map(move |(k, b)| { + b.iter() + .enumerate() + .map(move |(i, id)| (i + *k * batch_size, id)) + }) + } + + // Add an id to our batches + fn batches_add(batches: &mut HashMap>, batch_size: usize, id: &str) { + let index = batches.len().saturating_sub(1); + let count = batches + .get(&index) + .map(|b| b.len() % batch_size) + .unwrap_or(0); + // If there's no space in the last batch, we insert a new one + if count == 0 { + batches.insert(batches.len(), vec![id.to_string()]); + } else { + batches.get_mut(&index).unwrap().push(id.to_string()); + } + } + + pub fn clear(&mut self) -> ListRangeUpdate { + let len = self.partial_len(); + *self = Self::new_sized(self.batch_size); + ListRangeUpdate::removed(0, len) + } + + pub fn remove(&mut self, ids: &[String]) -> ListRangeUpdate { + let len = self.total_loaded; + let mut batches = HashMap::>::default(); + self.iter_ids_from(0) + .filter(|(_, s)| !ids.contains(s)) + // Removing is expensive, we have to recreate all batches + .for_each(|(_, next)| { + Self::batches_add(&mut batches, self.batch_size, next); + }); + self.last_batch_key = batches.len().saturating_sub(1); + self.batches = batches; + let removed = ids.len(); + self.total = self.total.saturating_sub(removed); + self.total_loaded = self.total_loaded.saturating_sub(removed); + // Lazy computation of the affected range, basically assume everything has changed + ListRangeUpdate(0, len as i32, self.total_loaded as i32) + } + + pub fn append(&mut self, songs: Vec) -> ListRangeUpdate { + let songs_len = songs.len(); + // How many loaded/visible songs so far + let insertion_start = self.estimated_len(self.last_batch_key + 1); + self.total = self.total.saturating_add(songs_len); + self.total_loaded = self.total_loaded.saturating_add(songs_len); + for song in songs { + Self::batches_add(&mut self.batches, self.batch_size, &song.id); + self.indexed_songs + .insert(song.id.clone(), SongModel::new(song)); + } + self.last_batch_key = self.batches.len().saturating_sub(1); + ListRangeUpdate::inserted(insertion_start, songs_len) + } + + pub fn prepend(&mut self, songs: Vec) -> ListRangeUpdate { + let songs_len = songs.len(); + let insertion_start = 0; + + // Prepending also requires redoing all the batches + let mut batches = HashMap::>::default(); + for song in songs { + Self::batches_add(&mut batches, self.batch_size, &song.id); + self.indexed_songs + .insert(song.id.clone(), SongModel::new(song)); + } + self.iter_ids_from(0).for_each(|(_, next)| { + Self::batches_add(&mut batches, self.batch_size, next); + }); + + self.total = self.total.saturating_add(songs_len); + self.total_loaded = self.total_loaded.saturating_add(songs_len); + self.last_batch_key = batches.len().saturating_sub(1); + self.batches = batches; + + // But it's a bit easier to computer the visibly affected range :) + ListRangeUpdate::inserted(insertion_start, songs_len) + } + + // Adding a batch is easy, might only require a resize + pub fn add(&mut self, song_batch: SongBatch) -> Option { + if song_batch.batch.batch_size != self.batch_size { + song_batch + .resize(self.batch_size) + .into_iter() + .map(|new_batch| { + debug!("adding batch {:?}", &new_batch.batch); + self.add_one(new_batch) + }) + .reduce(|acc, cur| { + // If we have added more than one batch we just merge the affected ranges + let merged = acc?.merge(cur?); + Some(merged).or(acc).or(cur) + }) + .unwrap_or(None) + } else { + self.add_one(song_batch) + } + } + + fn add_one(&mut self, SongBatch { songs, batch }: SongBatch) -> Option { + assert_eq!(batch.batch_size, self.batch_size); + + let index = batch.offset / batch.batch_size; + + if self.batches.contains_key(&index) { + debug!("batch already loaded"); + return None; + } + + let insertion_start = self.estimated_len(index); + let len = songs.len(); + let ids = songs + .into_iter() + .map(|song| { + let song_id = song.id.clone(); + self.indexed_songs + .insert(song_id.clone(), SongModel::new(song)); + song_id + }) + .collect(); + + self.batches.insert(index, ids); + self.total = batch.total; + self.total_loaded += len; + self.last_batch_key = usize::max(self.last_batch_key, index); + + Some(ListRangeUpdate::inserted(insertion_start, len)) + } + + fn index_mut(&mut self, i: usize) -> Option<&mut String> { + let batch_size = self.batch_size; + let i_batch = i / batch_size; + self.batches + .get_mut(&i_batch) + .and_then(|s| s.get_mut(i % batch_size)) + } + + pub fn swap(&mut self, a: usize, b: usize) -> Option { + if a == b { + return None; + } + let a_value = self.index_mut(a).map(std::mem::take); + let a_value = a_value.as_ref(); + let new_a_value = self + .index_mut(b) + .and_then(|v| Some(std::mem::replace(v, a_value?.clone()))) + .or_else(|| a_value.cloned()); + let a_mut = self.index_mut(a); + if let (Some(a_mut), Some(a_value)) = (a_mut, new_a_value) { + *a_mut = a_value; + } + Some(ListRangeUpdate::updated(a).merge(ListRangeUpdate::updated(b))) + } + + // Get the song at i (if the index is valid AND has been loaded) + pub fn index(&self, i: usize) -> Option<&SongModel> { + let batch_size = self.batch_size; + let batch_id = i / batch_size; + let indexed_songs = &self.indexed_songs; + self.batches + .get(&batch_id) + .and_then(|batch| batch.get(i % batch_size)) + .and_then(move |id| indexed_songs.get(id)) + } + + // Get the i-th loaded song. VERY different! + pub fn index_continuous(&self, i: usize) -> Option<&SongModel> { + let batch_size = self.batch_size; + let bi = i / batch_size; + let batch = (0..=self.last_batch_key) + // Skip missing/not loaded batches + .filter_map(move |i| self.batches.get(&i)) + .nth(bi)?; + batch + .get(i % batch_size) + .and_then(move |id| self.indexed_songs.get(id)) + } + + // Return the batch needed to access the song at index i (if it's not loaded yet) + pub fn needed_batch_for(&self, i: usize) -> Option { + let total = self.total; + let batch_size = self.batch_size; + let batch_id = i / batch_size; + if self.batches.contains_key(&batch_id) { + None + } else { + Some(Batch { + batch_size, + total, + offset: batch_id * batch_size, + }) + } + } + + // Get the full song batch that contains i + pub fn song_batch_for(&self, i: usize) -> Option { + let total = self.total; + let batch_size = self.batch_size; + let batch_id = i / batch_size; + let indexed_songs = &self.indexed_songs; + self.batches.get(&batch_id).map(|songs| SongBatch { + songs: songs + .iter() + .filter_map(move |id| Some(indexed_songs.get(id)?.into_description())) + .collect(), + batch: Batch { + batch_size, + total, + offset: batch_id * batch_size, + }, + }) + } + + // The last loaded batch + pub fn last_batch(&self) -> Option { + if self.total_loaded == 0 { + None + } else { + Some(Batch { + batch_size: self.batch_size, + total: self.total, + offset: self.last_batch_key * self.batch_size, + }) + } + } + + pub fn get(&self, id: &str) -> Option<&SongModel> { + self.indexed_songs.get(id) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + const NO_CHANGE: ListRangeUpdate = ListRangeUpdate(0, 0, 0); + + impl SongList { + fn new_from_initial_batch(initial: SongBatch) -> Self { + let mut s = Self::new_sized(initial.batch.batch_size); + s.add(initial); + s + } + } + + fn song(id: &str) -> SongDescription { + SongDescription { + id: id.to_string(), + uri: "".to_string(), + title: "Title".to_string(), + artists: vec![], + album: AlbumRef { + id: "".to_string(), + name: "".to_string(), + }, + duration: 1000, + art: None, + track_number: None, + } + } + + fn batch(id: usize) -> SongBatch { + let offset = id * 2; + SongBatch { + batch: Batch { + offset, + batch_size: 2, + total: 10, + }, + songs: vec![ + song(&format!("song{offset}")), + song(&format!("song{}", offset + 1)), + ], + } + } + + #[test] + fn test_merge_range() { + // [0, 1, 2, 3, 4, 5] + let change1 = ListRangeUpdate(0, 4, 2); + // [x, x, 4, 5] + let change2 = ListRangeUpdate(1, 1, 2); + // [x, y, y, 4, 5] + assert_eq!(change1.merge(change2), ListRangeUpdate(0, 4, 3)); + assert_eq!(change2.merge(change1), ListRangeUpdate(0, 4, 3)); + + // [0, 1, 2, 3, 4, 5, 6] + let change1 = ListRangeUpdate(0, 2, 3); + // [x, x, x, 2, 3, 4, 5, 6] + let change2 = ListRangeUpdate(4, 1, 1); + // [x, x, x, 2, y, 4, 5, 6] + assert_eq!(change1.merge(change2), ListRangeUpdate(0, 4, 5)); + assert_eq!(change2.merge(change1), ListRangeUpdate(0, 4, 5)); + + // [0, 1, 2, 3, 4, 5, 6] + let change1 = ListRangeUpdate(0, 3, 2); + // [x, x, 3, 4, 5, 6] + let change2 = ListRangeUpdate(4, 1, 1); + // [x, x, 3, 4, y, 6] + assert_eq!(change1.merge(change2), ListRangeUpdate(0, 6, 5)); + assert_eq!(change2.merge(change1), ListRangeUpdate(0, 6, 5)); + + // [0, 1, 2, 3, 4, 5] + let change1 = ListRangeUpdate(0, 4, 2); + // [x, x, 4, 5] + let change2 = ListRangeUpdate(1, 1, 1); + // [x, y, 4, 5] + assert_eq!(change1.merge(change2), ListRangeUpdate(0, 4, 2)); + assert_eq!(change2.merge(change1), ListRangeUpdate(0, 4, 2)); + + // [0, 1, 2, 3, 4, 5] + let change1 = ListRangeUpdate(0, 4, 2); + // [x, x, 4, 5] + let change2 = ListRangeUpdate(0, 4, 2); + // [y, y] + assert_eq!(change1.merge(change2), ListRangeUpdate(0, 6, 2)); + assert_eq!(change2.merge(change1), ListRangeUpdate(0, 6, 2)); + + // [] + let change1 = ListRangeUpdate(0, 0, 2); + // [x, x] + let change2 = ListRangeUpdate(2, 0, 2); + // [x, x, y, y] + assert_eq!(change1.merge(change2), ListRangeUpdate(0, 0, 4)); + assert_eq!(change2.merge(change1), ListRangeUpdate(0, 0, 4)); + + let change1 = ListRangeUpdate(0, 4, 2); + assert_eq!(change1.merge(NO_CHANGE), ListRangeUpdate(0, 4, 2)); + assert_eq!(NO_CHANGE.merge(change1), ListRangeUpdate(0, 4, 2)); + } + + #[test] + fn test_iter() { + let list = SongList::new_from_initial_batch(batch(0)); + + let mut list_iter = list.iter(); + assert_eq!(list_iter.next().unwrap().description().id, "song0"); + assert_eq!(list_iter.next().unwrap().description().id, "song1"); + assert!(list_iter.next().is_none()); + } + + #[test] + fn test_index() { + let list = SongList::new_from_initial_batch(batch(0)); + + let song1 = list.index(1); + assert!(song1.is_some()); + + let song3 = list.index(3); + assert!(song3.is_none()); + } + + #[test] + fn test_add() { + let mut list = SongList::new_from_initial_batch(batch(0)); + list.add(batch(1)); + + let song3 = list.index(3); + assert!(song3.is_some()); + let list_iter = list.iter(); + assert_eq!(list_iter.count(), 4); + } + + #[test] + fn test_add_with_range() { + let mut list = SongList::new_from_initial_batch(batch(0)); + + let range = list.add(batch(1)); + assert_eq!(range, Some(ListRangeUpdate::inserted(2, 2))); + assert_eq!(list.partial_len(), 4); + + let range = list.add(batch(3)); + assert_eq!(range, Some(ListRangeUpdate::inserted(4, 2))); + assert_eq!(list.partial_len(), 6); + + let range = list.add(batch(2)); + assert_eq!(range, Some(ListRangeUpdate::inserted(4, 2))); + assert_eq!(list.partial_len(), 8); + + let range = list.add(batch(2)); + assert_eq!(range, None); + assert_eq!(list.partial_len(), 8); + } + + #[test] + fn test_find_non_contiguous() { + let mut list = SongList::new_from_initial_batch(batch(0)); + list.add(batch(3)); + + let index = list.find_index("song6"); + + assert_eq!(index, Some(6)); + } + + #[test] + fn test_iter_non_contiguous() { + let mut list = SongList::new_from_initial_batch(batch(0)); + list.add(batch(2)); + + assert_eq!(list.partial_len(), 4); + + let mut list_iter = list.iter(); + assert_eq!(list_iter.next().unwrap().description().id, "song0"); + assert_eq!(list_iter.next().unwrap().description().id, "song1"); + assert_eq!(list_iter.next().unwrap().description().id, "song4"); + assert_eq!(list_iter.next().unwrap().description().id, "song5"); + assert!(list_iter.next().is_none()); + } + + #[test] + fn test_remove() { + let mut list = SongList::new_from_initial_batch(batch(0)); + list.add(batch(1)); + + list.remove(&["song0".to_string()]); + + assert_eq!(list.partial_len(), 3); + + let mut list_iter = list.iter(); + assert_eq!(list_iter.next().unwrap().description().id, "song1"); + assert_eq!(list_iter.next().unwrap().description().id, "song2"); + assert_eq!(list_iter.next().unwrap().description().id, "song3"); + assert!(list_iter.next().is_none()); + } + + #[test] + fn test_batch_for() { + let mut list = SongList::new_from_initial_batch(batch(0)); + list.add(batch(1)); + list.add(batch(2)); + list.add(batch(3)); + + assert_eq!(list.partial_len(), 8); + + let batch = list.song_batch_for(3); + assert_eq!(batch.unwrap().batch.offset, 2); + } + + #[test] + fn test_append() { + let mut list = SongList::new_from_initial_batch(batch(0)); + list.append(vec![song("song2")]); + list.append(vec![song("song3")]); + list.append(vec![song("song4")]); + + let mut list_iter = list.iter(); + assert_eq!(list_iter.next().unwrap().description().id, "song0"); + assert_eq!(list_iter.next().unwrap().description().id, "song1"); + assert_eq!(list_iter.next().unwrap().description().id, "song2"); + assert_eq!(list_iter.next().unwrap().description().id, "song3"); + assert_eq!(list_iter.next().unwrap().description().id, "song4"); + assert!(list_iter.next().is_none()); + } + + #[test] + fn test_swap() { + let mut list = SongList::new_sized(10); + list.append(vec![song("song0"), song("song1"), song("song2")]); + + list.swap(0, 3); // should be a no-op + list.swap(2, 3); // should be a no-op + list.swap(0, 2); + list.swap(0, 1); + list.swap(2, 2); // should be no-op + list.swap(2, 3); // should be no-op + + let mut list_iter = list.iter(); + assert_eq!(list_iter.next().unwrap().description().id, "song1"); + assert_eq!(list_iter.next().unwrap().description().id, "song2"); + assert_eq!(list_iter.next().unwrap().description().id, "song0"); + assert!(list_iter.next().is_none()); + } +} diff --git a/src/app/rng.rs b/src/app/rng.rs new file mode 100644 index 0000000..2c2e5c0 --- /dev/null +++ b/src/app/rng.rs @@ -0,0 +1,198 @@ +use rand::{rngs::SmallRng, RngCore, SeedableRng}; + +// A random, resizable mapping (i-th element to play => j-th track) used to handle shuffled playlists +// It's lazy: initially we don't compute what index i maps to +// It's resizable: if our playlist grows or shrinks, we have to keep the generated mappings stable +// (we don't want to reshuffle!) +#[derive(Debug)] +pub struct LazyRandomIndex { + rng: SmallRng, + indices: Vec, + // How many mapping were generated + generated: usize, +} + +impl Default for LazyRandomIndex { + fn default() -> Self { + Self::from(SmallRng::from_entropy()) + } +} + +impl LazyRandomIndex { + fn from(rng: SmallRng) -> Self { + Self { + rng, + indices: Default::default(), + generated: 0, + } + } + + // Resets the mapping, but make sure some index `first` will be mapped from 0 + // This is used to "pin" the position of a track when switching in and out of shuffle mode + // See tests below for an example + pub fn reset_picking_first(&mut self, first: usize) { + self.generated = 0; + if let Some(index) = self.indices.iter().position(|i| *i == first) { + self.pick_next(index); + } + } + + // Grow or shrink + pub fn resize(&mut self, size: usize) { + if size >= self.indices.len() { + self.grow(size); + } else { + self.shrink(size); + } + } + + // Resize the underlying Vec, but we don't generate mappings yet + // We don't update the `generated` count: for now whatever is beyond that index is just the non-shuffled index + pub fn grow(&mut self, size: usize) { + let current_size = self.indices.len(); + self.indices.extend(current_size..size); + } + + pub fn shrink(&mut self, size: usize) { + self.generated = usize::min(size, self.generated); + self.indices.truncate(size); + } + + // Get the index (for instance in a playlist) of the i-th next element to play + pub fn get(&self, i: usize) -> Option { + if i >= self.generated || i >= self.indices.len() { + None + } else { + Some(self.indices[i]) + } + } + + // Generate all mappings until the mapping for i has been generated + pub fn next_until(&mut self, i: usize) -> Option { + if i >= self.indices.len() { + return None; + } + + loop { + if self.generated > i { + break Some(self.indices[i]); + } + self.next(); + } + } + + // Generate the next mapping + pub fn next(&mut self) -> Option { + if self.indices.len() < self.generated { + return None; + } + + let last = self.generated; + // Pick a random index in the range [k, n[ where + // k is the next index to map + // n is the size of our targeted list + + // The element at that index will be swapped with whatever is in pos k + // That is, we never mess with the already picked indices, we want this to be stable: + // a0, a1, ..., ak-1, ak, ..., an + // <-already mapped-> + // <-not mapped-> + // We just pick something between k and n and make it ak + + // Example gen with size 3 + // [0, 1, 2], generated = 0 + // [1, 0, 2], generated = 1, we swapped 0 and 1 + // [1, 0, 2], generated = 2, we swapped 1 and 1 (no-op) + // [1, 0, 2], generated = 3, no-op again (no choice, only one element left to place) + let next = (self.rng.next_u64() as usize) % (self.indices.len() - last) + last; + Some(self.pick_next(next)) + } + + fn pick_next(&mut self, next: usize) -> usize { + let last = self.generated; + self.indices.swap(last, next); + self.generated += 1; + self.indices[last] + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + fn rng_for_test() -> SmallRng { + SmallRng::seed_from_u64(0) + } + + fn get_sequence(n: usize) -> Vec { + let mut rng = rng_for_test(); + (0..n) + .into_iter() + .map(|_| rng.next_u64() as usize) + .collect() + } + + #[test] + fn test_initial() { + let seq = get_sequence(10); + let first = Some(seq[0] % 10); + + // It's controlled randomness for the test :) + let mut index = LazyRandomIndex::from(rng_for_test()); + index.grow(10); + + let next = index.next(); + assert_eq!(next, first); + assert_eq!(index.get(0), first); + } + + #[test] + fn test_sample_all() { + let mut index = LazyRandomIndex::from(rng_for_test()); + index.grow(2); + index.grow(5); + + let mut values = (0..5) + .into_iter() + .filter_map(|_| index.next()) + .collect::>(); + let sorted = &mut values[..]; + sorted.sort(); + // Check that we have all our indices mapped + assert_eq!(sorted, &[0, 1, 2, 3, 4]); + } + + #[test] + fn test_after_grow() { + let mut index = LazyRandomIndex::from(rng_for_test()); + + index.grow(5); + index.next_until(2); + let values = &[index.get(0), index.get(1), index.get(2)]; + + index.grow(10); + let same_values = &[index.get(0), index.get(1), index.get(2)]; + + // After growing the index from 5 to 10, we want to check + // that the previously generated mappings are unaffected. + assert_eq!(values, same_values); + } + + #[test] + fn test_reset() { + let mut index = LazyRandomIndex::from(rng_for_test()); + + // Example use + // Assume we have 5 songs in our shuffled playlist, and we generate those 5 few random mappings + index.grow(5); + index.next_until(5); + + // We exit shuffle mode at some point + + // Shuffle is toggled again: we want index 2 to come up first in the shuffled playlist + // because it is what's currently playing. + index.reset_picking_first(2); + assert_eq!(index.get(0), Some(2)); + } +} diff --git a/src/app/state/app_model.rs b/src/app/state/app_model.rs new file mode 100644 index 0000000..5c777da --- /dev/null +++ b/src/app/state/app_model.rs @@ -0,0 +1,79 @@ +use crate::api::SpotifyApiClient; +use crate::app::{state::*, BatchLoader}; +use ref_filter_map::*; +use std::cell::{Ref, RefCell}; +use std::sync::Arc; + +pub struct AppServices { + pub spotify_api: Arc, + pub batch_loader: BatchLoader, +} + +// Two purposes: give access to some services to users of the AppModel (shared) +// and give a read only view of the state +pub struct AppModel { + state: RefCell, + services: AppServices, +} + +impl AppModel { + pub fn new(state: AppState, spotify_api: Arc) -> Self { + let services = AppServices { + batch_loader: BatchLoader::new(Arc::clone(&spotify_api)), + spotify_api, + }; + let state = RefCell::new(state); + Self { state, services } + } + + pub fn get_spotify(&self) -> Arc { + Arc::clone(&self.services.spotify_api) + } + + pub fn get_batch_loader(&self) -> BatchLoader { + self.services.batch_loader.clone() + } + + // Read only access to the state! + pub fn get_state(&self) -> Ref<'_, AppState> { + self.state.borrow() + } + + // Convenience... + pub fn map_state &T>(&self, map: F) -> Ref<'_, T> { + Ref::map(self.state.borrow(), map) + } + + // Convenience... + pub fn map_state_opt Option<&T>>( + &self, + map: F, + ) -> Option> { + ref_filter_map(self.state.borrow(), map) + } + + pub fn update_state(&self, action: AppAction) -> Vec { + // The LoginActions are a bit special, we intercept them to grab the Spotify token + // and save it in our Arc'd API client + match &action { + AppAction::LoginAction(LoginAction::SetLoginSuccess( + SetLoginSuccessAction::Password(creds), + )) => { + self.services.spotify_api.update_token(creds.token.clone()); + } + AppAction::LoginAction(LoginAction::SetLoginSuccess(SetLoginSuccessAction::Token( + creds, + ))) => { + self.services.spotify_api.update_token(creds.token.clone()); + } + AppAction::LoginAction(LoginAction::SetRefreshedToken { token, .. }) => { + self.services.spotify_api.update_token(token.clone()); + } + _ => {} + } + + // And this is the only mutable borrow of our state! + let mut state = self.state.borrow_mut(); + state.update_state(action) + } +} diff --git a/src/app/state/app_state.rs b/src/app/state/app_state.rs new file mode 100644 index 0000000..fb7ebb1 --- /dev/null +++ b/src/app/state/app_state.rs @@ -0,0 +1,270 @@ +use std::borrow::Cow; + +use crate::app::models::{PlaylistDescription, PlaylistSummary}; +use crate::app::state::{ + browser_state::{BrowserAction, BrowserEvent, BrowserState}, + login_state::{LoginAction, LoginEvent, LoginState}, + playback_state::{PlaybackAction, PlaybackEvent, PlaybackState}, + selection_state::{SelectionAction, SelectionContext, SelectionEvent, SelectionState}, + settings_state::{SettingsAction, SettingsEvent, SettingsState}, + ScreenName, UpdatableState, +}; + +// It's a big one... +// All possible actions! +// It's probably a VERY poor way to layout such a big enum, just look at the size, I'm so sorry I am not a sytems programmer +// Could use a few more Boxes maybe? +#[derive(Clone, Debug)] +pub enum AppAction { + // With sub categories :) + PlaybackAction(PlaybackAction), + BrowserAction(BrowserAction), + SelectionAction(SelectionAction), + LoginAction(LoginAction), + SettingsAction(SettingsAction), + Start, + Raise, + ShowNotification(String), + ViewNowPlaying, + // Cross-state actions + QueueSelection, + DequeueSelection, + MoveUpSelection, + MoveDownSelection, + SaveSelection, + UnsaveSelection, + EnableSelection(SelectionContext), + CancelSelection, + CreatePlaylist(PlaylistDescription), + UpdatePlaylistName(PlaylistSummary), +} + +// Not actual actions, just neat wrappers +impl AppAction { + // An action to open a Spotify URI + #[allow(non_snake_case)] + pub fn OpenURI(uri: String) -> Option { + debug!("parsing {}", &uri); + let mut parts = uri.split(':'); + if parts.next()? != "spotify" { + return None; + } + + // Might start with /// because of https://gitlab.gnome.org/GNOME/glib/-/issues/1886/ + let action = parts + .next()? + .strip_prefix("///") + .filter(|p| !p.is_empty())?; + let data = parts.next()?; + + match action { + "album" => Some(Self::ViewAlbum(data.to_string())), + "artist" => Some(Self::ViewArtist(data.to_string())), + "playlist" => Some(Self::ViewPlaylist(data.to_string())), + "user" => Some(Self::ViewUser(data.to_string())), + _ => None, + } + } + + #[allow(non_snake_case)] + pub fn ViewAlbum(id: String) -> Self { + BrowserAction::NavigationPush(ScreenName::AlbumDetails(id)).into() + } + + #[allow(non_snake_case)] + pub fn ViewArtist(id: String) -> Self { + BrowserAction::NavigationPush(ScreenName::Artist(id)).into() + } + + #[allow(non_snake_case)] + pub fn ViewPlaylist(id: String) -> Self { + BrowserAction::NavigationPush(ScreenName::PlaylistDetails(id)).into() + } + + #[allow(non_snake_case)] + pub fn ViewUser(id: String) -> Self { + BrowserAction::NavigationPush(ScreenName::User(id)).into() + } + + #[allow(non_snake_case)] + pub fn ViewSearch() -> Self { + BrowserAction::NavigationPush(ScreenName::Search).into() + } +} + +// Actions mutate stuff, and we know what changed thanks to these events +#[derive(Clone, Debug)] +pub enum AppEvent { + // Also subcategorized + PlaybackEvent(PlaybackEvent), + BrowserEvent(BrowserEvent), + SelectionEvent(SelectionEvent), + LoginEvent(LoginEvent), + Started, + Raised, + NotificationShown(String), + PlaylistCreatedNotificationShown(String), + NowPlayingShown, + SettingsEvent(SettingsEvent), +} + +// The actual state, split five-ways +pub struct AppState { + started: bool, + pub playback: PlaybackState, + pub browser: BrowserState, + pub selection: SelectionState, + pub logged_user: LoginState, + pub settings: SettingsState, +} + +impl AppState { + pub fn new() -> Self { + Self { + started: false, + playback: Default::default(), + browser: BrowserState::new(), + selection: Default::default(), + logged_user: Default::default(), + settings: Default::default(), + } + } + + pub fn update_state(&mut self, message: AppAction) -> Vec { + match message { + AppAction::Start if !self.started => { + self.started = true; + vec![AppEvent::Started] + } + // Couple of actions that don't mutate the state (not intested in keeping track of what they change) + // they're here just to have a consistent way of doing things (always an Action) + AppAction::ShowNotification(c) => vec![AppEvent::NotificationShown(c)], + AppAction::ViewNowPlaying => vec![AppEvent::NowPlayingShown], + AppAction::Raise => vec![AppEvent::Raised], + // Cross-state actions: multiple "substates" are affected by these actions, that's why they're handled here + // Might need some clean-up + AppAction::QueueSelection => { + self.playback.queue(self.selection.take_selection()); + vec![ + SelectionEvent::SelectionModeChanged(false).into(), + PlaybackEvent::PlaylistChanged.into(), + ] + } + AppAction::DequeueSelection => { + let tracks: Vec = self + .selection + .take_selection() + .into_iter() + .map(|s| s.id) + .collect(); + self.playback.dequeue(&tracks); + + vec![ + SelectionEvent::SelectionModeChanged(false).into(), + PlaybackEvent::PlaylistChanged.into(), + ] + } + AppAction::MoveDownSelection => { + let mut selection = self.selection.peek_selection(); + let playback = &mut self.playback; + selection + .next() + .and_then(|song| playback.move_down(&song.id)) + .map(|_| vec![PlaybackEvent::PlaylistChanged.into()]) + .unwrap_or_default() + } + AppAction::MoveUpSelection => { + let mut selection = self.selection.peek_selection(); + let playback = &mut self.playback; + selection + .next() + .and_then(|song| playback.move_up(&song.id)) + .map(|_| vec![PlaybackEvent::PlaylistChanged.into()]) + .unwrap_or_default() + } + AppAction::SaveSelection => { + let tracks = self.selection.take_selection(); + let mut events: Vec = forward_action( + BrowserAction::SaveTracks(tracks), + self.browser.home_state_mut().unwrap(), + ); + events.push(SelectionEvent::SelectionModeChanged(false).into()); + events + } + AppAction::UnsaveSelection => { + let tracks: Vec = self + .selection + .take_selection() + .into_iter() + .map(|s| s.id) + .collect(); + let mut events: Vec = forward_action( + BrowserAction::RemoveSavedTracks(tracks), + self.browser.home_state_mut().unwrap(), + ); + events.push(SelectionEvent::SelectionModeChanged(false).into()); + events + } + AppAction::EnableSelection(context) => { + if let Some(active) = self.selection.set_mode(Some(context)) { + vec![SelectionEvent::SelectionModeChanged(active).into()] + } else { + vec![] + } + } + AppAction::CancelSelection => { + if let Some(active) = self.selection.set_mode(None) { + vec![SelectionEvent::SelectionModeChanged(active).into()] + } else { + vec![] + } + } + AppAction::CreatePlaylist(playlist) => { + let id = playlist.id.clone(); + let mut events = forward_action( + LoginAction::PrependUserPlaylist(vec![playlist.clone().into()]), + &mut self.logged_user, + ); + let mut more_events = forward_action( + BrowserAction::PrependPlaylistsContent(vec![playlist]), + &mut self.browser, + ); + events.append(&mut more_events); + events.push(AppEvent::PlaylistCreatedNotificationShown(id)); + events + } + AppAction::UpdatePlaylistName(s) => { + let mut events = forward_action( + LoginAction::UpdateUserPlaylist(s.clone()), + &mut self.logged_user, + ); + let mut more_events = + forward_action(BrowserAction::UpdatePlaylistName(s), &mut self.browser); + events.append(&mut more_events); + events + } + // As for all other actions, we forward them to the substates :) + AppAction::PlaybackAction(a) => forward_action(a, &mut self.playback), + AppAction::BrowserAction(a) => forward_action(a, &mut self.browser), + AppAction::SelectionAction(a) => forward_action(a, &mut self.selection), + AppAction::LoginAction(a) => forward_action(a, &mut self.logged_user), + AppAction::SettingsAction(a) => forward_action(a, &mut self.settings), + _ => vec![], + } + } +} + +fn forward_action( + action: A, + target: &mut impl UpdatableState, +) -> Vec +where + A: Clone, + E: Into, +{ + target + .update_with(Cow::Owned(action)) + .into_iter() + .map(|e| e.into()) + .collect() +} diff --git a/src/app/state/browser_state.rs b/src/app/state/browser_state.rs new file mode 100644 index 0000000..34a4799 --- /dev/null +++ b/src/app/state/browser_state.rs @@ -0,0 +1,440 @@ +use super::{ + AppAction, AppEvent, ArtistState, DetailsState, HomeState, PlaylistDetailsState, ScreenName, + SearchState, UpdatableState, UserState, +}; +use crate::app::models::*; +use std::borrow::Cow; +use std::iter::Iterator; + +// Actions that affect any "screen" that we push over time +#[derive(Clone, Debug)] +pub enum BrowserAction { + SetNavigationHidden(bool), + SetHomeVisiblePage(&'static str), + SetLibraryContent(Vec), + PrependPlaylistsContent(Vec), + AppendLibraryContent(Vec), + SetPlaylistsContent(Vec), + AppendPlaylistsContent(Vec), + RemoveTracksFromPlaylist(String, Vec), + SetAlbumDetails(Box), + AppendAlbumTracks(String, Box), + SetPlaylistDetails(Box), + UpdatePlaylistName(PlaylistSummary), + AppendPlaylistTracks(String, Box), + Search(String), + SetSearchResults(Box), + SetArtistDetails(Box), + AppendArtistReleases(String, Vec), + NavigationPush(ScreenName), + NavigationPop, + NavigationPopTo(ScreenName), + SaveAlbum(Box), + UnsaveAlbum(String), + SetUserDetails(Box), + AppendUserPlaylists(String, Vec), + SetSavedTracks(Box), + AppendSavedTracks(Box), + SaveTracks(Vec), + RemoveSavedTracks(Vec), +} + +impl From for AppAction { + fn from(browser_action: BrowserAction) -> Self { + Self::BrowserAction(browser_action) + } +} + +#[derive(Eq, PartialEq, Clone, Debug)] +pub enum BrowserEvent { + NavigationHidden(bool), + HomeVisiblePageChanged(&'static str), + LibraryUpdated, + SavedPlaylistsUpdated, + AlbumDetailsLoaded(String), + AlbumTracksAppended(String), + PlaylistDetailsLoaded(String), + PlaylistTracksAppended(String), + PlaylistTracksRemoved(String), + SearchUpdated, + SearchResultsUpdated, + ArtistDetailsUpdated(String), + NavigationPushed(ScreenName), + NavigationPopped, + NavigationPoppedTo(ScreenName), + AlbumSaved(String), + AlbumUnsaved(String), + UserDetailsUpdated(String), + SavedTracksUpdated, +} + +impl From for AppEvent { + fn from(browser_event: BrowserEvent) -> Self { + Self::BrowserEvent(browser_event) + } +} + +// Any screen that can be "pushed" +pub enum BrowserScreen { + Home(Box), // Except this one is special, it's there at the start + AlbumDetails(Box), + Search(Box), + Artist(Box), + PlaylistDetails(Box), + User(Box), +} + +impl BrowserScreen { + fn from_name(name: &ScreenName) -> Self { + match name { + ScreenName::Home => BrowserScreen::Home(Default::default()), + ScreenName::AlbumDetails(id) => { + BrowserScreen::AlbumDetails(Box::new(DetailsState::new(id.to_string()))) + } + ScreenName::Search => BrowserScreen::Search(Default::default()), + ScreenName::Artist(id) => { + BrowserScreen::Artist(Box::new(ArtistState::new(id.to_string()))) + } + ScreenName::PlaylistDetails(id) => { + BrowserScreen::PlaylistDetails(Box::new(PlaylistDetailsState::new(id.to_string()))) + } + ScreenName::User(id) => BrowserScreen::User(Box::new(UserState::new(id.to_string()))), + } + } + + // Each screen has a state that can be updated with a BrowserAction + fn state(&mut self) -> &mut dyn UpdatableState { + match self { + Self::Home(state) => &mut **state, + Self::AlbumDetails(state) => &mut **state, + Self::Search(state) => &mut **state, + Self::Artist(state) => &mut **state, + Self::PlaylistDetails(state) => &mut **state, + Self::User(state) => &mut **state, + } + } +} + +impl NamedScreen for BrowserScreen { + type Name = ScreenName; + + fn name(&self) -> &Self::Name { + match self { + Self::Home(state) => &state.name, + Self::AlbumDetails(state) => &state.name, + Self::Search(state) => &state.name, + Self::Artist(state) => &state.name, + Self::PlaylistDetails(state) => &state.name, + Self::User(state) => &state.name, + } + } +} + +pub trait NamedScreen { + type Name: PartialEq; + fn name(&self) -> &Self::Name; +} + +#[derive(Debug)] +enum ScreenState { + NotPresent, + Present, + Current, +} + +// The navigation stack where we push screens (something with an equatable name) +struct NavStack(Vec); + +impl NavStack +where + Screen: NamedScreen, +{ + // Its len is guaranteed to be always 1 (see can_pop) + fn new(initial: Screen) -> Self { + Self(vec![initial]) + } + + fn iter_mut(&mut self) -> impl Iterator { + self.0.iter_mut().rev() + } + + fn iter_rev(&self) -> impl Iterator { + self.0.iter().rev() + } + + fn count(&self) -> usize { + self.0.len() + } + + fn current(&self) -> &Screen { + self.0.last().unwrap() + } + + fn current_mut(&mut self) -> &mut Screen { + self.0.last_mut().unwrap() + } + + fn can_pop(&self) -> bool { + self.0.len() > 1 + } + + fn push(&mut self, screen: Screen) { + self.0.push(screen) + } + + fn pop(&mut self) -> bool { + if self.can_pop() { + self.0.pop(); + true + } else { + false + } + } + + fn pop_to(&mut self, name: &Screen::Name) { + let split = self.0.iter().position(|s| s.name() == name).unwrap(); + self.0.truncate(split + 1); + } + + fn screen_visibility(&self, name: &Screen::Name) -> ScreenState { + // We iterate screens in reverse order + self.0 + .iter() + .rev() + .enumerate() + .find_map(|(i, screen)| { + let is_screen = screen.name() == name; + match (i, is_screen) { + // If we find the screen at pos 0 (last), it's therefore the current screen + (0, true) => Some(ScreenState::Current), + (_, true) => Some(ScreenState::Present), + (_, _) => None, + } + }) + .unwrap_or(ScreenState::NotPresent) + } +} + +pub struct BrowserState { + navigation_hidden: bool, + navigation: NavStack, +} + +macro_rules! extract_state { + ($e:expr, $p:pat if $guard:expr => $i:ident) => { + extract_state_full!($e, $p if $guard => $i) + }; + ($e:expr, $p:pat => $i:ident) => { + extract_state_full!($e, $p if true => $i) + }; +} + +macro_rules! extract_state_full { + ($e:expr, $p:pat if $guard:expr => $i:ident) => {{ + $e.navigation.iter_rev().find_map(|screen| match screen { + $p if $guard => Some(&**$i), + _ => None, + }) + }}; +} + +impl BrowserState { + pub fn new() -> Self { + Self { + navigation_hidden: false, + navigation: NavStack::new(BrowserScreen::Home(Default::default())), + } + } + + pub fn current_screen(&self) -> &ScreenName { + self.navigation.current().name() + } + + pub fn can_pop(&self) -> bool { + self.navigation.can_pop() || self.navigation_hidden + } + + pub fn count(&self) -> usize { + self.navigation.count() + } + + pub fn home_state(&self) -> Option<&HomeState> { + extract_state!(self, BrowserScreen::Home(s) => s) + } + + pub fn home_state_mut(&mut self) -> Option<&mut HomeState> { + self.navigation.iter_mut().find_map(|screen| match screen { + BrowserScreen::Home(s) => Some(&mut **s), + _ => None, + }) + } + + pub fn details_state(&self, id: &str) -> Option<&DetailsState> { + extract_state!(self, BrowserScreen::AlbumDetails(state) if state.id == id => state) + } + + pub fn search_state(&self) -> Option<&SearchState> { + extract_state!(self, BrowserScreen::Search(s) => s) + } + + pub fn artist_state(&self, id: &str) -> Option<&ArtistState> { + extract_state!(self, BrowserScreen::Artist(state) if state.id == id => state) + } + + pub fn playlist_details_state(&self, id: &str) -> Option<&PlaylistDetailsState> { + extract_state!(self, BrowserScreen::PlaylistDetails(state) if state.id == id => state) + } + + pub fn user_state(&self, id: &str) -> Option<&UserState> { + extract_state!(self, BrowserScreen::User(state) if state.id == id => state) + } + + // If a screen we want to push is already in the stack + // we just pop all the way back to it + fn push_if_needed(&mut self, name: &ScreenName) -> Vec { + let navigation = &mut self.navigation; + let screen_visibility = navigation.screen_visibility(name); + + match screen_visibility { + ScreenState::Current => vec![], + ScreenState::Present => { + navigation.pop_to(name); + vec![BrowserEvent::NavigationPoppedTo(name.clone())] + } + ScreenState::NotPresent => { + navigation.push(BrowserScreen::from_name(name)); + vec![BrowserEvent::NavigationPushed(name.clone())] + } + } + } +} + +impl UpdatableState for BrowserState { + type Action = BrowserAction; + type Event = BrowserEvent; + + fn update_with(&mut self, action: Cow) -> Vec { + let can_pop = self.navigation.can_pop(); + let action_ref = action.as_ref(); + + match action_ref { + BrowserAction::SetNavigationHidden(navigation_hidden) => { + self.navigation_hidden = *navigation_hidden; + vec![BrowserEvent::NavigationHidden(*navigation_hidden)] + } + // The search action will be handled here first before being passed down + // to push the search screen if it's not there already + BrowserAction::Search(_) => { + let mut events = self.push_if_needed(&ScreenName::Search); + + let mut update_events = self.navigation.current_mut().state().update_with(action); + events.append(&mut update_events); + events + } + BrowserAction::NavigationPush(name) => self.push_if_needed(name), + BrowserAction::NavigationPopTo(name) => { + self.navigation.pop_to(name); + vec![BrowserEvent::NavigationPoppedTo(name.clone())] + } + BrowserAction::NavigationPop if can_pop => { + self.navigation.pop(); + vec![BrowserEvent::NavigationPopped] + } + BrowserAction::NavigationPop if self.navigation_hidden => { + self.navigation_hidden = false; + vec![BrowserEvent::NavigationHidden(false)] + } + // Besides navigation actions, we just forward actions to each dedicated reducer + _ => self + .navigation + .iter_mut() + .flat_map(|s| s.state().update_with(Cow::Borrowed(action_ref))) + .collect(), + } + } +} + +#[cfg(test)] +pub mod tests { + + use super::*; + + #[test] + fn test_navigation_push() { + let mut state = BrowserState::new(); + + assert_eq!(*state.current_screen(), ScreenName::Home); + assert_eq!(state.count(), 1); + + let new_screen = ScreenName::Artist("some_id".to_string()); + state.update_with(Cow::Owned(BrowserAction::NavigationPush( + new_screen.clone(), + ))); + + assert_eq!(state.current_screen(), &new_screen); + assert_eq!(state.count(), 2); + assert_eq!(state.artist_state("some_id").is_some(), true); + } + + #[test] + fn test_navigation_pop() { + let mut state = BrowserState::new(); + let new_screen = ScreenName::Artist("some_id".to_string()); + state.update_with(Cow::Owned(BrowserAction::NavigationPush( + new_screen.clone(), + ))); + + assert_eq!(state.current_screen(), &new_screen); + assert_eq!(state.count(), 2); + + state.update_with(Cow::Owned(BrowserAction::NavigationPop)); + assert_eq!(state.current_screen(), &ScreenName::Home); + assert_eq!(state.count(), 1); + + let events = state.update_with(Cow::Owned(BrowserAction::NavigationPop)); + assert_eq!(state.current_screen(), &ScreenName::Home); + assert_eq!(state.count(), 1); + assert_eq!(events, vec![]); + } + + #[test] + fn test_navigation_push_same_screen() { + let mut state = BrowserState::new(); + let new_screen = ScreenName::Artist("some_id".to_string()); + state.update_with(Cow::Owned(BrowserAction::NavigationPush( + new_screen.clone(), + ))); + + assert_eq!(state.current_screen(), &new_screen); + assert_eq!(state.count(), 2); + + let events = state.update_with(Cow::Owned(BrowserAction::NavigationPush( + new_screen.clone(), + ))); + assert_eq!(state.current_screen(), &new_screen); + assert_eq!(state.count(), 2); + assert_eq!(events, vec![]); + } + + #[test] + fn test_navigation_push_same_screen_will_pop() { + let mut state = BrowserState::new(); + let new_screen = ScreenName::Artist("some_id".to_string()); + state.update_with(Cow::Owned(BrowserAction::NavigationPush( + new_screen.clone(), + ))); + state.update_with(Cow::Owned(BrowserAction::NavigationPush( + ScreenName::Search, + ))); + + assert_eq!(state.current_screen(), &ScreenName::Search); + assert_eq!(state.count(), 3); + + let events = state.update_with(Cow::Owned(BrowserAction::NavigationPush( + new_screen.clone(), + ))); + assert_eq!(state.current_screen(), &new_screen); + assert_eq!(state.count(), 2); + assert_eq!(events, vec![BrowserEvent::NavigationPoppedTo(new_screen)]); + } +} diff --git a/src/app/state/login_state.rs b/src/app/state/login_state.rs new file mode 100644 index 0000000..39fae50 --- /dev/null +++ b/src/app/state/login_state.rs @@ -0,0 +1,153 @@ +use gettextrs::*; +use std::borrow::Cow; +use std::time::SystemTime; + +use crate::app::credentials::Credentials; +use crate::app::models::PlaylistSummary; +use crate::app::state::{AppAction, AppEvent, UpdatableState}; + +#[derive(Clone, Debug)] +pub enum TryLoginAction { + Password { username: String, password: String }, + Token { username: String, token: String }, + OAuthSpotify {}, +} + +#[derive(Clone, Debug)] +pub enum SetLoginSuccessAction { + Password(Credentials), + Token(Credentials), +} + +#[derive(Clone, Debug)] +pub enum LoginAction { + ShowLogin, + TryLogin(TryLoginAction), + SetLoginSuccess(SetLoginSuccessAction), + SetUserPlaylists(Vec), + UpdateUserPlaylist(PlaylistSummary), + PrependUserPlaylist(Vec), + SetLoginFailure, + RefreshToken, + SetRefreshedToken { + token: String, + token_expiry_time: SystemTime, + }, + Logout, +} + +impl From for AppAction { + fn from(login_action: LoginAction) -> Self { + Self::LoginAction(login_action) + } +} + +#[derive(Clone, Debug)] +pub enum LoginStartedEvent { + Password { username: String, password: String }, + Token { username: String, token: String }, + OAuthSpotify {}, +} + +#[derive(Clone, Debug)] +pub enum LoginCompletedEvent { + Password(Credentials), + Token(Credentials), +} + +#[derive(Clone, Debug)] +pub enum LoginEvent { + LoginShown, + LoginStarted(LoginStartedEvent), + LoginCompleted(LoginCompletedEvent), + UserPlaylistsLoaded, + LoginFailed, + FreshTokenRequested, + RefreshTokenCompleted { + token: String, + token_expiry_time: SystemTime, + }, + LogoutCompleted, +} + +impl From for AppEvent { + fn from(login_event: LoginEvent) -> Self { + Self::LoginEvent(login_event) + } +} + +#[derive(Default)] +pub struct LoginState { + // Username + pub user: Option, + // Playlists owned by the logged in user + pub playlists: Vec, +} + +impl UpdatableState for LoginState { + type Action = LoginAction; + type Event = AppEvent; + + // The login state has a lot of actions that just translate to events + fn update_with(&mut self, action: Cow) -> Vec { + info!("update_with({:?})", action); + match action.into_owned() { + LoginAction::ShowLogin => vec![LoginEvent::LoginShown.into()], + LoginAction::TryLogin(TryLoginAction::Password { username, password }) => { + vec![ + LoginEvent::LoginStarted(LoginStartedEvent::Password { username, password }) + .into(), + ] + } + LoginAction::TryLogin(TryLoginAction::Token { username, token }) => { + vec![LoginEvent::LoginStarted(LoginStartedEvent::Token { username, token }).into()] + } + LoginAction::SetLoginSuccess(SetLoginSuccessAction::Password(creds)) => { + self.user = Some(creds.username.clone()); + vec![LoginEvent::LoginCompleted(LoginCompletedEvent::Password(creds)).into()] + } + LoginAction::SetLoginSuccess(SetLoginSuccessAction::Token(creds)) => { + self.user = Some(creds.username.clone()); + vec![LoginEvent::LoginCompleted(LoginCompletedEvent::Token(creds)).into()] + } + LoginAction::SetLoginFailure => vec![LoginEvent::LoginFailed.into()], + LoginAction::RefreshToken => vec![LoginEvent::FreshTokenRequested.into()], + LoginAction::SetRefreshedToken { + token, + token_expiry_time, + } => { + // translators: This notification is shown when, after some inactivity, the session is successfully restored. The user might have to repeat its last action. + vec![ + AppEvent::NotificationShown(gettext("Connection restored")), + LoginEvent::RefreshTokenCompleted { + token, + token_expiry_time, + } + .into(), + ] + } + LoginAction::Logout => { + self.user = None; + vec![LoginEvent::LogoutCompleted.into()] + } + LoginAction::SetUserPlaylists(playlists) => { + self.playlists = playlists; + vec![LoginEvent::UserPlaylistsLoaded.into()] + } + LoginAction::UpdateUserPlaylist(PlaylistSummary { id, title }) => { + if let Some(p) = self.playlists.iter_mut().find(|p| p.id == id) { + p.title = title; + } + vec![LoginEvent::UserPlaylistsLoaded.into()] + } + LoginAction::PrependUserPlaylist(mut summaries) => { + summaries.append(&mut self.playlists); + self.playlists = summaries; + vec![LoginEvent::UserPlaylistsLoaded.into()] + } + LoginAction::TryLogin(TryLoginAction::OAuthSpotify { .. }) => { + vec![LoginEvent::LoginStarted(LoginStartedEvent::OAuthSpotify {}).into()] + } + } + } +} diff --git a/src/app/state/mod.rs b/src/app/state/mod.rs new file mode 100644 index 0000000..2c3d272 --- /dev/null +++ b/src/app/state/mod.rs @@ -0,0 +1,25 @@ +mod app_model; +mod app_state; +mod browser_state; +mod login_state; +mod pagination; +mod playback_state; +mod screen_states; +mod selection_state; +mod settings_state; + +pub use app_model::AppModel; +pub use app_state::*; +pub use browser_state::*; +pub use login_state::*; +pub use playback_state::*; +pub use screen_states::*; +pub use selection_state::*; +pub use settings_state::*; + +pub trait UpdatableState { + type Action: Clone; + type Event; + + fn update_with(&mut self, action: std::borrow::Cow) -> Vec; +} diff --git a/src/app/state/pagination.rs b/src/app/state/pagination.rs new file mode 100644 index 0000000..b92a0e6 --- /dev/null +++ b/src/app/state/pagination.rs @@ -0,0 +1,60 @@ +// A structure for batched queries that I introduced before proper batch management +// Still used to load album lists for instance +// Doesn't know how many elements exist in total ahead of time +#[derive(Clone, Debug)] +pub struct Pagination +where + T: Clone, +{ + pub data: T, + // The next offset (of things to load) is set to None whenever we get less than we asked for + // as it probably means we've reached the end of some list + pub next_offset: Option, + pub batch_size: usize, +} + +impl Pagination +where + T: Clone, +{ + pub fn new(data: T, batch_size: usize) -> Self { + Self { + data, + next_offset: Some(0), + batch_size, + } + } + + pub fn reset_count(&mut self, new_length: usize) { + self.next_offset = if new_length >= self.batch_size { + Some(self.batch_size) + } else { + None + } + } + + pub fn set_loaded_count(&mut self, loaded_count: usize) { + if let Some(offset) = self.next_offset.take() { + self.next_offset = if loaded_count >= self.batch_size { + Some(offset + self.batch_size) + } else { + None + } + } + } + + // If we remove elements from paginated data without refetching from the source, + // we have to adjust the next offset to load + pub fn decrement(&mut self) { + if let Some(offset) = self.next_offset.take() { + self.next_offset = Some(offset - 1); + } + } + + // Same idea as decrement + pub fn increment(&mut self) { + if let Some(offset) = self.next_offset.take() { + self.next_offset = Some(offset + 1); + } + } +} diff --git a/src/app/state/playback_state.rs b/src/app/state/playback_state.rs new file mode 100644 index 0000000..5e07c94 --- /dev/null +++ b/src/app/state/playback_state.rs @@ -0,0 +1,785 @@ +use std::borrow::Cow; +use std::time::Instant; + +use crate::app::models::*; +use crate::app::state::{AppAction, AppEvent, UpdatableState}; +use crate::app::{BatchQuery, LazyRandomIndex, SongsSource}; + +#[derive(Debug)] +pub struct PlaybackState { + available_devices: Vec, + current_device: Device, + // A mapping of indices for shuffled playback + index: LazyRandomIndex, + // The actual list like thing backing the currently playing tracks + songs: SongListModel, + list_position: Option, + seek_position: PositionMillis, + source: Option, + repeat: RepeatMode, + is_playing: bool, + is_shuffled: bool, +} + +// Most mutatings methods shouldn't be pub +// If they are, they probably are only used by the app state +impl PlaybackState { + pub fn songs(&self) -> &SongListModel { + &self.songs + } + + pub fn is_playing(&self) -> bool { + self.is_playing && self.list_position.is_some() + } + + pub fn is_shuffled(&self) -> bool { + self.is_shuffled + } + + pub fn repeat_mode(&self) -> RepeatMode { + self.repeat + } + + // Whatever batch of songs we would need to grab if we were to play the next track + pub fn next_query(&self) -> Option { + let next_index = self.next_index()?; + let next_index = if self.is_shuffled { + self.index.get(next_index)? + } else { + next_index + }; + let batch = self.songs.needed_batch_for(next_index); + if let Some(batch) = batch { + let source = self.source.as_ref().cloned()?; + Some(BatchQuery { source, batch }) + } else { + None + } + } + + fn index(&self, i: usize) -> Option { + let song = if self.is_shuffled { + self.songs.index(self.index.get(i)?) + } else { + self.songs.index(i) + }; + Some(song?.into_description()) + } + + pub fn current_source(&self) -> Option<&SongsSource> { + self.source.as_ref() + } + + pub fn current_song_index(&self) -> Option { + self.list_position + } + + pub fn current_song_id(&self) -> Option { + Some(self.index(self.list_position?)?.id) + } + + pub fn current_song(&self) -> Option { + self.index(self.list_position?) + } + + fn next_id(&self) -> Option { + self.next_index() + .and_then(|i| Some(self.songs().index(i)?.description().id.clone())) + } + + fn clear(&mut self, source: Option) -> SongListModelPending { + self.source = source; + self.index = Default::default(); + self.list_position = None; + self.songs.clear() + } + + // Replaces (!) the current playlist with the contents of a song batch + fn set_batch(&mut self, source: Option, song_batch: SongBatch) -> bool { + let ok = self.clear(source).and(|s| s.add(song_batch)).commit(); + self.index.resize(self.songs.len()); + ok + } + + fn add_batch(&mut self, song_batch: SongBatch) -> bool { + let ok = self.songs.add(song_batch).commit(); + self.index.resize(self.songs.len()); + ok + } + + // Replaces (!) the current playlist with a bunch of songs (not batched, not expected to grow) + fn set_queue(&mut self, tracks: Vec) { + self.clear(None).and(|s| s.append(tracks)).commit(); + self.index.grow(self.songs.len()); + } + + pub fn queue(&mut self, tracks: Vec) { + self.source = None; + self.songs.append(tracks).commit(); + self.index.grow(self.songs.len()); + } + + pub fn dequeue(&mut self, ids: &[String]) { + let current_id = self.current_song_id(); + self.songs.remove(ids).commit(); + self.list_position = current_id.and_then(|id| self.songs.find_index(&id)); + self.index.shrink(self.songs.len()); + } + + // Update the current playing track (identified by a position in the list) if we're swapping songs + fn swap_pos(&mut self, index: usize, other_index: usize) { + let len = self.songs.len(); + self.list_position = self + .list_position + .map(|position| match position { + i if i == index => other_index, + i if i == other_index => index, + _ => position, + }) + .map(|p| usize::min(p, len - 1)) + } + + pub fn move_down(&mut self, id: &str) -> Option { + let index = self.songs.find_index(id)?; + self.songs.move_down(index).commit(); + self.swap_pos(index + 1, index); + Some(index) + } + + pub fn move_up(&mut self, id: &str) -> Option { + let index = self.songs.find_index(id).filter(|&index| index > 0)?; + self.songs.move_up(index).commit(); + self.swap_pos(index - 1, index); + Some(index) + } + + fn play(&mut self, id: &str) -> bool { + if self.current_song_id().map(|cur| cur == id).unwrap_or(false) { + return false; + } + + let found_index = self.songs.find_index(id); + + if let Some(index) = found_index { + // If shufflings songs, we make sure the track we just picked is the first to come up + if self.is_shuffled { + self.index.reset_picking_first(index); + self.play_index(0); + } else { + self.play_index(index); + } + true + } else { + false + } + } + + fn stop(&mut self) { + self.list_position = None; + self.is_playing = false; + self.seek_position.set(0, false); + } + + fn play_index(&mut self, index: usize) -> Option { + self.is_playing = true; + self.list_position.replace(index); + self.seek_position.set(0, true); + self.index.next_until(index + 1); + self.current_song_id() + } + + fn play_next(&mut self) -> Option { + self.next_index().and_then(|i| { + self.seek_position.set(0, true); + self.play_index(i) + }) + } + + pub fn next_index(&self) -> Option { + let len = self.songs.len(); + self.list_position.and_then(|p| match self.repeat { + RepeatMode::Song => Some(p), + RepeatMode::Playlist if len != 0 => Some((p + 1) % len), + RepeatMode::None => Some(p + 1).filter(|&i| i < len), + _ => None, + }) + } + + fn play_prev(&mut self) -> Option { + self.prev_index().and_then(|i| { + // Only jump to the previous track if we aren't more than 2 seconds (2,000 ms) into the current track. + // Otherwise, seek to the start of the current track. + // (This replicates the behavior of official Spotify clients.) + if self.seek_position.current() <= 2000 { + self.seek_position.set(0, true); + self.play_index(i) + } else { + self.seek_position.set(0, true); + None + } + }) + } + + pub fn prev_index(&self) -> Option { + let len = self.songs.len(); + self.list_position.and_then(|p| match self.repeat { + RepeatMode::Song => Some(p), + RepeatMode::Playlist if len != 0 => Some((if p == 0 { len } else { p }) - 1), + RepeatMode::None => Some(p).filter(|&i| i > 0).map(|i| i - 1), + _ => None, + }) + } + + fn toggle_play(&mut self) -> Option { + if self.list_position.is_some() { + self.is_playing = !self.is_playing; + + match self.is_playing { + false => self.seek_position.pause(), + true => self.seek_position.resume(), + }; + + Some(self.is_playing) + } else { + None + } + } + + fn set_shuffled(&mut self, shuffled: bool) { + self.is_shuffled = shuffled; + let old = self.list_position.replace(0).unwrap_or(0); + self.index.reset_picking_first(old); + } + + pub fn available_devices(&self) -> &Vec { + &self.available_devices + } + + pub fn current_device(&self) -> &Device { + &self.current_device + } +} + +impl Default for PlaybackState { + fn default() -> Self { + Self { + available_devices: vec![], + current_device: Device::Local, + index: LazyRandomIndex::default(), + songs: SongListModel::new(50), + list_position: None, + seek_position: PositionMillis::new(1.0), + source: None, + repeat: RepeatMode::None, + is_playing: false, + is_shuffled: false, + } + } +} + +#[derive(Clone, Debug)] +pub enum PlaybackAction { + TogglePlay, + Play, + Pause, + Stop, + SetRepeatMode(RepeatMode), + SetShuffled(bool), + ToggleRepeat, + ToggleShuffle, + Seek(u32), + // I can't remember the diff betweek Seek and SyncSeek right now. Probably the source of the action + SyncSeek(u32), + Load(String), + LoadSongs(Vec), + LoadPagedSongs(SongsSource, SongBatch), + SetVolume(f64), + Next, + Previous, + Preload, + Queue(Vec), + Dequeue(String), + SwitchDevice(Device), + SetAvailableDevices(Vec), +} + +impl From for AppAction { + fn from(playback_action: PlaybackAction) -> Self { + Self::PlaybackAction(playback_action) + } +} + +#[derive(Clone, Debug)] +pub enum Device { + Local, + Connect(ConnectDevice), +} + +#[derive(Clone, Debug)] +pub enum PlaybackEvent { + PlaybackPaused, + PlaybackResumed, + RepeatModeChanged(RepeatMode), + TrackSeeked(u32), + SeekSynced(u32), + VolumeSet(f64), + TrackChanged(String), + SourceChanged, + Preload(String), + ShuffleChanged(bool), + PlaylistChanged, + PlaybackStopped, + SwitchedDevice(Device), + AvailableDevicesChanged, +} + +impl From for AppEvent { + fn from(playback_event: PlaybackEvent) -> Self { + Self::PlaybackEvent(playback_event) + } +} + +impl UpdatableState for PlaybackState { + type Action = PlaybackAction; + type Event = PlaybackEvent; + + // Main "reducer" :) + fn update_with(&mut self, action: Cow) -> Vec { + match action.into_owned() { + PlaybackAction::TogglePlay => { + if let Some(playing) = self.toggle_play() { + if playing { + vec![PlaybackEvent::PlaybackResumed] + } else { + vec![PlaybackEvent::PlaybackPaused] + } + } else { + vec![] + } + } + PlaybackAction::Play => { + if !self.is_playing() && self.toggle_play() == Some(true) { + vec![PlaybackEvent::PlaybackResumed] + } else { + vec![] + } + } + PlaybackAction::Pause => { + if self.is_playing() && self.toggle_play() == Some(false) { + vec![PlaybackEvent::PlaybackPaused] + } else { + vec![] + } + } + PlaybackAction::ToggleRepeat => { + self.repeat = match self.repeat { + RepeatMode::Song => RepeatMode::None, + RepeatMode::Playlist => RepeatMode::Song, + RepeatMode::None => RepeatMode::Playlist, + }; + vec![PlaybackEvent::RepeatModeChanged(self.repeat)] + } + PlaybackAction::SetRepeatMode(mode) if self.repeat != mode => { + self.repeat = mode; + vec![PlaybackEvent::RepeatModeChanged(self.repeat)] + } + PlaybackAction::SetShuffled(shuffled) if self.is_shuffled != shuffled => { + self.set_shuffled(shuffled); + vec![PlaybackEvent::ShuffleChanged(shuffled)] + } + PlaybackAction::ToggleShuffle => { + self.set_shuffled(!self.is_shuffled); + vec![PlaybackEvent::ShuffleChanged(self.is_shuffled)] + } + PlaybackAction::Next => { + if let Some(id) = self.play_next() { + vec![ + PlaybackEvent::TrackChanged(id), + PlaybackEvent::PlaybackResumed, + ] + } else { + self.stop(); + vec![PlaybackEvent::PlaybackStopped] + } + } + PlaybackAction::Stop => { + self.stop(); + vec![PlaybackEvent::PlaybackStopped] + } + PlaybackAction::Previous => { + if let Some(id) = self.play_prev() { + vec![ + PlaybackEvent::TrackChanged(id), + PlaybackEvent::PlaybackResumed, + ] + } else { + vec![PlaybackEvent::TrackSeeked(0)] + } + } + PlaybackAction::Load(id) => { + if self.play(&id) { + vec![ + PlaybackEvent::TrackChanged(id), + PlaybackEvent::PlaybackResumed, + ] + } else { + vec![] + } + } + PlaybackAction::Preload => { + if let Some(id) = self.next_id() { + vec![PlaybackEvent::Preload(id)] + } else { + vec![] + } + } + PlaybackAction::LoadPagedSongs(source, batch) + if Some(&source) == self.source.as_ref() => + { + if self.add_batch(batch) { + vec![PlaybackEvent::PlaylistChanged] + } else { + vec![] + } + } + PlaybackAction::LoadPagedSongs(source, batch) + if Some(&source) != self.source.as_ref() => + { + debug!("new source: {:?}", &source); + self.set_batch(Some(source), batch); + vec![PlaybackEvent::PlaylistChanged, PlaybackEvent::SourceChanged] + } + PlaybackAction::LoadSongs(tracks) => { + self.set_queue(tracks); + vec![PlaybackEvent::PlaylistChanged, PlaybackEvent::SourceChanged] + } + PlaybackAction::Queue(tracks) => { + self.queue(tracks); + vec![PlaybackEvent::PlaylistChanged] + } + PlaybackAction::Dequeue(id) => { + self.dequeue(&[id]); + vec![PlaybackEvent::PlaylistChanged] + } + PlaybackAction::Seek(pos) => { + self.seek_position.set(pos as u64 * 1000, true); + vec![PlaybackEvent::TrackSeeked(pos)] + } + PlaybackAction::SyncSeek(pos) => { + self.seek_position.set(pos as u64 * 1000, true); + vec![PlaybackEvent::SeekSynced(pos)] + } + PlaybackAction::SetVolume(volume) => vec![PlaybackEvent::VolumeSet(volume)], + PlaybackAction::SetAvailableDevices(list) => { + self.available_devices = list; + vec![PlaybackEvent::AvailableDevicesChanged] + } + PlaybackAction::SwitchDevice(new_device) => { + self.current_device = new_device.clone(); + vec![PlaybackEvent::SwitchedDevice(new_device)] + } + _ => vec![], + } + } +} + +// A struct to keep track of the playback position +// Caller must call pause/play at the right time +#[derive(Debug)] +struct PositionMillis { + // Last recorded position in the track (in milliseconds) + last_known_position: u64, + // Last time we resumed playback + last_resume_instant: Option, + // Playback rate (1) + rate: f32, +} + +impl PositionMillis { + fn new(rate: f32) -> Self { + Self { + last_known_position: 0, + last_resume_instant: None, + rate, + } + } + + // Read the current pos by adding elapsed time since the last time we resumed playback to the last know position + fn current(&self) -> u64 { + let current_progress = self.last_resume_instant.map(|ri| { + let elapsed = ri.elapsed().as_millis() as f32; + let real_elapsed = self.rate * elapsed; + real_elapsed.ceil() as u64 + }); + self.last_known_position + current_progress.unwrap_or(0) + } + + fn set(&mut self, position: u64, playing: bool) { + self.last_known_position = position; + self.last_resume_instant = if playing { Some(Instant::now()) } else { None } + } + + fn pause(&mut self) { + self.last_known_position = self.current(); + self.last_resume_instant = None; + } + + fn resume(&mut self) { + self.last_resume_instant = Some(Instant::now()); + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::app::models::AlbumRef; + + fn song(id: &str) -> SongDescription { + SongDescription { + id: id.to_string(), + uri: "".to_string(), + title: "Title".to_string(), + artists: vec![], + album: AlbumRef { + id: "".to_string(), + name: "".to_string(), + }, + duration: 1000, + art: None, + track_number: None, + } + } + + impl PlaybackState { + fn current_position(&self) -> Option { + self.list_position + } + + fn prev_id(&self) -> Option { + self.prev_index() + .and_then(|i| Some(self.songs().index(i)?.description().id.clone())) + } + + fn song_ids(&self) -> Vec { + self.songs() + .collect() + .iter() + .map(|s| s.id.clone()) + .collect() + } + } + + #[test] + fn test_initial_state() { + let state = PlaybackState::default(); + assert!(!state.is_playing()); + assert!(!state.is_shuffled()); + assert!(state.current_song().is_none()); + assert!(state.prev_index().is_none()); + assert!(state.next_index().is_none()); + } + + #[test] + fn test_play_one() { + let mut state = PlaybackState::default(); + state.queue(vec![song("foo")]); + + state.play("foo"); + assert!(state.is_playing()); + + assert_eq!(state.current_song_id(), Some("foo".to_string())); + assert!(state.prev_index().is_none()); + assert!(state.next_index().is_none()); + + state.toggle_play(); + assert!(!state.is_playing()); + } + + #[test] + fn test_queue() { + let mut state = PlaybackState::default(); + state.queue(vec![song("1"), song("2"), song("3")]); + + assert_eq!(state.songs().len(), 3); + + state.play("2"); + + state.queue(vec![song("4")]); + assert_eq!(state.songs().len(), 4); + } + + #[test] + fn test_play_multiple() { + let mut state = PlaybackState::default(); + state.queue(vec![song("1"), song("2"), song("3")]); + assert_eq!(state.songs().len(), 3); + + state.play("2"); + assert!(state.is_playing()); + + assert_eq!(state.current_position(), Some(1)); + assert_eq!(state.prev_id(), Some("1".to_string())); + assert_eq!(state.current_song_id(), Some("2".to_string())); + assert_eq!(state.next_id(), Some("3".to_string())); + + state.toggle_play(); + assert!(!state.is_playing()); + + state.play_next(); + assert!(state.is_playing()); + assert_eq!(state.current_position(), Some(2)); + assert_eq!(state.prev_id(), Some("2".to_string())); + assert_eq!(state.current_song_id(), Some("3".to_string())); + assert!(state.next_index().is_none()); + + state.play_next(); + assert!(state.is_playing()); + assert_eq!(state.current_position(), Some(2)); + assert_eq!(state.current_song_id(), Some("3".to_string())); + + state.play_prev(); + state.play_prev(); + assert!(state.is_playing()); + assert_eq!(state.current_position(), Some(0)); + assert!(state.prev_index().is_none()); + assert_eq!(state.current_song_id(), Some("1".to_string())); + assert_eq!(state.next_id(), Some("2".to_string())); + + state.play_prev(); + assert!(state.is_playing()); + assert_eq!(state.current_position(), Some(0)); + assert_eq!(state.current_song_id(), Some("1".to_string())); + } + + #[test] + fn test_shuffle() { + let mut state = PlaybackState::default(); + state.queue(vec![song("1"), song("2"), song("3"), song("4")]); + + assert_eq!(state.songs().len(), 4); + + state.play("2"); + assert_eq!(state.current_position(), Some(1)); + + state.set_shuffled(true); + assert!(state.is_shuffled()); + assert_eq!(state.current_position(), Some(0)); + + state.play_next(); + assert_eq!(state.current_position(), Some(1)); + + state.set_shuffled(false); + assert!(!state.is_shuffled()); + + let ids = state.song_ids(); + assert_eq!( + ids, + vec![ + "1".to_string(), + "2".to_string(), + "3".to_string(), + "4".to_string() + ] + ); + } + + #[test] + fn test_shuffle_queue() { + let mut state = PlaybackState::default(); + state.queue(vec![song("1"), song("2"), song("3")]); + + state.set_shuffled(true); + assert!(state.is_shuffled()); + + state.queue(vec![song("4")]); + + state.set_shuffled(false); + assert!(!state.is_shuffled()); + + let ids = state.song_ids(); + assert_eq!( + ids, + vec![ + "1".to_string(), + "2".to_string(), + "3".to_string(), + "4".to_string() + ] + ); + } + + #[test] + fn test_move() { + let mut state = PlaybackState::default(); + state.queue(vec![song("1"), song("2"), song("3")]); + + state.play("2"); + assert!(state.is_playing()); + + state.move_down("1"); + assert_eq!(state.current_song_id(), Some("2".to_string())); + let ids = state.song_ids(); + assert_eq!(ids, vec!["2".to_string(), "1".to_string(), "3".to_string()]); + + state.move_down("2"); + state.move_down("2"); + assert_eq!(state.current_song_id(), Some("2".to_string())); + let ids = state.song_ids(); + assert_eq!(ids, vec!["1".to_string(), "3".to_string(), "2".to_string()]); + + state.move_down("2"); + assert_eq!(state.current_song_id(), Some("2".to_string())); + let ids = state.song_ids(); + assert_eq!(ids, vec!["1".to_string(), "3".to_string(), "2".to_string()]); + + state.move_up("2"); + + assert_eq!(state.current_song_id(), Some("2".to_string())); + let ids = state.song_ids(); + assert_eq!(ids, vec!["1".to_string(), "2".to_string(), "3".to_string()]); + } + + #[test] + fn test_dequeue_last() { + let mut state = PlaybackState::default(); + state.queue(vec![song("1"), song("2"), song("3")]); + + state.play("3"); + assert!(state.is_playing()); + + state.dequeue(&["3".to_string()]); + assert_eq!(state.current_song_id(), None); + } + + #[test] + fn test_dequeue_a_few_songs() { + let mut state = PlaybackState::default(); + state.queue(vec![ + song("1"), + song("2"), + song("3"), + song("4"), + song("5"), + song("6"), + ]); + + state.play("5"); + assert!(state.is_playing()); + + state.dequeue(&["1".to_string(), "2".to_string(), "3".to_string()]); + assert_eq!(state.current_song_id(), Some("5".to_string())); + } + + #[test] + fn test_dequeue_all() { + let mut state = PlaybackState::default(); + state.queue(vec![song("3")]); + + state.play("3"); + assert!(state.is_playing()); + + state.dequeue(&["3".to_string()]); + assert_eq!(state.current_song_id(), None); + } +} diff --git a/src/app/state/screen_states.rs b/src/app/state/screen_states.rs new file mode 100644 index 0000000..55240ae --- /dev/null +++ b/src/app/state/screen_states.rs @@ -0,0 +1,471 @@ +use std::borrow::Cow; +use std::cmp::PartialEq; + +use super::{pagination::Pagination, BrowserAction, BrowserEvent, UpdatableState}; +use crate::app::models::*; +use crate::app::ListStore; + +#[derive(Clone, Debug)] +pub enum ScreenName { + Home, + AlbumDetails(String), + Search, + Artist(String), + PlaylistDetails(String), + User(String), +} + +impl ScreenName { + pub fn identifier(&self) -> Cow { + match self { + Self::Home => Cow::Borrowed("home"), + Self::AlbumDetails(s) => Cow::Owned(format!("album_{s}")), + Self::Search => Cow::Borrowed("search"), + Self::Artist(s) => Cow::Owned(format!("artist_{s}")), + Self::PlaylistDetails(s) => Cow::Owned(format!("playlist_{s}")), + Self::User(s) => Cow::Owned(format!("user_{s}")), + } + } +} + +impl PartialEq for ScreenName { + fn eq(&self, other: &Self) -> bool { + self.identifier() == other.identifier() + } +} + +impl Eq for ScreenName {} + +// ALBUM details +pub struct DetailsState { + pub id: String, + pub name: ScreenName, + pub content: Option, + // Read the songs from here, not content (won't get more than the initial batch of songs) + pub songs: SongListModel, +} + +impl DetailsState { + pub fn new(id: String) -> Self { + Self { + id: id.clone(), + name: ScreenName::AlbumDetails(id), + content: None, + songs: SongListModel::new(50), + } + } +} + +impl UpdatableState for DetailsState { + type Action = BrowserAction; + type Event = BrowserEvent; + + fn update_with(&mut self, action: Cow) -> Vec { + match action.as_ref() { + BrowserAction::SetAlbumDetails(album) if album.description.id == self.id => { + let AlbumDescription { id, songs, .. } = album.description.clone(); + self.songs.add(songs).commit(); + self.content = Some(*album.clone()); + vec![BrowserEvent::AlbumDetailsLoaded(id)] + } + BrowserAction::AppendAlbumTracks(id, batch) if id == &self.id => { + self.songs.add(*batch.clone()).commit(); + vec![BrowserEvent::AlbumTracksAppended(id.clone())] + } + BrowserAction::SaveAlbum(album) if album.id == self.id => { + let id = album.id.clone(); + if let Some(album) = self.content.as_mut() { + album.description.is_liked = true; + vec![BrowserEvent::AlbumSaved(id)] + } else { + vec![] + } + } + BrowserAction::UnsaveAlbum(id) if id == &self.id => { + if let Some(album) = self.content.as_mut() { + album.description.is_liked = false; + vec![BrowserEvent::AlbumUnsaved(id.clone())] + } else { + vec![] + } + } + _ => vec![], + } + } +} + +pub struct PlaylistDetailsState { + pub id: String, + pub name: ScreenName, + pub playlist: Option, + // Read the songs from here, not content (won't get more than the initial batch of songs) + pub songs: SongListModel, +} + +impl PlaylistDetailsState { + pub fn new(id: String) -> Self { + Self { + id: id.clone(), + name: ScreenName::PlaylistDetails(id), + playlist: None, + songs: SongListModel::new(100), + } + } +} + +impl UpdatableState for PlaylistDetailsState { + type Action = BrowserAction; + type Event = BrowserEvent; + + fn update_with(&mut self, action: Cow) -> Vec { + match action.as_ref() { + BrowserAction::SetPlaylistDetails(playlist) if playlist.id == self.id => { + let PlaylistDescription { id, songs, .. } = *playlist.clone(); + self.songs.add(songs).commit(); + self.playlist = Some(*playlist.clone()); + vec![BrowserEvent::PlaylistDetailsLoaded(id)] + } + BrowserAction::UpdatePlaylistName(PlaylistSummary { id, title }) if id == &self.id => { + if let Some(p) = self.playlist.as_mut() { + p.title = title.clone(); + } + vec![BrowserEvent::PlaylistDetailsLoaded(self.id.clone())] + } + BrowserAction::AppendPlaylistTracks(id, song_batch) if id == &self.id => { + self.songs.add(*song_batch.clone()).commit(); + vec![BrowserEvent::PlaylistTracksAppended(id.clone())] + } + BrowserAction::RemoveTracksFromPlaylist(id, uris) if id == &self.id => { + self.songs.remove(&uris[..]).commit(); + vec![BrowserEvent::PlaylistTracksRemoved(self.id.clone())] + } + _ => vec![], + } + } +} + +pub struct ArtistState { + pub id: String, + pub name: ScreenName, + pub artist: Option, + pub next_page: Pagination, + pub albums: ListStore, + pub top_tracks: SongListModel, +} + +impl ArtistState { + pub fn new(id: String) -> Self { + Self { + id: id.clone(), + name: ScreenName::Artist(id.clone()), + artist: None, + next_page: Pagination::new(id, 20), + albums: ListStore::new(), + top_tracks: SongListModel::new(10), + } + } +} + +impl UpdatableState for ArtistState { + type Action = BrowserAction; + type Event = BrowserEvent; + + fn update_with(&mut self, action: Cow) -> Vec { + match action.as_ref() { + BrowserAction::SetArtistDetails(details) if details.id == self.id => { + let ArtistDescription { + id, + name, + albums, + mut top_tracks, + } = *details.clone(); + self.artist = Some(name); + self.albums + .replace_all(albums.into_iter().map(|a| a.into())); + self.next_page.reset_count(self.albums.len()); + + top_tracks.truncate(5); + self.top_tracks.append(top_tracks).commit(); + + vec![BrowserEvent::ArtistDetailsUpdated(id)] + } + BrowserAction::AppendArtistReleases(id, albums) if id == &self.id => { + self.next_page.set_loaded_count(albums.len()); + self.albums.extend(albums.iter().map(|a| a.into())); + vec![BrowserEvent::ArtistDetailsUpdated(self.id.clone())] + } + _ => vec![], + } + } +} + +// The "home" represents screens visible initially (saved albums, saved playlists, saved tracks) +pub struct HomeState { + pub name: ScreenName, + pub visible_page: &'static str, + pub next_albums_page: Pagination<()>, + pub albums: ListStore, + pub next_playlists_page: Pagination<()>, + pub playlists: ListStore, + pub saved_tracks: SongListModel, +} + +impl Default for HomeState { + fn default() -> Self { + Self { + name: ScreenName::Home, + visible_page: "library", + next_albums_page: Pagination::new((), 30), + albums: ListStore::new(), + next_playlists_page: Pagination::new((), 30), + playlists: ListStore::new(), + saved_tracks: SongListModel::new(50), + } + } +} + +impl UpdatableState for HomeState { + type Action = BrowserAction; + type Event = BrowserEvent; + + fn update_with(&mut self, action: Cow) -> Vec { + match action.as_ref() { + BrowserAction::SetHomeVisiblePage(page) => { + self.visible_page = *page; + vec![BrowserEvent::HomeVisiblePageChanged(page)] + } + BrowserAction::SetLibraryContent(content) => { + if !self.albums.eq(content, |a, b| a.uri() == b.id) { + self.albums.replace_all(content.iter().map(|a| a.into())); + self.next_albums_page.reset_count(self.albums.len()); + vec![BrowserEvent::LibraryUpdated] + } else { + vec![] + } + } + BrowserAction::PrependPlaylistsContent(content) => { + self.playlists.prepend(content.iter().map(|a| a.into())); + vec![BrowserEvent::SavedPlaylistsUpdated] + } + BrowserAction::AppendLibraryContent(content) => { + self.next_albums_page.set_loaded_count(content.len()); + self.albums.extend(content.iter().map(|a| a.into())); + vec![BrowserEvent::LibraryUpdated] + } + BrowserAction::SaveAlbum(album) => { + let album_id = album.id.clone(); + let already_present = self.albums.iter().any(|a| a.uri() == album_id); + if already_present { + vec![] + } else { + self.albums.insert(0, (*album.clone()).into()); + self.next_albums_page.increment(); + vec![BrowserEvent::LibraryUpdated] + } + } + BrowserAction::UnsaveAlbum(id) => { + let position = self.albums.iter().position(|a| a.uri() == *id); + if let Some(position) = position { + self.albums.remove(position as u32); + self.next_albums_page.decrement(); + vec![BrowserEvent::LibraryUpdated] + } else { + vec![] + } + } + BrowserAction::SetPlaylistsContent(content) => { + if !self.playlists.eq(content, |a, b| a.uri() == b.id) { + self.playlists.replace_all(content.iter().map(|a| a.into())); + self.next_playlists_page.reset_count(self.playlists.len()); + vec![BrowserEvent::SavedPlaylistsUpdated] + } else { + vec![] + } + } + BrowserAction::AppendPlaylistsContent(content) => { + self.next_playlists_page.set_loaded_count(content.len()); + self.playlists.extend(content.iter().map(|p| p.into())); + vec![BrowserEvent::SavedPlaylistsUpdated] + } + BrowserAction::UpdatePlaylistName(PlaylistSummary { id, title }) => { + if let Some(p) = self.playlists.iter().find(|p| &p.uri() == id) { + p.set_album(title.to_owned()); + } + vec![BrowserEvent::SavedPlaylistsUpdated] + } + BrowserAction::AppendSavedTracks(song_batch) => { + if self.saved_tracks.add(*song_batch.clone()).commit() { + vec![BrowserEvent::SavedTracksUpdated] + } else { + vec![] + } + } + BrowserAction::SetSavedTracks(song_batch) => { + let song_batch = *song_batch.clone(); + if self + .saved_tracks + .clear() + .and(|s| s.add(song_batch)) + .commit() + { + vec![BrowserEvent::SavedTracksUpdated] + } else { + vec![] + } + } + BrowserAction::SaveTracks(tracks) => { + self.saved_tracks.prepend(tracks.clone()).commit(); + vec![BrowserEvent::SavedTracksUpdated] + } + BrowserAction::RemoveSavedTracks(tracks) => { + self.saved_tracks.remove(&tracks[..]).commit(); + vec![BrowserEvent::SavedTracksUpdated] + } + _ => vec![], + } + } +} + +pub struct SearchState { + pub name: ScreenName, + pub query: String, + pub album_results: Vec, + pub artist_results: Vec, +} + +impl Default for SearchState { + fn default() -> Self { + Self { + name: ScreenName::Search, + query: "".to_owned(), + album_results: vec![], + artist_results: vec![], + } + } +} + +impl UpdatableState for SearchState { + type Action = BrowserAction; + type Event = BrowserEvent; + + fn update_with(&mut self, action: Cow) -> Vec { + match action.as_ref() { + BrowserAction::Search(query) if query != &self.query => { + self.query = query.clone(); + vec![BrowserEvent::SearchUpdated] + } + BrowserAction::SetSearchResults(results) => { + self.album_results = results.albums.clone(); + self.artist_results = results.artists.clone(); + vec![BrowserEvent::SearchResultsUpdated] + } + _ => vec![], + } + } +} + +// Screen when we click on the name of a playlist owner +pub struct UserState { + pub id: String, + pub name: ScreenName, + pub user: Option, + pub next_page: Pagination, + pub playlists: ListStore, +} + +impl UserState { + pub fn new(id: String) -> Self { + Self { + id: id.clone(), + name: ScreenName::User(id.clone()), + user: None, + next_page: Pagination::new(id, 30), + playlists: ListStore::new(), + } + } +} + +impl UpdatableState for UserState { + type Action = BrowserAction; + type Event = BrowserEvent; + + fn update_with(&mut self, action: Cow) -> Vec { + match action.as_ref() { + BrowserAction::SetUserDetails(user) if user.id == self.id => { + let UserDescription { + id, + name, + playlists, + } = *user.clone(); + self.user = Some(name); + self.playlists + .replace_all(playlists.iter().map(|p| p.into())); + self.next_page.reset_count(self.playlists.len()); + + vec![BrowserEvent::UserDetailsUpdated(id)] + } + BrowserAction::AppendUserPlaylists(id, playlists) if id == &self.id => { + self.next_page.set_loaded_count(playlists.len()); + self.playlists.extend(playlists.iter().map(|p| p.into())); + vec![BrowserEvent::UserDetailsUpdated(self.id.clone())] + } + _ => vec![], + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_next_page_no_next() { + let mut artist_state = ArtistState::new("id".to_owned()); + artist_state.update_with(Cow::Owned(BrowserAction::SetArtistDetails(Box::new( + ArtistDescription { + id: "id".to_owned(), + name: "Foo".to_owned(), + albums: vec![], + top_tracks: vec![], + }, + )))); + + let next = artist_state.next_page; + assert_eq!(None, next.next_offset); + } + + #[test] + fn test_next_page_more() { + let fake_album = AlbumDescription { + id: "".to_owned(), + title: "".to_owned(), + artists: vec![], + release_date: Some("1970-01-01".to_owned()), + art: Some("".to_owned()), + songs: SongBatch::empty(), + is_liked: false, + }; + let id = "id".to_string(); + let mut artist_state = ArtistState::new(id.clone()); + artist_state.update_with(Cow::Owned(BrowserAction::SetArtistDetails(Box::new( + ArtistDescription { + id: id.clone(), + name: "Foo".to_owned(), + albums: (0..20).map(|_| fake_album.clone()).collect(), + top_tracks: vec![], + }, + )))); + + let next = &artist_state.next_page; + assert_eq!(Some(20), next.next_offset); + + artist_state.update_with(Cow::Owned(BrowserAction::AppendArtistReleases( + id.clone(), + vec![], + ))); + + let next = &artist_state.next_page; + assert_eq!(None, next.next_offset); + } +} diff --git a/src/app/state/selection_state.rs b/src/app/state/selection_state.rs new file mode 100644 index 0000000..c1847b1 --- /dev/null +++ b/src/app/state/selection_state.rs @@ -0,0 +1,153 @@ +use std::borrow::Cow; +use std::collections::HashSet; + +use crate::app::models::SongDescription; +use crate::app::state::{AppAction, AppEvent, UpdatableState}; + +#[derive(Clone, Debug)] +pub enum SelectionAction { + Select(Vec), + Deselect(Vec), + Clear, +} + +impl From for AppAction { + fn from(selection_action: SelectionAction) -> Self { + Self::SelectionAction(selection_action) + } +} + +#[derive(Clone, Debug)] +pub enum SelectionEvent { + // Mode means selection active or not + SelectionModeChanged(bool), + SelectionChanged, +} + +impl From for AppEvent { + fn from(selection_event: SelectionEvent) -> Self { + Self::SelectionEvent(selection_event) + } +} + +#[derive(Debug, Clone)] +pub enum SelectionContext { + ReadOnlyQueue, + Queue, + Playlist, + EditablePlaylist(String), + SavedTracks, + Default, +} + +pub struct SelectionState { + selected_songs: Vec, + selected_songs_ids: HashSet, + selection_active: bool, + pub context: SelectionContext, +} + +impl Default for SelectionState { + fn default() -> Self { + Self { + selected_songs: Default::default(), + selected_songs_ids: Default::default(), + selection_active: false, + context: SelectionContext::Default, + } + } +} + +impl SelectionState { + fn select(&mut self, song: SongDescription) -> bool { + let selected = self.selected_songs_ids.contains(&song.id); + if !selected { + self.selected_songs_ids.insert(song.id.clone()); + self.selected_songs.push(song); + } + !selected + } + + fn deselect(&mut self, id: &str) -> bool { + let songs: Vec = std::mem::take(&mut self.selected_songs) + .into_iter() + .filter(|s| s.id != id) + .collect(); + self.selected_songs = songs; + self.selected_songs_ids.remove(id) + } + + pub fn set_mode(&mut self, context: Option) -> Option { + let currently_active = self.selection_active; + match (currently_active, context) { + (false, Some(context)) => { + *self = Default::default(); + self.selection_active = true; + self.context = context; + Some(true) + } + (true, None) => { + *self = Default::default(); + self.selection_active = false; + Some(false) + } + _ => None, + } + } + + pub fn is_selection_enabled(&self) -> bool { + self.selection_active + } + + pub fn is_song_selected(&self, id: &str) -> bool { + self.selected_songs_ids.contains(id) + } + + pub fn count(&self) -> usize { + self.selected_songs_ids.len() + } + + // Clears (!) the selection, returns associated memory + pub fn take_selection(&mut self) -> Vec { + std::mem::take(self).selected_songs + } + + // Just have a look at the selection without changing it + pub fn peek_selection(&self) -> impl Iterator { + self.selected_songs.iter() + } +} + +impl UpdatableState for SelectionState { + type Action = SelectionAction; + type Event = SelectionEvent; + + fn update_with(&mut self, action: Cow) -> Vec { + match action.into_owned() { + SelectionAction::Select(tracks) => { + let changed = tracks + .into_iter() + .fold(false, |result, track| self.select(track) || result); + if changed { + vec![SelectionEvent::SelectionChanged] + } else { + vec![] + } + } + SelectionAction::Deselect(ids) => { + let changed = ids + .iter() + .fold(false, |result, id| self.deselect(id) || result); + if changed { + vec![SelectionEvent::SelectionChanged] + } else { + vec![] + } + } + SelectionAction::Clear => { + self.take_selection(); + vec![SelectionEvent::SelectionModeChanged(false)] + } + } + } +} diff --git a/src/app/state/settings_state.rs b/src/app/state/settings_state.rs new file mode 100644 index 0000000..60e3b43 --- /dev/null +++ b/src/app/state/settings_state.rs @@ -0,0 +1,54 @@ +use crate::{ + app::state::{AppAction, AppEvent, UpdatableState}, + settings::SpotSettings, +}; + +#[derive(Clone, Debug)] +pub enum SettingsAction { + ChangeSettings, +} + +impl From for AppAction { + fn from(settings_action: SettingsAction) -> Self { + Self::SettingsAction(settings_action) + } +} + +#[derive(Clone, Debug)] +pub enum SettingsEvent { + PlayerSettingsChanged, +} + +impl From for AppEvent { + fn from(settings_event: SettingsEvent) -> Self { + Self::SettingsEvent(settings_event) + } +} + +#[derive(Default)] +pub struct SettingsState { + // Probably shouldn't be stored, the source of truth is GSettings anyway + pub settings: SpotSettings, +} + +impl UpdatableState for SettingsState { + type Action = SettingsAction; + type Event = AppEvent; + + fn update_with(&mut self, action: std::borrow::Cow) -> Vec { + match action.into_owned() { + SettingsAction::ChangeSettings => { + let old_settings = &self.settings; + let new_settings = SpotSettings::new_from_gsettings().unwrap_or_default(); + let player_settings_changed = + new_settings.player_settings != old_settings.player_settings; + self.settings = new_settings; + if player_settings_changed { + vec![SettingsEvent::PlayerSettingsChanged.into()] + } else { + vec![] + } + } + } + } +} diff --git a/src/config.rs.in b/src/config.rs.in new file mode 100644 index 0000000..05f5aff --- /dev/null +++ b/src/config.rs.in @@ -0,0 +1,5 @@ +// Configured by meson +pub static PKGDATADIR: &str = @PKGDATADIR@; +pub static VERSION: &str = "@VERSION@"; +pub static LOCALEDIR: &str = @LOCALEDIR@; +pub static APPID: &str = @APPID@; diff --git a/src/connect/mod.rs b/src/connect/mod.rs new file mode 100644 index 0000000..9f3cdde --- /dev/null +++ b/src/connect/mod.rs @@ -0,0 +1,47 @@ +use std::sync::Arc; +use std::time::Duration; + +use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; +use futures::StreamExt; +use tokio::{task, time}; + +use crate::api::SpotifyApiClient; +use crate::app::AppAction; + +mod player; +pub use player::ConnectCommand; + +#[tokio::main] +async fn connect_server( + api: Arc, + action_sender: UnboundedSender, + receiver: UnboundedReceiver, +) { + let player = Arc::new(player::ConnectPlayer::new(api, action_sender)); + + let player_clone = Arc::clone(&player); + task::spawn(async move { + let mut interval = time::interval(Duration::from_secs(5)); + loop { + interval.tick().await; + if player_clone.has_device() { + player_clone.sync_state().await; + } + } + }); + + receiver + .for_each(|command| async { player.handle_command(command).await.unwrap() }) + .await; +} + +pub fn start_connect_server( + api: Arc, + action_sender: UnboundedSender, +) -> UnboundedSender { + let (sender, receiver) = unbounded(); + + std::thread::spawn(move || connect_server(api, action_sender, receiver)); + + sender +} diff --git a/src/connect/player.rs b/src/connect/player.rs new file mode 100644 index 0000000..bffc51f --- /dev/null +++ b/src/connect/player.rs @@ -0,0 +1,257 @@ +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; +use std::sync::{Arc, RwLock}; + +use futures::channel::mpsc::UnboundedSender; +use gettextrs::gettext; + +use crate::api::{SpotifyApiClient, SpotifyApiError, SpotifyResult}; +use crate::app::models::{ConnectPlayerState, RepeatMode, SongDescription}; +use crate::app::state::{Device, PlaybackAction}; +use crate::app::{AppAction, SongsSource}; + +#[derive(Debug)] +pub enum ConnectCommand { + SetDevice(String), + PlayerLoadInContext { + source: SongsSource, + offset: usize, + song: String, + }, + PlayerLoad { + songs: Vec, + offset: usize, + }, + PlayerResume, + PlayerPause, + PlayerStop, + PlayerSeek(usize), + PlayerRepeat(RepeatMode), + PlayerShuffle(bool), + PlayerSetVolume(u8), +} + +pub struct ConnectPlayer { + api: Arc, + action_sender: UnboundedSender, + device_id: RwLock>, + last_queue: RwLock, + last_state: RwLock, +} + +impl ConnectPlayer { + pub fn new( + api: Arc, + action_sender: UnboundedSender, + ) -> Self { + Self { + api: api.clone(), + action_sender, + device_id: Default::default(), + last_queue: Default::default(), + last_state: Default::default(), + } + } + + fn send_actions(&self, actions: impl IntoIterator) { + for action in actions.into_iter() { + self.action_sender.unbounded_send(action).unwrap(); + } + } + + fn device_lost(&self) { + let _ = self.device_id.write().unwrap().take(); + self.send_actions([ + AppAction::ShowNotification(gettext("Connection to device lost!")), + PlaybackAction::SwitchDevice(Device::Local).into(), + PlaybackAction::SetAvailableDevices(vec![]).into(), + ]); + } + + async fn get_queue_if_changed(&self) -> Option> { + let last_queue = *self.last_queue.read().ok().as_deref().unwrap_or(&0u64); + let songs = self.api.get_player_queue().await.ok(); + songs.filter(|songs| { + let hash = { + let mut hasher = DefaultHasher::new(); + songs.hash(&mut hasher); + hasher.finish() + }; + if let Some(last_queue) = self.last_queue.try_write().ok().as_deref_mut() { + *last_queue = hash; + } + hash != last_queue + }) + } + + async fn apply_remote_state(&self, state: &ConnectPlayerState) { + if let Some(songs) = self.get_queue_if_changed().await { + self.send_actions([PlaybackAction::LoadSongs(songs).into()]); + } + + let play_pause = if state.is_playing { + PlaybackAction::Load(state.current_song_id.clone().unwrap()) + } else { + PlaybackAction::Pause + }; + + self.send_actions([ + play_pause.into(), + PlaybackAction::SetRepeatMode(state.repeat).into(), + PlaybackAction::SetShuffled(state.shuffle).into(), + PlaybackAction::SyncSeek(state.progress_ms).into(), + ]); + } + + pub fn has_device(&self) -> bool { + self.device_id + .read() + .map(|it| it.is_some()) + .unwrap_or(false) + } + + pub async fn sync_state(&self) { + debug!("polling connect device..."); + let player_state = self.api.player_state().await; + let Ok(state) = player_state else { + self.device_lost(); + return; + }; + self.apply_remote_state(&state).await; + if let Ok(mut last_state) = self.last_state.write() { + *last_state = state; + } + } + + async fn handle_player_load_in_context( + &self, + device_id: String, + current_state: &ConnectPlayerState, + command: ConnectCommand, + ) -> SpotifyResult<()> { + let ConnectCommand::PlayerLoadInContext { + source, + offset, + song, + } = command + else { + panic!("Illegal call"); + }; + let is_diff_song = current_state + .current_song_id + .as_ref() + .map(|it| it != &song) + .unwrap_or(true); + let is_paused = !current_state.is_playing; + if is_diff_song { + let context = source.spotify_uri().unwrap(); + self.api + .player_play_in_context(device_id, context, offset) + .await + } else if is_paused { + self.api.player_resume(device_id).await + } else { + Ok(()) + } + } + + async fn handle_player_load( + &self, + device_id: String, + current_state: &ConnectPlayerState, + command: ConnectCommand, + ) -> SpotifyResult<()> { + let ConnectCommand::PlayerLoad { songs, offset } = command else { + panic!("Illegal call"); + }; + let is_diff_song = current_state + .current_song_id + .as_ref() + .map(|it| it != &songs[offset]) + .unwrap_or(true); + let is_paused = !current_state.is_playing; + if is_diff_song { + self.api + .player_play_no_context( + device_id, + songs + .into_iter() + .map(|s| format!("spotify:track:{}", s)) + .collect(), + offset, + ) + .await + } else if is_paused { + self.api.player_resume(device_id).await + } else { + Ok(()) + } + } + + async fn handle_other_command( + &self, + device_id: String, + command: ConnectCommand, + ) -> SpotifyResult<()> { + let state = self.last_state.read(); + let Ok(state) = state.as_deref() else { + return Ok(()); + }; + match command { + ConnectCommand::PlayerLoadInContext { .. } => { + self.handle_player_load_in_context(device_id, state, command) + .await + } + ConnectCommand::PlayerLoad { .. } => { + self.handle_player_load(device_id, state, command).await + } + ConnectCommand::PlayerResume if !state.is_playing => { + self.api.player_resume(device_id).await + } + ConnectCommand::PlayerPause if state.is_playing => { + self.api.player_pause(device_id).await + } + ConnectCommand::PlayerSeek(offset) => self.api.player_seek(device_id, offset).await, + ConnectCommand::PlayerRepeat(mode) => self.api.player_repeat(device_id, mode).await, + ConnectCommand::PlayerShuffle(shuffle) => { + self.api.player_shuffle(device_id, shuffle).await + } + ConnectCommand::PlayerSetVolume(volume) => { + self.api.player_volume(device_id, volume).await + } + _ => Ok(()), + } + } + + pub async fn handle_command(&self, command: ConnectCommand) -> Option<()> { + let device_lost = match command { + ConnectCommand::SetDevice(new_device_id) => { + self.device_id.write().ok()?.replace(new_device_id); + self.sync_state().await; + false + } + ConnectCommand::PlayerStop => { + let device_id = self.device_id.write().ok()?.take(); + if let Some(old_id) = device_id { + let _ = self.api.player_pause(old_id).await; + } + false + } + _ => { + let device_id = self.device_id.read().ok()?.clone(); + if let Some(device_id) = device_id { + let result = self.handle_other_command(device_id, command).await; + matches!(result, Err(SpotifyApiError::BadStatus(404, _))) + } else { + true + } + } + }; + + if device_lost { + self.device_lost(); + } + + Some(()) + } +} diff --git a/src/dbus/listener.rs b/src/dbus/listener.rs new file mode 100644 index 0000000..d9c12ec --- /dev/null +++ b/src/dbus/listener.rs @@ -0,0 +1,131 @@ +use futures::channel::mpsc::UnboundedSender; +use std::rc::Rc; + +use crate::app::{ + components::EventListener, + models::{RepeatMode, SongDescription}, + state::PlaybackEvent, + AppEvent, AppModel, +}; + +use super::types::{LoopStatus, PlaybackStatus, TrackMetadata}; + +#[derive(Debug)] +#[allow(clippy::enum_variant_names)] +pub enum MprisStateUpdate { + SetVolume(f64), + SetCurrentTrack { + has_prev: bool, + current: Option, + has_next: bool, + }, + SetPositionMs(u128), + SetLoopStatus { + has_prev: bool, + loop_status: LoopStatus, + has_next: bool, + }, + SetShuffled(bool), + SetPlaying(PlaybackStatus), +} + +pub struct AppPlaybackStateListener { + app_model: Rc, + sender: UnboundedSender, +} + +impl AppPlaybackStateListener { + pub fn new(app_model: Rc, sender: UnboundedSender) -> Self { + Self { app_model, sender } + } + + fn make_track_meta(&self) -> Option { + let SongDescription { + id, + title, + artists, + album, + duration, + art, + .. + } = self.app_model.get_state().playback.current_song()?; + Some(TrackMetadata { + id: format!("/dev/alextren/Spot/Track/{id}"), + length: 1000 * duration as u64, + title, + album: album.name, + artist: artists.into_iter().map(|a| a.name).collect(), + art, + }) + } + + fn has_prev_next(&self) -> (bool, bool) { + let state = self.app_model.get_state(); + ( + state.playback.prev_index().is_some(), + state.playback.next_index().is_some(), + ) + } + + fn loop_status(&self) -> LoopStatus { + let state = self.app_model.get_state(); + match state.playback.repeat_mode() { + RepeatMode::None => LoopStatus::None, + RepeatMode::Song => LoopStatus::Track, + RepeatMode::Playlist => LoopStatus::Playlist, + } + } + + fn update_for(&self, event: &PlaybackEvent) -> Option { + match event { + PlaybackEvent::PlaybackPaused => { + Some(MprisStateUpdate::SetPlaying(PlaybackStatus::Paused)) + } + PlaybackEvent::PlaybackResumed => { + Some(MprisStateUpdate::SetPlaying(PlaybackStatus::Playing)) + } + PlaybackEvent::PlaybackStopped => { + Some(MprisStateUpdate::SetPlaying(PlaybackStatus::Stopped)) + } + PlaybackEvent::TrackChanged(_) => { + let current = self.make_track_meta(); + let (has_prev, has_next) = self.has_prev_next(); + Some(MprisStateUpdate::SetCurrentTrack { + has_prev, + has_next, + current, + }) + } + PlaybackEvent::RepeatModeChanged(_) => { + let loop_status = self.loop_status(); + let (has_prev, has_next) = self.has_prev_next(); + Some(MprisStateUpdate::SetLoopStatus { + has_prev, + has_next, + loop_status, + }) + } + PlaybackEvent::ShuffleChanged(shuffled) => { + Some(MprisStateUpdate::SetShuffled(*shuffled)) + } + PlaybackEvent::TrackSeeked(pos) | PlaybackEvent::SeekSynced(pos) => { + let pos = 1000 * (*pos as u128); + Some(MprisStateUpdate::SetPositionMs(pos)) + } + PlaybackEvent::VolumeSet(vol) => Some(MprisStateUpdate::SetVolume(*vol)), + _ => None, + } + } +} + +impl EventListener for AppPlaybackStateListener { + fn on_event(&mut self, event: &AppEvent) { + if let AppEvent::PlaybackEvent(event) = event { + if let Some(update) = self.update_for(event) { + self.sender + .unbounded_send(update) + .expect("Could not send event to DBUS server"); + } + } + } +} diff --git a/src/dbus/mod.rs b/src/dbus/mod.rs new file mode 100644 index 0000000..51752fa --- /dev/null +++ b/src/dbus/mod.rs @@ -0,0 +1,103 @@ +use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; +use futures::StreamExt; +use std::rc::Rc; +use std::thread; +use zbus::Connection; + +use crate::app::{AppAction, AppModel}; + +mod mpris; +pub use mpris::*; + +mod types; + +mod listener; +use listener::*; + +#[tokio::main] +async fn dbus_server( + mpris: SpotMpris, + player: SpotMprisPlayer, + receiver: UnboundedReceiver, +) -> zbus::Result<()> { + let connection = Connection::session().await?; + connection + .object_server() + .at("/org/mpris/MediaPlayer2", mpris) + .await?; + connection + .object_server() + .at("/org/mpris/MediaPlayer2", player) + .await?; + connection + .request_name("org.mpris.MediaPlayer2.Spot") + .await?; + + receiver + .for_each(|update| async { + if let Ok(player_ref) = connection + .object_server() + .interface::<_, SpotMprisPlayer>("/org/mpris/MediaPlayer2") + .await + { + let mut player = player_ref.get_mut().await; + let ctxt = player_ref.signal_context(); + let res: zbus::Result<()> = match update { + MprisStateUpdate::SetVolume(volume) => { + player.state_mut().set_volume(volume); + player.volume_changed(ctxt).await + } + MprisStateUpdate::SetCurrentTrack { + has_prev, + has_next, + current, + } => { + player.state_mut().set_has_prev(has_prev); + player.state_mut().set_has_next(has_next); + player.state_mut().set_current_track(current); + player.notify_current_track_changed(ctxt).await + } + MprisStateUpdate::SetPositionMs(position) => { + player.state_mut().set_position(position); + Ok(()) + } + MprisStateUpdate::SetLoopStatus { + has_prev, + has_next, + loop_status, + } => { + player.state_mut().set_has_prev(has_prev); + player.state_mut().set_has_next(has_next); + player.state_mut().set_loop_status(loop_status); + player.notify_loop_status(ctxt).await + } + MprisStateUpdate::SetShuffled(shuffled) => { + player.state_mut().set_shuffled(shuffled); + player.shuffle_changed(ctxt).await + } + MprisStateUpdate::SetPlaying(status) => { + player.state_mut().set_playing(status); + player.playback_status_changed(ctxt).await + } + }; + res.expect("Signal emission failed"); + } + }) + .await; + + Ok(()) +} + +pub fn start_dbus_server( + app_model: Rc, + sender: UnboundedSender, +) -> AppPlaybackStateListener { + let mpris = SpotMpris::new(sender.clone()); + let player = SpotMprisPlayer::new(sender); + + let (sender, receiver) = unbounded(); + + thread::spawn(move || dbus_server(mpris, player, receiver)); + + AppPlaybackStateListener::new(app_model, sender) +} diff --git a/src/dbus/mpris.rs b/src/dbus/mpris.rs new file mode 100644 index 0000000..caaa998 --- /dev/null +++ b/src/dbus/mpris.rs @@ -0,0 +1,348 @@ +#![allow(non_snake_case)] +#![allow(unused_variables)] + +use std::collections::HashMap; +use std::convert::TryInto; + +use futures::channel::mpsc::UnboundedSender; +use zbus::fdo::{Error, Result}; +use zbus::{dbus_interface, Interface, SignalContext}; +use zvariant::{ObjectPath, Value}; + +use super::types::*; +use crate::app::models::RepeatMode; +use crate::app::state::PlaybackAction; +use crate::app::AppAction; + +#[derive(Clone)] +pub struct SpotMpris { + sender: UnboundedSender, +} + +impl SpotMpris { + pub fn new(sender: UnboundedSender) -> Self { + Self { sender } + } +} + +#[dbus_interface(interface = "org.mpris.MediaPlayer2")] +impl SpotMpris { + fn quit(&self) -> Result<()> { + Err(Error::NotSupported("Not implemented".to_string())) + } + + fn raise(&self) -> Result<()> { + self.sender + .unbounded_send(AppAction::Raise) + .map_err(|_| Error::Failed("Could not send action".to_string())) + } + + #[dbus_interface(property)] + fn can_quit(&self) -> bool { + false + } + + #[dbus_interface(property)] + fn can_raise(&self) -> bool { + true + } + + #[dbus_interface(property)] + fn has_track_list(&self) -> bool { + false + } + + #[dbus_interface(property)] + fn identity(&self) -> &'static str { + "Spot" + } + + #[dbus_interface(property)] + fn supported_mime_types(&self) -> Vec { + vec![] + } + + #[dbus_interface(property)] + fn supported_uri_schemes(&self) -> Vec { + vec![] + } + + #[dbus_interface(property)] + fn desktop_entry(&self) -> &'static str { + "dev.alextren.Spot" + } +} + +pub struct SpotMprisPlayer { + state: MprisState, + sender: UnboundedSender, +} + +impl SpotMprisPlayer { + pub fn new(sender: UnboundedSender) -> Self { + Self { + state: MprisState::new(), + sender, + } + } + + pub fn state_mut(&mut self) -> &mut MprisState { + &mut self.state + } + + pub async fn notify_current_track_changed(&self, ctxt: &SignalContext<'_>) -> zbus::Result<()> { + let metadata = Value::from(self.metadata()); + let can_go_next = Value::from(self.can_go_next()); + let can_go_previous = Value::from(self.can_go_previous()); + + zbus::fdo::Properties::properties_changed( + ctxt, + Self::name(), + &HashMap::from([ + ("Metadata", &metadata), + ("CanGoNext", &can_go_next), + ("CanGoPrevious", &can_go_previous), + ]), + &[], + ) + .await + } + + pub async fn notify_loop_status(&self, ctxt: &SignalContext<'_>) -> zbus::Result<()> { + let loop_status = Value::from(self.loop_status()); + let can_go_next = Value::from(self.can_go_next()); + let can_go_previous = Value::from(self.can_go_previous()); + + zbus::fdo::Properties::properties_changed( + ctxt, + Self::name(), + &HashMap::from([ + ("LoopStatus", &loop_status), + ("CanGoNext", &can_go_next), + ("CanGoPrevious", &can_go_previous), + ]), + &[], + ) + .await + } +} + +#[dbus_interface(interface = "org.mpris.MediaPlayer2.Player")] +impl SpotMprisPlayer { + pub fn next(&self) -> Result<()> { + self.sender + .unbounded_send(PlaybackAction::Next.into()) + .map_err(|_| Error::Failed("Could not send action".to_string())) + } + + pub fn open_uri(&self, Uri: &str) -> Result<()> { + Err(Error::NotSupported("Not implemented".to_string())) + } + + pub fn pause(&self) -> Result<()> { + self.sender + .unbounded_send(PlaybackAction::Pause.into()) + .map_err(|_| Error::Failed("Could not send action".to_string())) + } + + pub fn play(&self) -> Result<()> { + self.sender + .unbounded_send(PlaybackAction::Play.into()) + .map_err(|_| Error::Failed("Could not send action".to_string())) + } + + pub fn play_pause(&self) -> Result<()> { + self.sender + .unbounded_send(PlaybackAction::TogglePlay.into()) + .map_err(|_| Error::Failed("Could not send action".to_string())) + } + + pub fn previous(&self) -> Result<()> { + self.sender + .unbounded_send(PlaybackAction::Previous.into()) + .map_err(|_| Error::Failed("Could not send action".to_string())) + } + + pub fn seek(&self, Offset: i64) -> Result<()> { + if self.state.current_track().is_none() { + return Ok(()); + } + + let mut new_pos: i128 = (self.state.position() as i128 + i128::from(Offset)) / 1000; + // As per spec, if new position is less than 0 round to 0 + if new_pos < 0 { + new_pos = 0; + } + + let new_pos: u32 = (new_pos) + .try_into() + .map_err(|_| Error::Failed("Could not parse position".to_string()))?; + + // As per spec, if new position is past the length of the song skip to + // the next song + if u64::from(new_pos) >= self.metadata().length / 1000 { + self.sender + .unbounded_send(PlaybackAction::Next.into()) + .map_err(|_| Error::Failed("Could not send action".to_string())) + } else { + self.sender + .unbounded_send(PlaybackAction::Seek(new_pos).into()) + .map_err(|_| Error::Failed("Could not send action".to_string())) + } + } + + pub fn set_position(&self, TrackId: ObjectPath, Position: i64) -> Result<()> { + if self.state.current_track().is_none() { + return Ok(()); + } + + if TrackId.to_string() != self.metadata().id { + return Ok(()); + } + + let length: i64 = self + .metadata() + .length + .try_into() + .map_err(|_| Error::Failed("Could not cast length (too large)".to_string()))?; + + if Position > length { + return Ok(()); + } + + let pos: u32 = (Position / 1000) + .try_into() + .map_err(|_| Error::Failed("Could not parse position".to_string()))?; + + self.sender + .unbounded_send(PlaybackAction::Seek(pos).into()) + .map_err(|_| Error::Failed("Could not send action".to_string())) + } + + pub fn stop(&self) -> Result<()> { + Err(Error::NotSupported("Not implemented".to_string())) + } + + #[dbus_interface(signal)] + pub async fn seeked(ctxt: &SignalContext<'_>, Position: i64) -> zbus::Result<()>; + + #[dbus_interface(property)] + pub fn can_control(&self) -> bool { + true + } + + #[dbus_interface(property)] + pub fn can_go_next(&self) -> bool { + self.state.has_next() + } + + #[dbus_interface(property)] + pub fn can_go_previous(&self) -> bool { + self.state.has_prev() + } + + #[dbus_interface(property)] + pub fn can_pause(&self) -> bool { + true + } + + #[dbus_interface(property)] + pub fn can_play(&self) -> bool { + true + } + + #[dbus_interface(property)] + pub fn can_seek(&self) -> bool { + self.state.current_track().is_some() + } + + #[dbus_interface(property)] + pub fn maximum_rate(&self) -> f64 { + 1.0f64 + } + + #[dbus_interface(property)] + pub fn metadata(&self) -> TrackMetadata { + self.state + .current_track() + .cloned() + .unwrap_or_else(|| TrackMetadata { + id: String::new(), + length: 0, + title: "Not playing".to_string(), + artist: vec![], + album: String::new(), + art: None, + }) + } + + #[dbus_interface(property)] + pub fn minimum_rate(&self) -> f64 { + 1.0f64 + } + + #[dbus_interface(property)] + pub fn playback_status(&self) -> PlaybackStatus { + self.state.status() + } + + #[dbus_interface(property)] + pub fn loop_status(&self) -> LoopStatus { + self.state.loop_status() + } + + #[dbus_interface(property)] + pub fn set_loop_status(&self, value: LoopStatus) -> zbus::Result<()> { + let mode = match value { + LoopStatus::None => RepeatMode::None, + LoopStatus::Track => RepeatMode::Song, + LoopStatus::Playlist => RepeatMode::Playlist, + }; + self.sender + .unbounded_send(PlaybackAction::SetRepeatMode(mode).into()) + .map_err(|_| Error::Failed("Could not send action".to_string()))?; + Ok(()) + } + + #[dbus_interface(property)] + pub fn position(&self) -> i64 { + self.state.position() as i64 + } + + #[dbus_interface(property)] + pub fn rate(&self) -> f64 { + 1.0f64 + } + + #[dbus_interface(property)] + pub fn set_rate(&self, value: f64) {} + + #[dbus_interface(property)] + pub fn shuffle(&self) -> bool { + self.state.is_shuffled() + } + + #[dbus_interface(property)] + pub fn set_shuffle(&self, value: bool) -> zbus::Result<()> { + self.sender + .unbounded_send(PlaybackAction::ToggleShuffle.into()) + .map_err(|_| Error::Failed("Could not send action".to_string()))?; + Ok(()) + } + + #[dbus_interface(property)] + pub fn volume(&self) -> f64 { + self.state.volume() + } + + #[dbus_interface(property)] + pub fn set_volume(&self, value: f64) -> zbus::Result<()> { + // As per spec, if new volume less than 0 round to 0 + // also, we don't support volume higher than 100% at the moment. + let volume = value.clamp(0.0, 1.0); + self.sender + .unbounded_send(PlaybackAction::SetVolume(value).into()) + .map_err(|_| Error::Failed("Could not send action".to_string()))?; + Ok(()) + } +} diff --git a/src/dbus/types.rs b/src/dbus/types.rs new file mode 100644 index 0000000..5594081 --- /dev/null +++ b/src/dbus/types.rs @@ -0,0 +1,247 @@ +use std::convert::{Into, TryFrom}; +use std::time::Instant; +use zvariant::Type; +use zvariant::{Dict, Signature, Str, Value}; + +fn boxed_value<'a, V: Into>>(v: V) -> Value<'a> { + Value::new(v.into()) +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LoopStatus { + None, + Track, + Playlist, +} + +impl Type for LoopStatus { + fn signature() -> Signature<'static> { + Str::signature() + } +} + +impl From> for LoopStatus { + fn from(value: Value<'_>) -> Self { + String::try_from(value) + .map(|s| match s.as_str() { + "Track" => LoopStatus::Track, + "Playlist" => LoopStatus::Playlist, + _ => LoopStatus::None, + }) + .unwrap_or(LoopStatus::None) + } +} + +impl From for Value<'_> { + fn from(status: LoopStatus) -> Self { + match status { + LoopStatus::None => "None".into(), + LoopStatus::Track => "Track".into(), + LoopStatus::Playlist => "Playlist".into(), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PlaybackStatus { + Playing, + Paused, + Stopped, +} + +impl Type for PlaybackStatus { + fn signature() -> Signature<'static> { + Str::signature() + } +} + +impl From for Value<'_> { + fn from(status: PlaybackStatus) -> Self { + match status { + PlaybackStatus::Playing => "Playing".into(), + PlaybackStatus::Paused => "Paused".into(), + PlaybackStatus::Stopped => "Stopped".into(), + } + } +} + +struct PositionMicros { + last_known_position: u128, + last_resume_instant: Option, + rate: f32, +} + +impl PositionMicros { + fn new(rate: f32) -> Self { + Self { + last_known_position: 0, + last_resume_instant: None, + rate, + } + } + + fn current(&self) -> u128 { + let current_progress = self.last_resume_instant.map(|ri| { + let elapsed = ri.elapsed().as_micros() as f32; + let real_elapsed = self.rate * elapsed; + real_elapsed.ceil() as u128 + }); + self.last_known_position + current_progress.unwrap_or(0) + } + + fn set(&mut self, position: u128, playing: bool) { + self.last_known_position = position; + self.last_resume_instant = if playing { Some(Instant::now()) } else { None } + } + + fn pause(&mut self) { + self.last_known_position = self.current(); + self.last_resume_instant = None; + } + + fn resume(&mut self) { + self.last_resume_instant = Some(Instant::now()); + } +} + +#[derive(Debug, Clone)] +pub struct TrackMetadata { + pub id: String, + pub length: u64, + pub artist: Vec, + pub album: String, + pub title: String, + pub art: Option, +} + +impl Type for TrackMetadata { + fn signature() -> Signature<'static> { + Signature::from_str_unchecked("a{sv}") + } +} + +impl From for Value<'_> { + fn from(meta: TrackMetadata) -> Self { + let mut d = Dict::new(Str::signature(), Value::signature()); + d.append("mpris:trackid".into(), boxed_value(meta.id)) + .unwrap(); + d.append("mpris:length".into(), boxed_value(meta.length)) + .unwrap(); + d.append("xesam:title".into(), boxed_value(meta.title)) + .unwrap(); + d.append("xesam:artist".into(), boxed_value(meta.artist.clone())) + .unwrap(); + d.append("xesam:albumArtist".into(), boxed_value(meta.artist)) + .unwrap(); + d.append("xesam:album".into(), boxed_value(meta.album)) + .unwrap(); + if let Some(art) = meta.art { + d.append("mpris:artUrl".into(), boxed_value(art)).unwrap(); + } + Value::Dict(d) + } +} + +pub struct MprisState { + status: PlaybackStatus, + loop_status: LoopStatus, + volume: f64, + shuffled: bool, + position: PositionMicros, + metadata: Option, + has_prev: bool, + has_next: bool, +} + +impl MprisState { + pub fn new() -> Self { + Self { + status: PlaybackStatus::Stopped, + loop_status: LoopStatus::None, + shuffled: false, + position: PositionMicros::new(1.0), + metadata: None, + has_prev: false, + has_next: false, + volume: 1f64, + } + } + + pub fn volume(&self) -> f64 { + self.volume + } + + pub fn set_volume(&mut self, volume: f64) { + self.volume = volume; + } + + pub fn status(&self) -> PlaybackStatus { + self.status + } + + pub fn loop_status(&self) -> LoopStatus { + self.loop_status + } + + pub fn is_shuffled(&self) -> bool { + self.shuffled + } + + pub fn current_track(&self) -> Option<&TrackMetadata> { + self.metadata.as_ref() + } + + pub fn has_prev(&self) -> bool { + self.has_prev + } + + pub fn has_next(&self) -> bool { + self.has_next + } + + pub fn set_has_prev(&mut self, has_prev: bool) { + self.has_prev = has_prev; + } + + pub fn set_has_next(&mut self, has_next: bool) { + self.has_next = has_next; + } + + pub fn set_current_track(&mut self, track: Option) { + let playing = self.status == PlaybackStatus::Playing; + self.metadata = track; + self.position.set(0, playing); + } + + pub fn position(&self) -> u128 { + self.position.current() + } + + pub fn set_position(&mut self, position: u128) { + let playing = self.status == PlaybackStatus::Playing; + self.position.set(position, playing); + } + + pub fn set_loop_status(&mut self, loop_status: LoopStatus) { + self.loop_status = loop_status; + } + + pub fn set_shuffled(&mut self, shuffled: bool) { + self.shuffled = shuffled; + } + + pub fn set_playing(&mut self, status: PlaybackStatus) { + self.status = status; + match status { + PlaybackStatus::Playing => { + self.position.resume(); + } + PlaybackStatus::Paused => { + self.position.pause(); + } + PlaybackStatus::Stopped => { + self.position.set(0, false); + } + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..381410f --- /dev/null +++ b/src/main.rs @@ -0,0 +1,207 @@ +#[macro_use(clone)] +extern crate glib; +#[macro_use] +extern crate lazy_static; +#[macro_use] +extern crate log; + +use app::state::ScreenName; +use futures::channel::mpsc::UnboundedSender; +use gettextrs::*; +use gio::prelude::*; +use gio::ApplicationFlags; +use gio::SimpleAction; +use gtk::prelude::*; + +mod api; +mod app; +mod config; +mod connect; +mod dbus; +mod player; +mod settings; + +use crate::app::components::expose_custom_widgets; +use crate::app::dispatch::{spawn_task_handler, DispatchLoop}; +use crate::app::{state::PlaybackAction, App, AppAction, BrowserAction}; + +fn main() { + let settings = settings::SpotSettings::new_from_gsettings().unwrap_or_default(); + setup_gtk(&settings); + + // Looks like there's a side effect to declaring widgets that allows them to be referenced them in ui/blueprint files + // so here goes! + expose_custom_widgets(); + + let gtk_app = gtk::Application::new(Some(config::APPID), ApplicationFlags::HANDLES_OPEN); + let builder = gtk::Builder::from_resource("/dev/alextren/Spot/window.ui"); + let window: libadwaita::ApplicationWindow = builder.object("window").unwrap(); + + // In debug mode, the app id is different (see meson config) so we fix the resource path (and add a distinctive style) + // Having a different app id allows running both the stable and development version at the same time + if cfg!(debug_assertions) { + window.add_css_class("devel"); + gtk_app.set_resource_base_path(Some("/dev/alextren/Spot")); + } + + let context = glib::MainContext::default(); + let dispatch_loop = DispatchLoop::new(); + let sender = dispatch_loop.make_dispatcher(); + + // Couple of actions used with shortcuts + register_actions(>k_app, sender.clone()); + setup_credits(builder.object::("about").unwrap()); + + // Main app logic is hooked up here + let app = App::new( + settings, + builder, + sender.clone(), + spawn_task_handler(&context), + ); + context.spawn_local(app.attach(dispatch_loop)); + + let sender_clone = sender.clone(); + gtk_app.connect_activate(move |gtk_app| { + debug!("activate"); + if let Some(existing_window) = gtk_app.active_window() { + existing_window.present(); + } else { + // Only send the Start action if we've just created the window + window.set_application(Some(gtk_app)); + gtk_app.add_window(&window); + sender_clone.unbounded_send(AppAction::Start).unwrap(); + } + }); + + gtk_app.connect_open(move |gtk_app, targets, _| { + gtk_app.activate(); + + // There should only be one target because %u is used in desktop file + let target = &targets[0]; + let uri = target.uri().to_string(); + let action = AppAction::OpenURI(uri) + .unwrap_or_else(|| AppAction::ShowNotification(gettext("Failed to open link!"))); + sender.unbounded_send(action).unwrap(); + }); + + context.invoke_local(move || { + gtk_app.run(); + }); + + std::process::exit(0); +} + +fn setup_gtk(settings: &settings::SpotSettings) { + // Setup logging + env_logger::init(); + + // Setup translations + textdomain("spot") + .and_then(|_| bindtextdomain("spot", config::LOCALEDIR)) + .and_then(|_| bind_textdomain_codeset("spot", "UTF-8")) + .expect("Could not setup localization"); + + // Setup Gtk, Adwaita... + gtk::init().unwrap_or_else(|_| panic!("Failed to initialize GTK")); + libadwaita::init().unwrap_or_else(|_| panic!("Failed to initialize libadwaita")); + + let manager = libadwaita::StyleManager::default(); + manager.set_color_scheme(settings.theme_preference); + + let res = gio::Resource::load(config::PKGDATADIR.to_owned() + "/spot.gresource") + .expect("Could not load resources"); + gio::resources_register(&res); + + let provider = gtk::CssProvider::new(); + provider.load_from_resource("/dev/alextren/Spot/app.css"); + + gtk::style_context_add_provider_for_display( + &gdk::Display::default().unwrap(), + &provider, + gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); +} + +fn setup_credits(about: libadwaita::AboutWindow) { + // Read from a couple files at compile time and update the about dialog + let authors: Vec<&str> = include_str!("../AUTHORS") + .trim_end_matches('\n') + .split('\n') + .collect(); + let translators = include_str!("../TRANSLATORS").trim_end_matches('\n'); + let artists: Vec<&str> = include_str!("../ARTISTS") + .trim_end_matches('\n') + .split('\n') + .collect(); + about.set_version(config::VERSION); + about.set_developers(&authors); + about.set_translator_credits(translators); + about.set_artists(&artists); +} + +fn register_actions(app: >k::Application, sender: UnboundedSender) { + let quit = SimpleAction::new("quit", None); + quit.connect_activate(clone!(@weak app => move |_, _| { + if let Some(existing_window) = app.active_window() { + existing_window.close(); + } + app.quit(); + })); + app.add_action(&quit); + + app.add_action(&make_action( + "toggle_playback", + PlaybackAction::TogglePlay.into(), + sender.clone(), + )); + + app.add_action(&make_action( + "player_prev", + PlaybackAction::Previous.into(), + sender.clone(), + )); + + app.add_action(&make_action( + "player_next", + PlaybackAction::Next.into(), + sender.clone(), + )); + + app.add_action(&make_action( + "nav_pop", + AppAction::BrowserAction(BrowserAction::NavigationPop), + sender.clone(), + )); + + app.add_action(&make_action( + "search", + AppAction::BrowserAction(BrowserAction::NavigationPush(ScreenName::Search)), + sender.clone(), + )); + + app.add_action(&{ + let action = SimpleAction::new("open_playlist", Some(glib::VariantTy::STRING)); + action.set_enabled(true); + action.connect_activate(move |_, playlist_id| { + if let Some(id) = playlist_id.and_then(|s| s.str()) { + sender + .unbounded_send(AppAction::ViewPlaylist(id.to_owned())) + .unwrap(); + } + }); + action + }); +} + +fn make_action( + name: &str, + app_action: AppAction, + sender: UnboundedSender, +) -> SimpleAction { + let action = SimpleAction::new(name, None); + action.connect_activate(move |_, _| { + sender.unbounded_send(app_action.clone()).unwrap(); + }); + action +} diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 0000000..900f3fa --- /dev/null +++ b/src/meson.build @@ -0,0 +1,167 @@ +gnome = import('gnome') +gnome.post_install( + glib_compile_schemas: true, + gtk_update_icon_cache: true, + update_desktop_database: true, +) + +dependency('libadwaita-1') +dependency('gtk4') +dependency('glib-2.0') +dependency('openssl') +dependency('alsa') +dependency('libpulse') + +conf = configuration_data() + +conf.set_quoted('LOCALEDIR', get_option('prefix') / get_option('localedir')) + +pkgdatadir = get_option('prefix') / get_option('datadir') / meson.project_name() +conf.set_quoted('PKGDATADIR', pkgdatadir) + +if get_option('buildtype') == 'debug' + conf.set_quoted('APPID', 'dev.alextren.Spot.Devel') + conf.set('VERSION', meson.project_version() + '-dev') +else + conf.set_quoted('APPID', 'dev.alextren.Spot') + conf.set('VERSION', meson.project_version()) +endif + +blueprints = custom_target('blueprints', + build_always_stale: true, + input: files( + 'app/components/album/album.blp', + 'app/components/artist/artist.blp', + 'app/components/artist_details/artist_details.blp', + 'app/components/details/album_header.blp', + 'app/components/details/details.blp', + 'app/components/details/release_details.blp', + 'app/components/device_selector/device_selector.blp', + 'app/components/headerbar/headerbar.blp', + 'app/components/library/library.blp', + 'app/components/login/login.blp', + 'app/components/now_playing/now_playing.blp', + 'app/components/playback/playback_controls.blp', + 'app/components/playback/playback_info.blp', + 'app/components/playback/playback_widget.blp', + 'app/components/playlist/song.blp', + 'app/components/playlist_details/playlist_details.blp', + 'app/components/playlist_details/playlist_header.blp', + 'app/components/playlist_details/playlist_headerbar.blp', + 'app/components/saved_playlists/saved_playlists.blp', + 'app/components/saved_tracks/saved_tracks.blp', + 'app/components/search/search.blp', + 'app/components/selection/selection_toolbar.blp', + 'app/components/settings/settings.blp', + 'app/components/user_details/user_details.blp', + 'app/components/scrolling_header/scrolling_header.blp', + 'app/components/sidebar/create_playlist.blp', + 'app/components/sidebar/sidebar_row.blp', + 'window.blp', + ), + output: '.', + command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'], +) + +gnome.compile_resources('spot', + 'spot.gresource.xml', + gresource_bundle: true, + install: true, + install_dir: pkgdatadir, + dependencies: blueprints +) + +configure_file( + input: 'config.rs.in', + output: 'config.rs', + configuration: conf +) + +# Copy the config.rs output to the source directory. +run_command( + 'cp', + meson.current_build_dir() / 'config.rs', + meson.current_source_dir() / 'config.rs', + check: true +) + +cargo = find_program('cargo', required: true) + +if get_option('buildtype') == 'release' + rust_target = 'release' + cargo_profile_option = '--release' +else + rust_target = 'debug' + cargo_profile_option = '--verbose' +endif + +cargo_manifest = meson.project_source_root() / 'Cargo.toml' +cargo_output = 'src' / rust_target / meson.project_name() + +env = environment() +env.set('CARGO_HOME', meson.project_source_root() / 'cargo') + +cargo_options = [ + '--manifest-path', cargo_manifest, + '--features', get_option('features'), + '--target-dir', meson.project_build_root() / 'src', + cargo_profile_option +] + +if get_option('offline') + cargo_options += ['--offline'] +endif + +cargo_release = custom_target( + 'cargo-build', + build_always_stale: true, + output: 'bin', + console: true, + install: false, + command: [ + cargo, 'build', cargo_options, + ], + env: env +) + +final_bin = custom_target( + 'copy-cargo-build', + build_by_default: true, + build_always_stale: true, + input: cargo_release, + output: meson.project_name(), + console: true, + install: true, + install_dir: get_option('bindir'), + command: [ + 'cp', meson.project_build_root() / cargo_output, '@OUTPUT@' + ] +) + +test('Unit tests', + cargo, + args: [ + 'test', + '--manifest-path', cargo_manifest, + '--target-dir', meson.project_build_root() / 'src', + cargo_profile_option + ], + timeout: 180, + env: env +) + +test('Clippy', + cargo, + args: [ + 'clippy', + '--manifest-path', cargo_manifest, + '--target-dir', meson.project_build_root() / 'src', + '--', + '-D', 'warnings', + '-A', 'clippy::module_inception', + '-A', 'clippy::new_without_default', + '-A', 'clippy::enum-variant-names' + ], + timeout: 180, + env: env +) diff --git a/src/player/mod.rs b/src/player/mod.rs new file mode 100644 index 0000000..d5bb17a --- /dev/null +++ b/src/player/mod.rs @@ -0,0 +1,133 @@ +use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; +use librespot::core::spotify_id::SpotifyId; +use std::cell::RefCell; +use std::rc::Rc; +use std::time::SystemTime; +use tokio::task; + +use crate::app::credentials::Credentials; +use crate::app::state::{LoginAction, PlaybackAction, SetLoginSuccessAction}; +use crate::app::AppAction; +#[allow(clippy::module_inception)] +mod player; +pub use player::*; + +#[derive(Debug, Clone)] +pub enum Command { + PasswordLogin { username: String, password: String }, + TokenLogin { username: String, token: String }, + OAuthLogin, + Logout, + PlayerLoad { track: SpotifyId, resume: bool }, + PlayerResume, + PlayerPause, + PlayerStop, + PlayerSeek(u32), + PlayerSetVolume(f64), + PlayerPreload(SpotifyId), + RefreshToken, + ReloadSettings, +} + +struct AppPlayerDelegate { + sender: RefCell>, +} + +impl AppPlayerDelegate { + fn new(sender: UnboundedSender) -> Self { + let sender = RefCell::new(sender); + Self { sender } + } +} + +impl SpotifyPlayerDelegate for AppPlayerDelegate { + fn end_of_track_reached(&self) { + self.sender + .borrow_mut() + .unbounded_send(PlaybackAction::Next.into()) + .unwrap(); + } + + fn password_login_successful(&self, credentials: Credentials) { + self.sender + .borrow_mut() + .unbounded_send( + LoginAction::SetLoginSuccess(SetLoginSuccessAction::Password(credentials)).into(), + ) + .unwrap(); + } + + fn token_login_successful(&self, credentials: Credentials) { + self.sender + .borrow_mut() + .unbounded_send( + LoginAction::SetLoginSuccess(SetLoginSuccessAction::Token(credentials)).into(), + ) + .unwrap(); + } + + fn refresh_successful(&self, token: String, token_expiry_time: SystemTime) { + self.sender + .borrow_mut() + .unbounded_send( + LoginAction::SetRefreshedToken { + token, + token_expiry_time, + } + .into(), + ) + .unwrap(); + } + + fn report_error(&self, error: SpotifyError) { + self.sender + .borrow_mut() + .unbounded_send(match error { + SpotifyError::LoginFailed => LoginAction::SetLoginFailure.into(), + _ => AppAction::ShowNotification(format!("{error}")), + }) + .unwrap(); + } + + fn notify_playback_state(&self, position: u32) { + self.sender + .borrow_mut() + .unbounded_send(PlaybackAction::SyncSeek(position).into()) + .unwrap(); + } + + fn preload_next_track(&self) { + self.sender + .borrow_mut() + .unbounded_send(PlaybackAction::Preload.into()) + .unwrap(); + } +} + +#[tokio::main] +async fn player_main( + player_settings: SpotifyPlayerSettings, + appaction_sender: UnboundedSender, + receiver: UnboundedReceiver, +) { + task::LocalSet::new() + .run_until(async move { + task::spawn_local(async move { + let delegate = Rc::new(AppPlayerDelegate::new(appaction_sender.clone())); + let player = SpotifyPlayer::new(player_settings, delegate); + player.start(receiver).await.unwrap(); + }) + .await + .unwrap(); + }) + .await; +} + +pub fn start_player_service( + player_settings: SpotifyPlayerSettings, + appaction_sender: UnboundedSender, +) -> UnboundedSender { + let (sender, receiver) = unbounded::(); + std::thread::spawn(move || player_main(player_settings, appaction_sender, receiver)); + sender +} diff --git a/src/player/player.rs b/src/player/player.rs new file mode 100644 index 0000000..31d2986 --- /dev/null +++ b/src/player/player.rs @@ -0,0 +1,444 @@ +use futures::channel::mpsc::UnboundedReceiver; +use futures::stream::StreamExt; + +use librespot::core::authentication::Credentials; +use librespot::core::cache::Cache; +use librespot::core::config::SessionConfig; +use librespot::core::session::Session; + +use librespot::playback::mixer::softmixer::SoftMixer; +use librespot::playback::mixer::{Mixer, MixerConfig}; + +use librespot::playback::audio_backend; +use librespot::playback::config::{AudioFormat, Bitrate, PlayerConfig, VolumeCtrl}; +use librespot::playback::player::{Player, PlayerEvent, PlayerEventChannel}; + +use super::Command; +use crate::api::oauth2::get_access_token; +use crate::app::credentials; +use crate::settings::SpotSettings; +use std::cell::RefCell; +use std::env; +use std::error::Error; +use std::fmt; +use std::rc::Rc; +use std::sync::Arc; +use std::time::{Instant, SystemTime}; + +#[derive(Debug)] +pub enum SpotifyError { + LoginFailed, + TokenFailed, + PlayerNotReady, + TechnicalError, +} + +impl Error for SpotifyError {} + +impl fmt::Display for SpotifyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::LoginFailed => write!(f, "Login failed!"), + Self::TokenFailed => write!(f, "Token retrieval failed!"), + Self::PlayerNotReady => write!(f, "Player is not responding."), + Self::TechnicalError => { + write!(f, "A technical error occured. Check your connectivity.") + } + } + } +} + +pub trait SpotifyPlayerDelegate { + fn end_of_track_reached(&self); + fn password_login_successful(&self, credentials: credentials::Credentials); + fn token_login_successful(&self, credentials: credentials::Credentials); + fn refresh_successful(&self, token: String, token_expiry_time: SystemTime); + fn report_error(&self, error: SpotifyError); + fn notify_playback_state(&self, position: u32); + fn preload_next_track(&self); +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AudioBackend { + GStreamer(String), + PulseAudio, + Alsa(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SpotifyPlayerSettings { + pub bitrate: Bitrate, + pub backend: AudioBackend, + pub gapless: bool, + pub ap_port: Option, +} + +impl Default for SpotifyPlayerSettings { + fn default() -> Self { + Self { + bitrate: Bitrate::Bitrate160, + gapless: true, + backend: AudioBackend::PulseAudio, + ap_port: None, + } + } +} + +pub struct SpotifyPlayer { + settings: SpotifyPlayerSettings, + player: Option>, + mixer: Option>, + session: Option, + delegate: Rc, +} + +impl SpotifyPlayer { + pub fn new(settings: SpotifyPlayerSettings, delegate: Rc) -> Self { + Self { + settings, + mixer: None, + player: None, + session: None, + delegate, + } + } + + async fn handle(&mut self, action: Command) -> Result<(), SpotifyError> { + match action { + Command::PlayerSetVolume(volume) => { + if let Some(mixer) = self.mixer.as_mut() { + mixer.set_volume((VolumeCtrl::MAX_VOLUME as f64 * volume) as u16); + } + Ok(()) + } + Command::PlayerResume => { + self.player + .as_ref() + .ok_or(SpotifyError::PlayerNotReady)? + .play(); + Ok(()) + } + Command::PlayerPause => { + self.player + .as_ref() + .ok_or(SpotifyError::PlayerNotReady)? + .pause(); + Ok(()) + } + Command::PlayerStop => { + self.player + .as_ref() + .ok_or(SpotifyError::PlayerNotReady)? + .stop(); + Ok(()) + } + Command::PlayerSeek(position) => { + self.player + .as_ref() + .ok_or(SpotifyError::PlayerNotReady)? + .seek(position); + Ok(()) + } + Command::PlayerLoad { track, resume } => { + self.player + .as_mut() + .ok_or(SpotifyError::PlayerNotReady)? + .load(track, resume, 0); + Ok(()) + } + Command::PlayerPreload(track) => { + self.player + .as_mut() + .ok_or(SpotifyError::PlayerNotReady)? + .preload(track); + Ok(()) + } + Command::RefreshToken => { + let session = self.session.as_ref().ok_or(SpotifyError::PlayerNotReady)?; + let (token, token_expiry_time) = get_access_token_and_expiry_time(session).await?; + self.delegate.refresh_successful(token, token_expiry_time); + Ok(()) + } + Command::Logout => { + self.session + .take() + .ok_or(SpotifyError::PlayerNotReady)? + .shutdown(); + let _ = self.player.take(); + Ok(()) + } + Command::PasswordLogin { username, password } => { + let credentials = Credentials::with_password(username, password.clone()); + let new_session = create_session(&credentials, self.settings.ap_port).await?; + let (token, token_expiry_time) = + get_access_token_and_expiry_time(&new_session).await?; + let credentials = credentials::Credentials { + username: new_session.username(), + password, + token, + token_expiry_time: Some(token_expiry_time), + }; + self.delegate.password_login_successful(credentials); + + let new_player = self.create_player(new_session.clone()); + tokio::task::spawn_local(player_setup_delegate( + new_player.get_player_event_channel(), + Rc::clone(&self.delegate), + )); + self.player.replace(new_player); + self.session.replace(new_session); + + Ok(()) + } + Command::TokenLogin { username, token } => { + info!("Login with token, username {}", username); + let credentials = Credentials::with_access_token(token.clone()); + let new_session = create_session(&credentials, self.settings.ap_port).await?; + let credentials = credentials::Credentials { + username: new_session.username(), + password: "".to_string(), + token, + token_expiry_time: None, + }; + self.delegate.token_login_successful(credentials); + + let new_player = self.create_player(new_session.clone()); + tokio::task::spawn_local(player_setup_delegate( + new_player.get_player_event_channel(), + Rc::clone(&self.delegate), + )); + self.player.replace(new_player); + self.session.replace(new_session); + + Ok(()) + } + Command::OAuthLogin => { + let (token, token_expiry_time) = get_access_token_oauth()?; + info!("Login with OAuth2"); + let credentials = Credentials::with_access_token(token.clone()); + let new_session = create_session(&credentials, self.settings.ap_port).await?; + let credentials = credentials::Credentials { + username: new_session.username(), + password: "".to_string(), + token, + token_expiry_time: Some(token_expiry_time), + }; + self.delegate.token_login_successful(credentials); + + let new_player = self.create_player(new_session.clone()); + tokio::task::spawn_local(player_setup_delegate( + new_player.get_player_event_channel(), + Rc::clone(&self.delegate), + )); + self.player.replace(new_player); + self.session.replace(new_session); + + Ok(()) + } + Command::ReloadSettings => { + let settings = SpotSettings::new_from_gsettings().unwrap_or_default(); + self.settings = settings.player_settings; + + let session = self.session.take().ok_or(SpotifyError::PlayerNotReady)?; + let new_player = self.create_player(session); + tokio::task::spawn_local(player_setup_delegate( + new_player.get_player_event_channel(), + Rc::clone(&self.delegate), + )); + self.player.replace(new_player); + + Ok(()) + } + } + } + + fn create_player(&mut self, session: Session) -> Arc { + let backend = self.settings.backend.clone(); + + let player_config = PlayerConfig { + gapless: self.settings.gapless, + bitrate: self.settings.bitrate, + ..Default::default() + }; + info!("bitrate: {:?}", &player_config.bitrate); + + let soft_volume = self + .mixer + .get_or_insert_with(|| { + let mix = Box::new(SoftMixer::open(MixerConfig { + // This value feels reasonable to me. Feel free to change it + volume_ctrl: VolumeCtrl::Log(VolumeCtrl::DEFAULT_DB_RANGE / 2.0), + ..Default::default() + })); + // TODO: Should read volume from somewhere instead of hard coding. + // Sets volume to 100% + mix.set_volume(VolumeCtrl::MAX_VOLUME); + mix + }) + .get_soft_volume(); + Player::new(player_config, session, soft_volume, move || match backend { + AudioBackend::GStreamer(pipeline) => { + let backend = audio_backend::find(Some("gstreamer".to_string())).unwrap(); + backend(Some(pipeline), AudioFormat::default()) + } + AudioBackend::PulseAudio => { + info!("using pulseaudio"); + env::set_var("PULSE_PROP_application.name", "Spot"); + let backend = audio_backend::find(Some("pulseaudio".to_string())).unwrap(); + backend(None, AudioFormat::default()) + } + AudioBackend::Alsa(device) => { + info!("using alsa ({})", &device); + let backend = audio_backend::find(Some("alsa".to_string())).unwrap(); + backend(Some(device), AudioFormat::default()) + } + }) + } + + #[allow(clippy::await_holding_refcell_ref)] + pub async fn start(self, receiver: UnboundedReceiver) -> Result<(), ()> { + let _self = RefCell::new(self); + receiver + .for_each(|action| async { + let mut _self = _self.borrow_mut(); + match _self.handle(action).await { + Ok(_) => {} + Err(err) => _self.delegate.report_error(err), + } + }) + .await; + Ok(()) + } +} + +const REDIRECT_URI: &str = "http://127.0.0.1:8898/login"; +const SPOTIFY_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd"; + +const SCOPES: &str = "user-read-private,\ +playlist-read-private,\ +playlist-read-collaborative,\ +user-library-read,\ +user-library-modify,\ +user-top-read,\ +user-read-recently-played,\ +user-read-playback-state,\ +playlist-modify-public,\ +playlist-modify-private,\ +user-modify-playback-state,\ +streaming,\ +playlist-modify-public"; + +const KNOWN_AP_PORTS: [Option; 4] = [None, Some(80), Some(443), Some(4070)]; + +async fn get_access_token_and_expiry_time( + session: &Session, +) -> Result<(String, SystemTime), SpotifyError> { + let token = session + .token_provider() + .get_token(SCOPES) + .await + .map_err(|_e| SpotifyError::TokenFailed)?; + let expiry_time = SystemTime::now() + token.expires_in; + Ok((token.access_token, expiry_time)) +} + +fn get_access_token_oauth() -> Result<(String, SystemTime), SpotifyError> { + let scopes: Vec<&str> = vec![ + "user-read-private", + "playlist-read-private", + "playlist-read-collaborative", + "user-library-read", + "user-library-modify", + "user-top-read", + "user-read-recently-played", + "user-read-playback-state", + "playlist-modify-public", + "playlist-modify-private", + "user-modify-playback-state", + "streaming", + "playlist-modify-public", + ]; + + match get_access_token(SPOTIFY_CLIENT_ID, REDIRECT_URI, scopes) { + Ok(token) => { + info!("Success: {token:#?}"); + let duration = token.expires_at.duration_since(Instant::now()); + Ok((token.access_token, SystemTime::now() + duration)) + } + Err(e) => { + error!("Failed: {e}"); + Err(SpotifyError::TokenFailed) + } + } +} + +async fn create_session_with_port( + credentials: &Credentials, + ap_port: Option, +) -> Result { + let session_config = SessionConfig { + ap_port, + ..Default::default() + }; + let root = glib::user_cache_dir().join("spot").join("librespot"); + let cache = Cache::new( + Some(root.join("credentials")), + Some(root.join("volume")), + Some(root.join("audio")), + None, + ) + .map_err(|e| dbg!(e)) + .ok(); + let session = Session::new(session_config, cache); + match session.connect(credentials.clone(), true).await { + Ok(_) => Ok(session), + Err(err) => { + warn!("Login failure: {}", err); + Err(SpotifyError::LoginFailed) + } + } +} + +async fn create_session( + credentials: &Credentials, + ap_port: Option, +) -> Result { + match ap_port { + Some(_) => create_session_with_port(credentials, ap_port).await, + None => { + let mut ports_to_try = KNOWN_AP_PORTS.iter(); + loop { + if let Some(next_port) = ports_to_try.next() { + let res = create_session_with_port(credentials, *next_port).await; + match res { + Err(SpotifyError::TechnicalError) => continue, + _ => break res, + } + } else { + break Err(SpotifyError::TechnicalError); + } + } + } + } +} + +async fn player_setup_delegate( + mut channel: PlayerEventChannel, + delegate: Rc, +) { + while let Some(event) = channel.recv().await { + match event { + PlayerEvent::EndOfTrack { .. } => { + delegate.end_of_track_reached(); + } + PlayerEvent::Playing { position_ms, .. } => { + delegate.notify_playback_state(position_ms); + } + PlayerEvent::TimeToPreloadNextTrack { .. } => { + debug!("Requestiong next track to be preloaded..."); + delegate.preload_next_track(); + } + _ => {} + } + } +} diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 0000000..8e67c03 --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,115 @@ +use crate::player::{AudioBackend, SpotifyPlayerSettings}; +use gio::prelude::SettingsExt; +use libadwaita::ColorScheme; +use librespot::playback::config::Bitrate; + +const SETTINGS: &str = "dev.alextren.Spot"; + +#[derive(Clone, Debug, Default)] +pub struct WindowGeometry { + pub width: i32, + pub height: i32, + pub is_maximized: bool, +} + +impl WindowGeometry { + pub fn new_from_gsettings() -> Self { + let settings = gio::Settings::new(SETTINGS); + Self { + width: settings.int("window-width"), + height: settings.int("window-height"), + is_maximized: settings.boolean("window-is-maximized"), + } + } + + pub fn save(&self) -> Option<()> { + let settings = gio::Settings::new(SETTINGS); + settings.delay(); + settings.set_int("window-width", self.width).ok()?; + settings.set_int("window-height", self.height).ok()?; + settings + .set_boolean("window-is-maximized", self.is_maximized) + .ok()?; + settings.apply(); + Some(()) + } +} + +// Player (librespot) settings +impl SpotifyPlayerSettings { + pub fn new_from_gsettings() -> Option { + let settings = gio::Settings::new(SETTINGS); + let bitrate = match settings.enum_("player-bitrate") { + 0 => Some(Bitrate::Bitrate96), + 1 => Some(Bitrate::Bitrate160), + 2 => Some(Bitrate::Bitrate320), + _ => None, + }?; + let backend = match settings.enum_("audio-backend") { + 0 => Some(AudioBackend::PulseAudio), + 1 => Some(AudioBackend::Alsa( + settings.string("alsa-device").as_str().to_string(), + )), + 2 => Some(AudioBackend::GStreamer( + "audioconvert dithering=none ! audioresample ! pipewiresink".to_string(), // This should be configurable eventually + )), + _ => None, + }?; + let gapless = settings.boolean("gapless-playback"); + + let ap_port_val = settings.uint("ap-port"); + if ap_port_val > 65535 { + panic!("Invalid access point port"); + } + + // Access points usually use port 80, 443 or 4070. Since gsettings + // does not allow optional values, we use 0 to indicate that any + // port is OK and we should pass None to librespot's ap-port. + let ap_port = match ap_port_val { + 0 => None, + x => Some(x as u16), + }; + + Some(Self { + bitrate, + backend, + gapless, + ap_port, + }) + } +} + +#[derive(Debug, Clone)] +pub struct SpotSettings { + pub theme_preference: ColorScheme, + pub player_settings: SpotifyPlayerSettings, + pub window: WindowGeometry, +} + +// Application settings +impl SpotSettings { + pub fn new_from_gsettings() -> Option { + let settings = gio::Settings::new(SETTINGS); + let theme_preference = match settings.enum_("theme-preference") { + 0 => Some(ColorScheme::ForceLight), + 1 => Some(ColorScheme::ForceDark), + 2 => Some(ColorScheme::Default), + _ => None, + }?; + Some(Self { + theme_preference, + player_settings: SpotifyPlayerSettings::new_from_gsettings()?, + window: WindowGeometry::new_from_gsettings(), + }) + } +} + +impl Default for SpotSettings { + fn default() -> Self { + Self { + theme_preference: ColorScheme::PreferDark, + player_settings: Default::default(), + window: Default::default(), + } + } +} diff --git a/src/spot.gresource.xml b/src/spot.gresource.xml new file mode 100644 index 0000000..83e70d1 --- /dev/null +++ b/src/spot.gresource.xml @@ -0,0 +1,86 @@ + + + + window.ui + app.css + + app/components/login/login.ui + + app/components/settings/settings.ui + + app/components/search/search.ui + + app/components/album/album.ui + app/components/album/album.css + + app/components/artist/artist.ui + + app/components/details/details.ui + app/components/details/album_header.ui + app/components/details/album_header.css + app/components/details/release_details.ui + + app/components/playlist_details/playlist_details.ui + app/components/playlist_details/playlist_header.ui + app/components/playlist_details/playlist_header.css + + app/components/artist_details/artist_details.css + app/components/artist_details/artist_details.ui + + app/components/library/library.ui + + app/components/saved_playlists/saved_playlists.ui + + app/components/now_playing/now_playing.ui + app/components/device_selector/device_selector.ui + + app/components/saved_tracks/saved_tracks.ui + + app/components/playlist/song.css + app/components/playlist/song.ui + + app/components/user_details/user_details.css + app/components/user_details/user_details.ui + + app/components/playback/playback.css + app/components/playback/playback_controls.ui + app/components/playback/playback_info.ui + app/components/playback/playback_widget.ui + + app/components/selection/selection_toolbar.ui + app/components/selection/selection_toolbar.css + + app/components/headerbar/headerbar.ui + app/components/playlist_details/playlist_headerbar.ui + + app/components/sidebar/sidebar_row.ui + app/components/sidebar/create_playlist.ui + + app/components/scrolling_header/scrolling_header.ui + + + app/components/selection/icons/music-queue-symbolic.svg + app/components/selection/icons/playlist2-symbolic.svg + app/components/sidebar/icons/library-music-symbolic.svg + + + app/components/playlist/playback-indicator/playback-paused-symbolic.svg + app/components/playlist/playback-indicator/playback-0-symbolic.svg + app/components/playlist/playback-indicator/playback-1-symbolic.svg + app/components/playlist/playback-indicator/playback-2-symbolic.svg + app/components/playlist/playback-indicator/playback-3-symbolic.svg + app/components/playlist/playback-indicator/playback-4-symbolic.svg + app/components/playlist/playback-indicator/playback-5-symbolic.svg + app/components/playlist/playback-indicator/playback-6-symbolic.svg + app/components/playlist/playback-indicator/playback-7-symbolic.svg + app/components/playlist/playback-indicator/playback-8-symbolic.svg + app/components/playlist/playback-indicator/playback-9-symbolic.svg + app/components/playlist/playback-indicator/playback-10-symbolic.svg + app/components/playlist/playback-indicator/playback-11-symbolic.svg + app/components/playlist/playback-indicator/playback-12-symbolic.svg + app/components/playlist/playback-indicator/playback-13-symbolic.svg + app/components/playlist/playback-indicator/playback-14-symbolic.svg + app/components/playlist/playback-indicator/playback-15-symbolic.svg + app/components/playlist/playback-indicator/playback-16-symbolic.svg + + diff --git a/src/window.blp b/src/window.blp new file mode 100644 index 0000000..38822fa --- /dev/null +++ b/src/window.blp @@ -0,0 +1,134 @@ +using Gtk 4.0; +using Adw 1; + +Adw.ApplicationWindow window { + default-width: 1080; + default-height: 720; + + Box { + orientation: vertical; + + ShortcutController { + scope: local; + + Shortcut { + trigger: "space"; + action: "action(app.toggle_playback)"; + } + + Shortcut { + trigger: "Q"; + action: "action(app.quit)"; + } + + Shortcut { + trigger: "P"; + action: "action(app.player_prev)"; + } + + Shortcut { + trigger: "N"; + action: "action(app.player_next)"; + } + + Shortcut { + trigger: "Left"; + action: "action(app.nav_pop)"; + } + + Shortcut { + trigger: "F"; + action: "action(app.search)"; + } + } + + Adw.Leaflet leaflet { + vexpand: true; + + Adw.LeafletPage { + navigatable: false; + child: Box { + orientation: vertical; + + Adw.HeaderBar { + show-end-title-buttons: bind leaflet.folded; + + Button search_button { + icon-name: "system-search-symbolic"; + } + + [title] + Adw.WindowTitle { + title: "Spot"; + } + + [end] + MenuButton user { + icon-name: "open-menu-symbolic"; + } + } + + ScrolledWindow { + hscrollbar-policy: never; + ListBox home_listbox { + width-request: 200; + vexpand: true; + + styles [ + "navigation-sidebar", + ] + } + } + }; + } + + Adw.LeafletPage { + navigatable: false; + child: Separator { + orientation: vertical; + }; + } + + Adw.LeafletPage { + name: "main"; + child: Box { + orientation: vertical; + + Adw.ToastOverlay main { + hexpand: true; + vexpand: true; + + Stack navigation_stack { + transition-type: slide_left_right; + } + } + + Overlay { + hexpand: true; + + $PlaybackWidget playback { + hexpand: "1"; + } + + [overlay] + $SelectionToolbarWidget selection_toolbar { + hexpand: "1"; + } + } + }; + } + + visible-child: main; + } + } +} + +Adw.AboutWindow about { + modal: true; + destroy-with-parent: true; + transient-for: window; + application-name: "Spot"; + website: "https://github.com/xou816/spot"; + application-icon: "dev.alextren.Spot"; + license-type: mit_x11; +} diff --git a/subprojects/blueprint-compiler.wrap b/subprojects/blueprint-compiler.wrap new file mode 100644 index 0000000..2dbf858 --- /dev/null +++ b/subprojects/blueprint-compiler.wrap @@ -0,0 +1,8 @@ +[wrap-git] +directory = blueprint-compiler +url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git +revision = v0.8.1 +depth = 1 + +[provide] +program_names = blueprint-compiler