From 1d0927d1b83b4f38ab6e8d34258b7823c87eb6a5 Mon Sep 17 00:00:00 2001 From: Dzheremi-belpois Date: Wed, 25 Dec 2024 00:49:53 +0300 Subject: [PATCH] feat: added syncing page layout (adaptive) - chore: added .pyi files for better IDE working - added realization for easy cover update in SongCard for future --- chronograph/__builtins__.pyi | 1 + chronograph/main.py | 4 +- chronograph/shared.pyi | 9 +- chronograph/ui/BoxDialog.py | 4 +- chronograph/ui/BoxDialog.pyi | 22 ++ chronograph/ui/SongCard.py | 77 ++++- chronograph/ui/SongCard.pyi | 43 +++ chronograph/utils/file.py | 6 +- chronograph/utils/file.pyi | 34 ++ chronograph/utils/file_mutagen_id3.pyi | 13 + chronograph/utils/file_mutagen_vorbis.pyi | 13 + chronograph/utils/parsers.py | 2 +- chronograph/utils/parsers.pyi | 7 + chronograph/utils/select_data.py | 2 - chronograph/utils/select_data.pyi | 4 + chronograph/window.py | 5 + chronograph/window.pyi | 49 +++ data/Chronograph.gresource.xml.in | 10 + data/gtk/ui/SongCard.blp | 12 +- data/gtk/window.blp | 290 ++++++++++++++++++ .../icons/symbolic/100ms-back-symbolic.svg | 2 + .../icons/symbolic/100ms-forw-symbolic.svg | 2 + .../icons/symbolic/add-line-symbolic.svg | 2 + .../icons/symbolic/export-to-symbolic.svg | 2 + .../icons/symbolic/import-from-symbolic.svg | 2 + .../icons/symbolic/info-button-symbolic.svg | 2 + .../icons/symbolic/line-actions-symbolic.svg | 3 + .../icons/symbolic/replay-line-symbolic.svg | 3 + .../icons/symbolic/sync-line-symbolic.svg | 2 + .../icons/symbolic/toggle-repeat-symbolic.svg | 2 + 30 files changed, 606 insertions(+), 23 deletions(-) create mode 100644 chronograph/__builtins__.pyi create mode 100644 chronograph/ui/BoxDialog.pyi create mode 100644 chronograph/ui/SongCard.pyi create mode 100644 chronograph/utils/file.pyi create mode 100644 chronograph/utils/file_mutagen_id3.pyi create mode 100644 chronograph/utils/file_mutagen_vorbis.pyi create mode 100644 chronograph/utils/parsers.pyi create mode 100644 chronograph/utils/select_data.pyi create mode 100644 chronograph/window.pyi create mode 100644 data/icons/icons/symbolic/100ms-back-symbolic.svg create mode 100644 data/icons/icons/symbolic/100ms-forw-symbolic.svg create mode 100644 data/icons/icons/symbolic/add-line-symbolic.svg create mode 100644 data/icons/icons/symbolic/export-to-symbolic.svg create mode 100644 data/icons/icons/symbolic/import-from-symbolic.svg create mode 100644 data/icons/icons/symbolic/info-button-symbolic.svg create mode 100644 data/icons/icons/symbolic/line-actions-symbolic.svg create mode 100644 data/icons/icons/symbolic/replay-line-symbolic.svg create mode 100644 data/icons/icons/symbolic/sync-line-symbolic.svg create mode 100644 data/icons/icons/symbolic/toggle-repeat-symbolic.svg diff --git a/chronograph/__builtins__.pyi b/chronograph/__builtins__.pyi new file mode 100644 index 0000000..7a7d99b --- /dev/null +++ b/chronograph/__builtins__.pyi @@ -0,0 +1 @@ +def _(_msg: str, /) -> str: ... \ No newline at end of file diff --git a/chronograph/main.py b/chronograph/main.py index 5abb794..4c45975 100644 --- a/chronograph/main.py +++ b/chronograph/main.py @@ -18,7 +18,9 @@ class ChronographApplication(Adw.Application): win: ChronographWindow def __init__(self) -> None: - super().__init__(application_id=shared.APP_ID) + super().__init__( + application_id=shared.APP_ID, flags=Gio.ApplicationFlags.DEFAULT_FLAGS + ) theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()) theme.add_resource_path(shared.PREFIX + "/data/icons") diff --git a/chronograph/shared.pyi b/chronograph/shared.pyi index 3e94144..8478cf0 100644 --- a/chronograph/shared.pyi +++ b/chronograph/shared.pyi @@ -1,6 +1,9 @@ from pathlib import Path -from gi.repository import Gio, Adw # type: ignore +from gi.repository import Gio + +from chronograph.main import ChronographApplication +from chronograph.window import ChronographWindow # type: ignore APP_ID: str VERSION: str @@ -12,5 +15,5 @@ cache_dir: Path schema: Gio.Settings state_schema: Gio.Settings -app: Adw.Application -win: Adw.ApplicationWindow +app: ChronographApplication +win: ChronographWindow diff --git a/chronograph/ui/BoxDialog.py b/chronograph/ui/BoxDialog.py index af7d614..7e4bb5a 100644 --- a/chronograph/ui/BoxDialog.py +++ b/chronograph/ui/BoxDialog.py @@ -21,7 +21,7 @@ class BoxDialog(Adw.Dialog): diaglog_title_label : Gtk.Label -> Label of the dialog props_list : Gtk.ListBox -> ListBox with `Adw.ActionRow(s)` with provided data """ - + __gtype_name__ = "BoxDialog" dialog_title_label: Gtk.Label = Gtk.Template.Child() @@ -33,7 +33,7 @@ def __init__(self, label: str, lines_content: tuple) -> None: for entry in lines_content: self.props_list.append( Adw.ActionRow( - title=entry[0], subtitle=entry[1], css_classes=["property"] + title=entry[0], subtitle=entry[1], css_classes=["property"], use_markup=False ) ) diff --git a/chronograph/ui/BoxDialog.pyi b/chronograph/ui/BoxDialog.pyi new file mode 100644 index 0000000..ab46300 --- /dev/null +++ b/chronograph/ui/BoxDialog.pyi @@ -0,0 +1,22 @@ +from gi.repository import Adw, Gtk + +class BoxDialog(Adw.Dialog): + """Dialog with lines of `Adw.ActionRow(s)` with provided content + + Parameters + ---------- + label : str + Label of the dialog + lines_content : tuple + titles and subtitles of `Adw.ActionRow(s)`. Like `(("1st Title", "1st subtitle"), ("2nd title", "2nd subtitle"), ...)` + + GTK Objects + ---------- + :: + + diaglog_title_label : Gtk.Label -> Label of the dialog + props_list : Gtk.ListBox -> ListBox with `Adw.ActionRow(s)` with provided data + """ + + dialog_title_label: Gtk.Label + props_list: Gtk.ListBox diff --git a/chronograph/ui/SongCard.py b/chronograph/ui/SongCard.py index 187dad9..3308b08 100644 --- a/chronograph/ui/SongCard.py +++ b/chronograph/ui/SongCard.py @@ -3,16 +3,23 @@ from gi.repository import GObject, Gtk # type: ignore from chronograph import shared +from chronograph.ui.BoxDialog import BoxDialog from chronograph.utils.file_mutagen_id3 import FileID3 from chronograph.utils.file_mutagen_vorbis import FileVorbis +label_str = _("About File") +title_str = _("Title") +artist_str = _("Artist") +album_str = _("Album") +path_str = _("Path") + @Gtk.Template(resource_path=shared.PREFIX + "/gtk/ui/SongCard.ui") class SongCard(Gtk.Box): """Card with Title, Artist and Cover of provided file Parameters - ---------- + # ---------- file : Union[FileID3, FileVorbis] File of `.ogg`, `.flac`, `.mp3` and `.wav` formats @@ -34,8 +41,9 @@ class SongCard(Gtk.Box): buttons_revealer: Gtk.Revealer = Gtk.Template.Child() play_button: Gtk.Button = Gtk.Template.Child() metadata_editor_button: Gtk.Button = Gtk.Template.Child() + info_button: Gtk.Button = Gtk.Template.Child() cover_button: Gtk.Button = Gtk.Template.Child() - cover: Gtk.Image = Gtk.Template.Child() + cover_img: Gtk.Image = Gtk.Template.Child() title_label: Gtk.Label = Gtk.Template.Child() artist_label: Gtk.Label = Gtk.Template.Child() @@ -48,15 +56,35 @@ def __init__(self, file: Union[FileID3, FileVorbis]) -> None: self.add_controller(self.event_controller_motion) self.event_controller_motion.connect("enter", self.toggle_buttons) self.event_controller_motion.connect("leave", self.toggle_buttons) - # self.metadata_editor_button.connect( - # "clicked", lambda *_: self.set_property("title", "New value") - # ) + self.info_button.connect( + "clicked", + lambda *_: BoxDialog( + label_str, + ( + (title_str, self.title), + (artist_str, self.artist), + (album_str, self.album), + (path_str, self._file.path), + ), + ).present(shared.win), + ) + self.cover_button.connect( + "clicked", + lambda *_: ( + shared.win.navigation_view.push(shared.win.sync_navigation_page), + mediastream := Gtk.MediaFile.new_for_filename(self._file.path), + shared.win.controls.set_media_stream(mediastream), + shared.win.controls_shrinked.set_media_stream(mediastream), + ), + ) + self.metadata_editor_button.connect( + "clicked", + lambda *_: self.set_property( + "cover", open("/home/dzheremi/Pictures/pp.jpg", "rb").read() + ), + ) self.bind_props() - - if (_texture := self._file.get_cover_texture()) == "icon": - self.cover.set_from_icon_name("note-placeholder") - else: - self.cover.props.paintable = _texture + self.invalidate_cover() def toggle_buttons(self, *_args) -> None: """Sets if buttons should be visible or not""" @@ -74,10 +102,24 @@ def invalidate_update(self, property: str) -> None: """ getattr(self, f"{property}_label").set_text(getattr(self._file, f"{property}")) + def invalidate_cover(self) -> None: + """Automatically updates cover on property change""" + if (_texture := self._file.get_cover_texture()) == "icon": + self.cover_img.set_from_icon_name("note-placeholder") + else: + self.cover_img.props.paintable = _texture + def bind_props(self) -> None: """Binds properties to update interface labels on change""" - self.connect("notify::title", lambda *_: self.invalidate_update("title")) - self.connect("notify::artist", lambda *_: self.invalidate_update("artist")) + self.connect( + "notify::title", + lambda _object, property: self.invalidate_update(property.name), + ) + self.connect( + "notify::artist", + lambda _object, property: self.invalidate_update(property.name), + ) + self.connect("notify::cover", lambda *_: self.invalidate_cover()) @GObject.Property(type=str) def title(self) -> str: @@ -102,3 +144,14 @@ def album(self) -> str: @album.setter def album(self, value: str) -> None: self._file.album = value + + @GObject.Property + def cover(self) -> Union[str, bytes]: + return self._file.cover + + @cover.setter + def cover(self, data: bytes) -> None: + if type(data) == bytes: + self._file.cover = data + else: + raise ValueError("Cover must be bytes") diff --git a/chronograph/ui/SongCard.pyi b/chronograph/ui/SongCard.pyi new file mode 100644 index 0000000..b6b072e --- /dev/null +++ b/chronograph/ui/SongCard.pyi @@ -0,0 +1,43 @@ +from typing import Union + +from gi.repository import Gtk + +from chronograph.utils.file_mutagen_id3 import FileID3 +from chronograph.utils.file_mutagen_vorbis import FileVorbis # type: ignore + +class SongCard(Gtk.Box): + """Card with Title, Artist and Cover of provided file + + Parameters + ---------- + file : Union[FileID3, FileVorbis] + File of `.ogg`, `.flac`, `.mp3` and `.wav` formats + + GTK Objects + ---------- + :: + + buttons_revealer: Gtk.Revealer -> Revealer for Play and Edit buttons + play_button: Gtk.Button -> Play button + metadata_editor_button: Gtk.Button -> Metadata editor button + cover_button: Gtk.Button -> Clickable cover of song + cover: Gtk.Image -> Cover image of song + title_label: Gtk.Label -> Title of song + artist_label: Gtk.Label -> Artist of song + """ + + buttons_revealer: Gtk.Revealer + play_button: Gtk.Button + metadata_editor_button: Gtk.Button + info_button: Gtk.Button + cover_button: Gtk.Button + cover_img: Gtk.Image + title_label: Gtk.Label + artist_label: Gtk.Label + + _file: Union[FileID3, FileVorbis] + + def toggle_buttons(self, *_args) -> None: ... + def invalidate_update(self, property: str) -> None: ... + def invalidate_cover(self) -> None: ... + def bind_props(self) -> None: ... diff --git a/chronograph/utils/file.py b/chronograph/utils/file.py index 0c28a87..a064c6d 100644 --- a/chronograph/utils/file.py +++ b/chronograph/utils/file.py @@ -27,7 +27,7 @@ class BaseFile: _title: str = "Unknown" _artist: str = "Unknown" _album: str = "Unknown" - _cover: Union[Gdk.Texture, str] = None + _cover: Union[bytes, str] = None _mutagen_file: dict = None def __init__(self, path: str) -> None: @@ -91,6 +91,10 @@ def cover(self) -> bytes: def cover(self, data: bytes) -> None: self._cover = data + @property + def path(self) -> str: + return self._path + def load_str_data(self) -> None: """Should be implemenmted in file specific child classes""" raise NotImplementedError diff --git a/chronograph/utils/file.pyi b/chronograph/utils/file.pyi new file mode 100644 index 0000000..8a14acb --- /dev/null +++ b/chronograph/utils/file.pyi @@ -0,0 +1,34 @@ +from typing import Union + +from gi.repository import Gdk + +class BaseFile: + """A base class for mutagen filetypes classes + + Parameters + ---------- + path : str + A path to file for loading + + Props + -------- + :: + + title : str -> Title of song + artist : str -> Artist of song + album : str -> Album of song + cover : Gdk.Texture | str -> Cover of song + """ + + _title: str + _artist: str + _album: str + _cover: Union[bytes, str] + _mutagen_file: dict + + _path: str + + def load_from_file(self, path: str) -> None: ... + def get_cover_texture(self) -> Union[Gdk.Texture, str]: ... + def load_str_data(self) -> None: ... + def load_cover(self) -> None: ... diff --git a/chronograph/utils/file_mutagen_id3.pyi b/chronograph/utils/file_mutagen_id3.pyi new file mode 100644 index 0000000..9377f20 --- /dev/null +++ b/chronograph/utils/file_mutagen_id3.pyi @@ -0,0 +1,13 @@ +from .file import BaseFile + +class FileID3(BaseFile): + """A ID3 compatible file class. Inherited from `BaseFile` + + Parameters + -------- + path : str + A path to file for loading + """ + + def load_cover(self) -> None: ... + def load_str_data(self) -> None: ... diff --git a/chronograph/utils/file_mutagen_vorbis.pyi b/chronograph/utils/file_mutagen_vorbis.pyi new file mode 100644 index 0000000..cfcb528 --- /dev/null +++ b/chronograph/utils/file_mutagen_vorbis.pyi @@ -0,0 +1,13 @@ +from .file import BaseFile + +class FileVorbis(BaseFile): + """A Vorbis (ogg, flac) compatible file class. Inherited from `BaseFile` + + Parameters + -------- + path : str + A path to file for loading + """ + + def load_cover(self) -> None: ... + def load_str_data(self, tags: list = ["title", "artist", "album"]) -> None: ... diff --git a/chronograph/utils/parsers.py b/chronograph/utils/parsers.py index 6645962..415f8c6 100644 --- a/chronograph/utils/parsers.py +++ b/chronograph/utils/parsers.py @@ -19,6 +19,7 @@ def dir_parser(path: str, *_args) -> None: Path to directory to parse """ shared.win.library.remove_all() + shared.win.library_scrolled_window.set_child(shared.win.library) path = path + "/" mutagen_files = [] for file in os.listdir(path): @@ -31,7 +32,6 @@ def dir_parser(path: str, *_args) -> None: for file in mutagen_files: GLib.idle_add(songcard_idle, file) - shared.win.library_scrolled_window.set_child(shared.win.library) shared.win.right_buttons_revealer.set_reveal_child(True) shared.win.left_buttons_revealer.set_reveal_child(True) # NOTE: This should be implemented in ALL parsers functions diff --git a/chronograph/utils/parsers.pyi b/chronograph/utils/parsers.pyi new file mode 100644 index 0000000..baca30e --- /dev/null +++ b/chronograph/utils/parsers.pyi @@ -0,0 +1,7 @@ +from typing import Union + +from chronograph.utils.file_mutagen_id3 import FileID3 +from chronograph.utils.file_mutagen_vorbis import FileVorbis + +def dir_parser(path: str, *_args) -> None: ... +def songcard_idle(file: Union[FileID3, FileVorbis]) -> None: ... diff --git a/chronograph/utils/select_data.py b/chronograph/utils/select_data.py index e248fb6..f12f552 100644 --- a/chronograph/utils/select_data.py +++ b/chronograph/utils/select_data.py @@ -1,5 +1,4 @@ import threading -from typing import Any from gi.repository import Gio, Gtk # type: ignore @@ -25,7 +24,6 @@ def on_selected_dir(file_dialog: Gtk.FileDialog, result: Gio.Task) -> None: result : Gio.Task Task for reading, callbacked from `select_dir` """ - print(result) dir = file_dialog.select_folder_finish(result) thread = threading.Thread(target=lambda: (dir_parser(dir.get_path()))) thread.daemon = True diff --git a/chronograph/utils/select_data.pyi b/chronograph/utils/select_data.pyi new file mode 100644 index 0000000..12676a4 --- /dev/null +++ b/chronograph/utils/select_data.pyi @@ -0,0 +1,4 @@ +from gi.repository import Gio, Gtk # type: ignore + +def select_dir(*_args) -> None: ... +def on_selected_dir(file_dialog: Gtk.FileDialog, result: Gio.Task) -> None: ... diff --git a/chronograph/window.py b/chronograph/window.py index b1ca5a0..8432e96 100644 --- a/chronograph/window.py +++ b/chronograph/window.py @@ -44,6 +44,11 @@ class ChronographWindow(Adw.ApplicationWindow): library_scrolled_window: Gtk.ScrolledWindow = Gtk.Template.Child() library: Gtk.FlowBox = Gtk.Template.Child() + # Syncing page widgets + sync_navigation_page: Adw.NavigationPage = Gtk.Template.Child() + controls: Gtk.MediaControls = Gtk.Template.Child() + controls_shrinked: Gtk.MediaControls = Gtk.Template.Child() + sort_state: str = shared.state_schema.get_string("sorting") def __init__(self, **kwargs) -> None: diff --git a/chronograph/window.pyi b/chronograph/window.pyi new file mode 100644 index 0000000..e858715 --- /dev/null +++ b/chronograph/window.pyi @@ -0,0 +1,49 @@ +from gi.repository import Adw, Gtk # type: ignore + +class ChronographWindow(Adw.ApplicationWindow): + """App window class + + GTK Objects + ---------- + :: + + # Status pages + no_source_opened: Adw.StatusPage -> Status page> displayed when no items in library + + # Library view widgets + navigation_view: Adw.NavigationView -> Main Navigation view + library_nav_page: Adw.NavigationPage -> Library Navigation page + overlay_split_view: Adw.OverlaySplitView -> Split view for Sidebar and Library + search_bar: Gtk.SearchBar -> Search bar + search_entry: Gtk.SearchEntry -> Search field + library_overlay: Gtk.Overlay -> Library overlay + library_scrolled_window: Gtk.ScrolledWindow -> Library scroll possibility + library: Gtk.FlowBox -> Library itself + """ + # Status pages + no_source_opened: Adw.StatusPage + + # Library view widgets + navigation_view: Adw.NavigationView + library_nav_page: Adw.NavigationPage + overlay_split_view: Adw.OverlaySplitView + right_buttons_revealer: Gtk.Revealer + left_buttons_revealer: Gtk.Revealer + search_bar: Gtk.SearchBar + search_entry: Gtk.SearchEntry + library_overlay: Gtk.Overlay + library_scrolled_window: Gtk.ScrolledWindow + library: Gtk.FlowBox + + # Syncing page widgets + sync_navigation_page: Adw.NavigationPage + controls: Gtk.MediaControls + controls_shrinked: Gtk.MediaControls + + def on_toggle_sidebar_action(self, *_args) -> None: ... + def on_toggle_search_action(self, *_args) -> None: ... + def on_select_dir_action(self, *_args) -> None: ... + def filtering(self, child: Gtk.FlowBoxChild) -> bool: ... + def sorting(self, child1: Gtk.FlowBoxChild, child2: Gtk.FlowBoxChild) -> int: ... + def on_search_changed(self, entry: Gtk.SearchEntry) -> None: ... + def on_sort_changed(self, *_args) -> None: ... diff --git a/data/Chronograph.gresource.xml.in b/data/Chronograph.gresource.xml.in index fc5f176..54e9715 100644 --- a/data/Chronograph.gresource.xml.in +++ b/data/Chronograph.gresource.xml.in @@ -15,6 +15,16 @@ icons/icons/symbolic/play-button-symbolic.svg icons/icons/symbolic/edit-metadata-symbolic.svg icons/icons/symbolic/sort-alphabetically-symbolic.svg + icons/icons/symbolic/toggle-repeat-symbolic.svg + icons/icons/symbolic/info-button-symbolic.svg + icons/icons/symbolic/sync-line-symbolic.svg + icons/icons/symbolic/replay-line-symbolic.svg + icons/icons/symbolic/100ms-back-symbolic.svg + icons/icons/symbolic/100ms-forw-symbolic.svg + icons/icons/symbolic/import-from-symbolic.svg + icons/icons/symbolic/export-to-symbolic.svg + icons/icons/symbolic/line-actions-symbolic.svg + icons/icons/symbolic/add-line-symbolic.svg icons/icons/scalable/note-placeholder.svg diff --git a/data/gtk/ui/SongCard.blp b/data/gtk/ui/SongCard.blp index b53f9b7..5c9f9a8 100644 --- a/data/gtk/ui/SongCard.blp +++ b/data/gtk/ui/SongCard.blp @@ -46,6 +46,16 @@ template $SongCard: Box { "osd" ] } + + Button info_button { + icon-name: "info-button-symbolic"; + tooltip-text: _("Show song info"); + + styles [ + "circular", + "osd" + ] + } } } @@ -60,7 +70,7 @@ template $SongCard: Box { Box { orientation: vertical; - Image cover { + Image cover_img { name: "cover"; width-request: 160; height-request: 160; diff --git a/data/gtk/window.blp b/data/gtk/window.blp index 3a4e3ae..36c51ba 100644 --- a/data/gtk/window.blp +++ b/data/gtk/window.blp @@ -32,6 +32,8 @@ template $ChronographWindow : Adw.ApplicationWindow { setters { overlay_split_view.collapsed: true; + controls_box_shrinked.visible: true; + syncing_buttons_box.halign: center; } } @@ -157,6 +159,263 @@ template $ChronographWindow : Adw.ApplicationWindow { } } +Adw.NavigationPage sync_navigation_page { + title: _("Syncing"); + + Adw.ToolbarView { + [top] + Adw.HeaderBar { + decoration-layout: ":close"; + } + + [top] + Adw.Clamp { + maximum-size: 600; + orientation: horizontal; + + Box { + orientation: horizontal; + spacing: 8; + margin-top: 12; + margin-start: 4; + margin-end: 4; + + styles ["card"] + + Image sync_page_cover { + icon-name: "note-placeholder"; + pixel-size: 100; + overflow: hidden; + + styles ["rounded"] + } + + Box { + orientation: vertical; + vexpand: true; + spacing: 2; + margin-top: 8; + margin-end: 4; + + Inscription sync_page_title { + text: "Unknown"; + text-overflow: ellipsize_end; + hexpand: true; + + styles ["heading"] + } + + Inscription sync_page_artist { + text: "Unknown"; + text-overflow: ellipsize_end; + hexpand: true; + + styles ["heading"] + } + + Box { + visible: bind controls_box_shrinked.visible bidirectional; + orientation: horizontal; + margin-top: 4; + spacing: 6; + + ToggleButton toggle_repeat_button_shrinked { + tooltip-text: bind toggle_repeat_button.tooltip-text bidirectional; + icon-name: bind toggle_repeat_button.icon-name bidirectional; + active: bind toggle_repeat_button.active bidirectional; + } + + Button info_button_shrinked { + tooltip-text: bind info_button.tooltip-text bidirectional; + icon-name: bind info_button.icon-name bidirectional; + } + } + + Box controls_box { + margin-top: 4; + margin-end: 8; + spacing: 4; + visible: bind controls_box_shrinked.visible inverted; + + MediaControls controls { + hexpand: true; + visible: true; + } + + ToggleButton toggle_repeat_button { + tooltip-text: _("Toggle song repeat"); + icon-name: "toggle-repeat-symbolic"; + active: false; + halign: center; + } + } + } + } + } + + [top] + Adw.Clamp { + orientation: vertical; + maximum-size: 30; + margin-top: 4; + + Adw.Clamp { + orientation: horizontal; + maximum-size: 600; + + Box controls_box_shrinked { + orientation: horizontal; + visible: false; + margin-start: 4; + margin-end: 4; + + Box { + margin-end: 8; + margin-start: 8; + spacing: 4; + valign: center; + + MediaControls controls_shrinked { + hexpand: true; + } + } + + styles ["card"] + } + } + } + + [top] + Adw.Clamp { + margin-top: 4; + orientation: horizontal; + maximum-size: 600; + + Box syncing_buttons_box { + orientation: horizontal; + valign: center; + spacing: 4; + margin-bottom: 4; + + Button sync_line_button { + tooltip-text: _("Sync/Re-sync line"); + icon-name: "sync-line-symbolic"; + + styles [ + "suggested-action", + "circular" + ] + } + + Button replay_line_button { + tooltip-text: _("Replay selected line"); + icon-name: "replay-line-symbolic"; + + styles ["circular"] + } + + Button rew100_button { + tooltip-text: _("Re-sync selected line 100ms back"); + icon-name: "100ms-back-symbolic"; + + styles ["circular"] + } + + Button forw100_button { + tooltip-text: _("Re-sync selected line 100ms forward"); + icon-name: "100ms-forw-symbolic"; + + styles ["circular"] + } + + MenuButton import_lyrics_button { + tooltip-text: _("Import from …"); + icon-name: "import-from-symbolic"; + menu-model: import_from; + + styles [ + "osd", + "circular" + ] + } + + MenuButton export_lyrics_button { + tooltip-text: _("Export to …"); + icon-name: "export-to-symbolic"; + menu-model: export_to; + + styles [ + "osd", + "circular" + ] + } + + Separator { + hexpand: true; + visible: bind controls_box_shrinked.visible inverted; + + styles ["spacer"] + } + + MenuButton line_actions_button { + tooltip-text: _("Actions with selected line"); + icon-name: "line-actions-symbolic"; + menu-model: line_actions; + + styles [ + "osd", + "circular" + ] + } + + Button info_button { + tooltip-text: _("About file"); + icon-name: "info-button-symbolic"; + visible: bind controls_box_shrinked.visible inverted; + + styles ["circular"] + } + } + } + + ScrolledWindow { + Adw.Clamp { + orientation: horizontal; + maximum-size: 600; + + Box { + orientation: vertical; + spacing: 8; + hexpand: true; + margin-start: 4; + margin-end: 4; + + ListBox sync_lines { + selection-mode: none; + + Adw.EntryRow {} + + styles ["boxed-list"] + } + + Box add_line_button_box { + halign: end; + + Button add_line_button { + Adw.ButtonContent { + label: _("Add line"); + icon-name: "add-line-symbolic"; + } + + styles ["suggested-action"] + } + } + } + } + } + } +} + menu open_source_menu { section { item (_("Directory"), "win.select_dir") @@ -178,4 +437,35 @@ menu sorting_menu { target: "z-a"; } } +} + +menu import_from { + section { + label: _("Import from …"); + + item (_("File"), "win.import_from_file") + item (_("Clipboard"), "win.import_from_clipboard") + item (_("LRClib"), "win.import_from_lrclib") + } +} + +menu export_to { + section { + label: _("Export to …"); + + item (_("File"), "win.export_to_file") + item (_("Clipboard"), "win.export_to_clipboard") + item (_("LRClib"), "win.export_to_lrclib") + } +} + +menu line_actions { + section { + label: _("Actions with selected line"); + + item (_("Remove line"), "win.remove_selected_line") + item (_("Prepend line"), "win.prepend_selected_line") + item (_("Append line"), "win.append_selected_line") + item (_("Append line to end"), "win.append_line") + } } \ No newline at end of file diff --git a/data/icons/icons/symbolic/100ms-back-symbolic.svg b/data/icons/icons/symbolic/100ms-back-symbolic.svg new file mode 100644 index 0000000..21e4adb --- /dev/null +++ b/data/icons/icons/symbolic/100ms-back-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/icons/icons/symbolic/100ms-forw-symbolic.svg b/data/icons/icons/symbolic/100ms-forw-symbolic.svg new file mode 100644 index 0000000..9b9a5ac --- /dev/null +++ b/data/icons/icons/symbolic/100ms-forw-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/icons/icons/symbolic/add-line-symbolic.svg b/data/icons/icons/symbolic/add-line-symbolic.svg new file mode 100644 index 0000000..61849b1 --- /dev/null +++ b/data/icons/icons/symbolic/add-line-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/icons/icons/symbolic/export-to-symbolic.svg b/data/icons/icons/symbolic/export-to-symbolic.svg new file mode 100644 index 0000000..b6c0657 --- /dev/null +++ b/data/icons/icons/symbolic/export-to-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/icons/icons/symbolic/import-from-symbolic.svg b/data/icons/icons/symbolic/import-from-symbolic.svg new file mode 100644 index 0000000..eaefb41 --- /dev/null +++ b/data/icons/icons/symbolic/import-from-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/icons/icons/symbolic/info-button-symbolic.svg b/data/icons/icons/symbolic/info-button-symbolic.svg new file mode 100644 index 0000000..254e7e8 --- /dev/null +++ b/data/icons/icons/symbolic/info-button-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/icons/icons/symbolic/line-actions-symbolic.svg b/data/icons/icons/symbolic/line-actions-symbolic.svg new file mode 100644 index 0000000..59f5468 --- /dev/null +++ b/data/icons/icons/symbolic/line-actions-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/data/icons/icons/symbolic/replay-line-symbolic.svg b/data/icons/icons/symbolic/replay-line-symbolic.svg new file mode 100644 index 0000000..8126c85 --- /dev/null +++ b/data/icons/icons/symbolic/replay-line-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/data/icons/icons/symbolic/sync-line-symbolic.svg b/data/icons/icons/symbolic/sync-line-symbolic.svg new file mode 100644 index 0000000..f3b8737 --- /dev/null +++ b/data/icons/icons/symbolic/sync-line-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/icons/icons/symbolic/toggle-repeat-symbolic.svg b/data/icons/icons/symbolic/toggle-repeat-symbolic.svg new file mode 100644 index 0000000..4096b37 --- /dev/null +++ b/data/icons/icons/symbolic/toggle-repeat-symbolic.svg @@ -0,0 +1,2 @@ + +