Пример #1
0
class FileNotFoundDialog():
    _importer: Importer = inject.attr(Importer)
    _library: Library = inject.attr(Library)

    def __init__(self, chapter: Chapter):
        self.missing_chapter = chapter
        self.parent = cozy.ui.main_view.CozyUI()
        self.builder = Gtk.Builder.new_from_resource(
            "/com/github/geigi/cozy/file_not_found.ui")
        self.dialog = self.builder.get_object("dialog")
        self.dialog.set_modal(self.parent.window)
        self.builder.get_object("file_label").set_markup("<tt>" +
                                                         chapter.file +
                                                         "</tt>")

        cancel_button = self.builder.get_object("cancel_button")
        cancel_button.connect("clicked", self.close)
        locate_button = self.builder.get_object("locate_button")
        locate_button.connect("clicked", self.locate)

    def show(self):
        self.dialog.show()

    def close(self, _):
        self.dialog.destroy()

    def locate(self, __):
        directory, filename = os.path.split(self.missing_chapter.file)
        dialog = Gtk.FileChooserDialog(
            "Please locate the file " + filename, self.parent.window,
            Gtk.FileChooserAction.OPEN,
            (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN,
             Gtk.ResponseType.OK))

        filter = Gtk.FileFilter()
        filter.add_pattern(filename)
        filter.set_name(filename)
        dialog.add_filter(filter)
        path, file_extension = os.path.splitext(self.missing_chapter.file)
        filter = Gtk.FileFilter()
        filter.add_pattern("*" + file_extension)
        filter.set_name(file_extension + " files")
        dialog.add_filter(filter)
        filter = Gtk.FileFilter()
        filter.add_pattern("*")
        filter.set_name(_("All files"))
        dialog.add_filter(filter)
        dialog.set_local_only(False)

        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            new_location = dialog.get_filename()
            self.missing_chapter.file = new_location
            self._importer.scan()
            self.dialog.destroy()

        dialog.destroy()
Пример #2
0
class WhatsNewWindow(Gtk.Window):
    __gtype_name__ = 'WhatsNew'

    content_stack: Gtk.Stack = Gtk.Template.Child()
    continue_button: Gtk.Button = Gtk.Template.Child()
    children: List[Gtk.Widget]

    main_window: CozyUI = inject.attr("MainWindow")
    app_settings: ApplicationSettings = inject.attr(ApplicationSettings)

    page = 0

    def __init__(self, **kwargs):
        if self.app_settings.last_launched_version == CozyVersion:
            return

        super().__init__(**kwargs)

        self.set_modal(self.main_window.window)

        self._fill_window()
        if len(self.children) < 1:
            self.end()
            return

        self.set_default_size(800, 550)

        for widget in self.children:
            self.content_stack.add(widget)

        self.continue_button.connect("clicked", self.__on_continue_clicked)
        self.show()

    def _fill_window(self):
        self.children = []

        if version.parse(self.app_settings.last_launched_version
                         ) < version.parse(INTRODUCED):
            self.children.append(WhatsNewM4B())

        if not self.app_settings.last_launched_version:
            self.children.append(WhatsNewImporter())
            self.children.append(ErrorReporting())

    def __on_continue_clicked(self, widget):
        if len(self.children) == self.page + 1:
            self.end()
            return

        self.page += 1
        self.content_stack.set_visible_child(self.children[self.page])

    def end(self):
        self.app_settings.last_launched_version = CozyVersion
        self.close()
        self.destroy()
Пример #3
0
class PlaybackSpeedViewModel(Observable, EventSender):
    _player: Player = inject.attr(Player)

    def __init__(self):
        super().__init__()
        super(Observable, self).__init__()

        self._book: Book = self._player.loaded_book

        self._player.add_listener(self._on_player_event)

    @property
    def playback_speed(self) -> float:
        if self._book:
            return self._book.playback_speed
        else:
            return 1.0

    @playback_speed.setter
    def playback_speed(self, new_value: float):
        if self._book:
            self._book.playback_speed = new_value
            self._player.playback_speed = new_value

    def _on_player_event(self, event: str, message):
        if event == "chapter-changed" and message:
            self._book = message
            self._notify("playback_speed")
Пример #4
0
class PlaybackSpeedPopover(Gtk.Popover):
    __gtype_name__ = "PlaybackSpeedPopover"

    _view_model: PlaybackSpeedViewModel = inject.attr(PlaybackSpeedViewModel)

    playback_speed_scale: Gtk.Scale = Gtk.Template.Child()
    playback_speed_label: Gtk.Label = Gtk.Template.Child()

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        self.playback_speed_scale.add_mark(1.0, Gtk.PositionType.RIGHT, None)
        self.playback_speed_scale.set_increments(0.02, 0.05)
        self.playback_speed_scale.connect(
            "value-changed", self._on_playback_speed_scale_changed)

        self._connect_view_model()
        self._on_playback_speed_changed()

    def _connect_view_model(self):
        self._view_model.bind_to("playback_speed",
                                 self._on_playback_speed_changed)

    def _on_playback_speed_scale_changed(self, _):
        speed = round(self.playback_speed_scale.get_value(), 2)
        self._view_model.playback_speed = speed

        self.playback_speed_label.set_markup(
            "<span font_features='tnum'>{speed:3.1f} x</span>".format(
                speed=speed))

    def _on_playback_speed_changed(self):
        self.playback_speed_scale.set_value(self._view_model.playback_speed)
Пример #5
0
class PlaybackSpeed(EventSender):
    """
    Contains the playback speed logic.
    """
    ui = None
    speed = 1.0
    _player: Player = inject.attr(Player)

    def __init__(self):
        super().__init__()
        self.ui = cozy.ui.main_view.CozyUI()

        self.builder = Gtk.Builder.new_from_resource(
            "/com/github/geigi/cozy/playback_speed_popover.ui")

        self.speed_scale = self.builder.get_object("playback_speed_scale")
        self.speed_label = self.builder.get_object("playback_speed_label")
        self.popover = self.builder.get_object("speed_popover")

        self.speed_scale.add_mark(1.0, Gtk.PositionType.RIGHT, None)
        self.speed_scale.set_increments(0.02, 0.05)
        self.speed_scale.connect("value-changed", self.__set_playback_speed)

        player.add_player_listener(self.__player_changed)

    def get_popover(self):
        return self.popover

    def get_speed(self):
        return self.speed

    def set_speed(self, speed):
        self.speed_scale.set_value(speed)
        self.__set_playback_speed(None)

    def __set_playback_speed(self, widget):
        """
        Set and save the playback speed.
        Update playback speed label.
        """
        self.speed = round(self.speed_scale.get_value(), 2)
        self.speed_label.set_text('{speed:3.1f} x'.format(speed=self.speed))

        player.set_playback_speed(self.speed)

        book = self._player.loaded_book
        if book:
            book.playback_speed = self.speed

        self.emit_event("playback-speed-changed", self.speed)

    def __player_changed(self, event, message):
        """
        Listen to and handle all gst player messages that are important for the ui.
        """
        if event == "track-changed":
            track = message
            speed = track.book.playback_speed
            self.speed_scale.set_value(speed)
            self.__set_playback_speed(None)
Пример #6
0
class StorageBlockList:
    _db = cache = inject.attr(SqliteDatabase)

    def rebase_path(self, old_path: str, new_path: str):
        for element in StorageBlackList.select():
            if old_path in element.path:
                new_file_path = element.path.replace(old_path, new_path)
                StorageBlackList.update(path=new_file_path).where(
                    StorageBlackList.id == element.id).execute()
Пример #7
0
class SettingsViewModel(Observable, object):
    _importer: Importer = inject.attr(Importer)
    _model: Settings = inject.attr(Settings)

    def __init__(self):
        super().__init__()

        self._swap_author_reader: bool = None

        if self._model.first_start:
            self._importer.scan()

    @property
    def swap_author_reader(self):
        return self._swap_author_reader

    @swap_author_reader.setter
    def swap_author_reader(self, value):
        self._swap_author_reader = value
        for callback in self._observers["swap_author_reader"]:
            callback(value)
Пример #8
0
class Settings:
    _storages: List[Storage] = []
    _db = cache = inject.attr(SqliteDatabase)

    def __init__(self):
        with self._db:
            self._db_object: SettingsModel = SettingsModel.get()

    @property
    def first_start(self) -> bool:
        return self._db_object.first_start

    @property
    def last_played_book(self) -> Book:
        return self._db_object.last_played_book

    @last_played_book.setter
    def last_played_book(self, new_value: Book):
        with self._db:
            self._db_object.last_played_book = new_value
            self._db_object.save(only=self._db_object.dirty_fields)

    @property
    def default_location(self):
        return next(location for location in self.storage_locations
                    if location.default)

    @property
    def storage_locations(self):
        if not self._storages:
            self._load_all_storage_locations()

        return self._storages

    @property
    def external_storage_locations(self):
        if not self._storages:
            self._load_all_storage_locations()

        return [storage for storage in self._storages if storage.external]

    def invalidate(self):
        self._storages = []

    def _load_all_storage_locations(self):
        with self._db:
            for storage_db_obj in StorageModel.select(StorageModel.id):
                try:
                    self._storages.append(Storage(self._db, storage_db_obj.id))
                except InvalidPath:
                    log.error(
                        "Invalid path found in database, skipping: {}".format(
                            storage_db_obj.path))
Пример #9
0
class ErrorReporting(Gtk.Box):
    __gtype_name__ = 'ErrorReporting'

    level_label: Gtk.Label = Gtk.Template.Child()
    description_label: Gtk.Label = Gtk.Template.Child()
    details_label: Gtk.Label = Gtk.Template.Child()

    verbose_adjustment: Gtk.Adjustment = Gtk.Template.Child()
    verbose_scale: Gtk.Scale = Gtk.Template.Child()

    app_settings: ApplicationSettings = inject.attr(ApplicationSettings)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        self.__init_scale()
        self.__connect()

        level = self.app_settings.report_level
        self.verbose_adjustment.set_value(level + 1)
        self._adjustment_changed(self.verbose_adjustment)

    def __init_scale(self):
        for i in range(1, 5):
            self.verbose_scale.add_mark(i, Gtk.PositionType.RIGHT, None)
        self.verbose_scale.set_round_digits(0)

    def __connect(self):
        self.verbose_adjustment.connect("value-changed",
                                        self._adjustment_changed)

    def _adjustment_changed(self, adjustment: Gtk.Adjustment):
        level = int(adjustment.get_value()) - 1
        self.app_settings.report_level = level
        self._update_ui_texts(level)

    def _update_ui_texts(self, level: int):
        self.level_label.set_text(LEVELS[level])
        self._update_description(level)
        self._update_details(level)

    def _update_description(self, value):
        detail_index = min(value, 1)
        self.description_label.set_text(LEVEL_DESCRIPTION[detail_index])

    def _update_details(self, value):
        details = ""
        for i in range(value + 1):
            for line in LEVEL_DETAILS[i]:
                details += "• {}\n".format(line)
        self.details_label.set_text(details)
Пример #10
0
class InfoBanner:
    _builder: Gtk.Builder = inject.attr("MainWindowBuilder")

    def __init__(self):
        super(InfoBanner, self).__init__()

        self._overlay: Gtk.Overlay = self._builder.get_object("banner_overlay")
        self._toast: Granite.WidgetsToast = Granite.WidgetsToast.new("")
        self._toast.show_all()
        self._overlay.add_overlay(self._toast)
        self._banner_displayed: bool = False

    def show(self, message: str):
        self._toast.set_property("title", message)
        self._toast.set_reveal_child(True)
Пример #11
0
class PowerManager:
    _player: Player = inject.attr(Player)
    _gtk_app = inject.attr("GtkApp")

    def __init__(self):
        self._inhibit_cookie = None

        self._player.add_listener(self._on_player_changed)

    def _on_player_changed(self, event: str, data):
        if event in ["pause", "stop"]:
            if self._inhibit_cookie:
                log.info("Uninhibited standby.")
                self._gtk_app.uninhibit(self._inhibit_cookie)
                self._inhibit_cookie = None

        elif event == "play":
            if self._inhibit_cookie:
                return

            self._inhibit_cookie = self._gtk_app.inhibit(
                None, Gtk.ApplicationInhibitFlags.SUSPEND,
                "Playback of audiobook")
            log.info("Inhibited standby.")
Пример #12
0
class DeleteBookView(Gtk.Dialog):
    __gtype_name__ = 'DeleteBookDialog'

    main_window = inject.attr("MainWindow")

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        self.set_modal(self.main_window.window)

    def get_delete_book(self):
        response = self.run()

        if response == Gtk.ResponseType.APPLY:
            return True
        else:
            return False
Пример #13
0
class Warnings():
    _fs_monitor: FilesystemMonitor = inject.attr("FilesystemMonitor")

    def __init__(self, button: Gtk.MenuButton):
        self.button = button

        self.builder = Gtk.Builder.new_from_resource(
            "/com/github/geigi/cozy/warning_popover.ui")

        self.popover = self.builder.get_object("warning_popover")
        self.warning_container: Gtk.Box = self.builder.get_object(
            "warning_container")

        self._fs_monitor.add_listener(self.__on_storage_changed)

        for storage in self._fs_monitor.get_offline_storages():
            self.append_text(
                gettext('{storage} is offline.').format(storage=storage))

        self.__hide_show_button()

    def get_popover(self):
        return self.popover

    def append_text(self, text):
        label = Gtk.Label()
        self.warning_container.add(label)
        label.set_visible(True)
        label.set_text(text)

    def __on_storage_changed(self, event, message):
        if event == "storage-offline":
            self.append_text(
                gettext('{storage} is offline.').format(storage=message))
        if event == "storage-online":
            for label in self.warning_container.get_children():
                if message in label.get_text():
                    self.warning_container.remove(label)

        self.__hide_show_button()

    def __hide_show_button(self):
        if len(self.warning_container.get_children()) > 0:
            self.button.set_visible(True)
        else:
            self.button.set_visible(False)
Пример #14
0
class StorageBlockList:
    _db = cache = inject.attr(SqliteDatabase)

    def rebase_path(self, old_path: str, new_path: str):
        for element in StorageBlackList.select():
            if old_path in element.path:
                new_file_path = element.path.replace(old_path, new_path)
                StorageBlackList.update(path=new_file_path).where(
                    StorageBlackList.id == element.id).execute()

    def add_book(self, book: Book):
        book_tracks = [
            TrackModel.get_by_id(chapter.id) for chapter in book.chapters
        ]

        data = list((t.file, ) for t in book_tracks)
        chunks = [data[x:x + 500] for x in range(0, len(data), 500)]
        for chunk in chunks:
            StorageBlackList.insert_many(chunk, fields=[StorageBlackList.path
                                                        ]).execute()
Пример #15
0
class BookSearchResult(SearchResult):
    """
    This class represents a book search result.
    """
    _artwork_cache: ArtworkCache = inject.attr(ArtworkCache)

    def __init__(self, book: Book, on_click):
        super().__init__(on_click, book)

        self.set_tooltip_text(_("Play this book"))
        scale = self.get_scale_factor()

        pixbuf = self._artwork_cache.get_cover_pixbuf(book.db_object, scale,
                                                      BOOK_ICON_SIZE)
        if pixbuf:
            surface = Gdk.cairo_surface_create_from_pixbuf(pixbuf, scale, None)
            img = Gtk.Image.new_from_surface(surface)
        else:
            img = Gtk.Image.new_from_icon_name("book-open-variant-symbolic",
                                               Gtk.IconSize.MENU)
            img.props.pixel_size = BOOK_ICON_SIZE
        img.set_size_request(BOOK_ICON_SIZE, BOOK_ICON_SIZE)

        title_label = Gtk.Label()
        title_label.set_text(tools.shorten_string(book.name, MAX_BOOK_LENGTH))
        title_label.set_halign(Gtk.Align.START)
        title_label.props.margin = 4
        title_label.props.hexpand = True
        title_label.props.hexpand_set = True
        title_label.set_margin_right(5)
        title_label.props.width_request = 100
        title_label.props.xalign = 0.0
        title_label.set_line_wrap(True)

        self.box.add(img)
        self.box.add(title_label)
        self.add(self.box)
        self.show_all()
Пример #16
0
class StorageListBoxRow(Gtk.ListBoxRow):
    """
    This class represents a listboxitem for a storage location.
    """
    _library: Library = inject.attr(Library)
    _block_list: StorageBlockList = inject.attr(StorageBlockList)
    _settings: Settings = inject.attr(Settings)

    def __init__(self, parent, db_id, path, external, default=False):
        super(Gtk.ListBoxRow, self).__init__()
        self.ui = cozy.ui.main_view.CozyUI()
        self.db_id = db_id
        self.path = path
        self.default = default
        self.external = external
        self.parent = parent

        box = Gtk.Box()
        box.set_orientation(Gtk.Orientation.HORIZONTAL)
        box.set_spacing(3)
        box.set_halign(Gtk.Align.FILL)
        box.set_valign(Gtk.Align.CENTER)
        box.set_margin_left(4)
        box.set_margin_right(4)
        box.set_margin_top(5)
        box.set_margin_bottom(5)

        self.default_image = Gtk.Image()
        self.default_image.set_from_icon_name("emblem-default-symbolic",
                                              Gtk.IconSize.LARGE_TOOLBAR)
        self.default_image.set_margin_right(5)

        self.type_image = self.__get_type_image()

        self.location_chooser = Gtk.FileChooserButton()
        self.location_chooser.set_local_only(False)
        self.location_chooser.set_action(Gtk.FileChooserAction.SELECT_FOLDER)
        if path != "":
            self.location_chooser.set_current_folder(path)
        self.location_chooser.set_halign(Gtk.Align.START)
        self.location_chooser.props.hexpand = True
        self.location_chooser.connect("file-set", self.__on_folder_changed)

        box.add(self.type_image)
        box.add(self.location_chooser)
        box.add(self.default_image)
        self.add(box)
        self.show_all()
        self.default_image.set_visible(default)

    def set_default(self, default):
        """
        Set this storage location as the default
        :param default: Boolean
        """
        self.default = default
        self.default_image.set_visible(default)
        Storage.update(default=default).where(
            Storage.id == self.db_id).execute()

    def get_default(self):
        """
        Is this storage location the default one?
        """
        return self.default

    def set_selected(self, selected):
        """
        Set UI colors for the default img.
        :param selected: Boolean
        """
        if selected:
            self.default_image.get_style_context().add_class("selected")
        else:
            self.default_image.get_style_context().remove_class("selected")

    def set_external(self, external):
        """
        Set this entry as external/internal storage.
        This method also writes the setting to the cozy.
        """
        self.external = external
        if external:
            self.type_image.set_from_icon_name("network-server-symbolic",
                                               Gtk.IconSize.LARGE_TOOLBAR)
            self.type_image.set_tooltip_text(_("External drive"))
        else:
            self.type_image.set_from_icon_name("drive-harddisk-symbolic",
                                               Gtk.IconSize.LARGE_TOOLBAR)
            self.type_image.set_tooltip_text(_("Internal drive"))

        Storage.update(external=external).where(
            Storage.id == self.db_id).execute()

    def __on_folder_changed(self, widget):
        """
        Update the location in the database.
        Start an import scan or a rebase operation.
        """
        new_path = self.location_chooser.get_file().get_path()
        # First test if the new location is already in the database
        if Storage.select().where(Storage.path == new_path).count() > 0:
            return

        # If not, add it to the database
        old_path = Storage.select().where(Storage.id == self.db_id).get().path
        self.path = new_path
        Storage.update(path=new_path).where(Storage.id == self.db_id).execute()
        self._settings.invalidate()

        # Run a reimport or rebase
        if old_path == "":
            self.parent.emit_event("storage-added", self.path)
            log.info("New audiobook location added. Starting import scan.")
            self.ui.scan(None, None)
        else:
            self.parent.emit_event("storage-changed", self.path)
            log.info(
                "Audio book location changed, rebasing the location in cozy.")
            self.ui.switch_to_working(_("Changing audio book location…"),
                                      False)
            self._block_list.rebase_path(old_path, new_path)
            thread = Thread(target=self._library.rebase_path,
                            args=(old_path, new_path),
                            name="RebaseStorageLocationThread")
            thread.start()

    def __get_type_image(self):
        """
        Returns the matching drive icon for this storage location.
        :return: External or internal drive gtk image.
        """
        type_image = Gtk.Image()
        if self.external:
            icon_name = "network-server-symbolic"
            type_image.set_tooltip_text(_("External drive"))
        else:
            icon_name = "drive-harddisk-symbolic"
            type_image.set_tooltip_text(_("Internal drive"))
        type_image.set_from_icon_name(icon_name, Gtk.IconSize.LARGE_TOOLBAR)
        type_image.set_margin_right(5)

        return type_image
Пример #17
0
class Library(EventSender):
    _db = cache = inject.attr(SqliteDatabase)

    _books: List[Book] = []
    _chapters: Set[Chapter] = set()
    _files: Set[str] = set()

    def __init__(self):
        super().__init__()

    @property
    def authors(self):
        authors = {book.author for book in self.books}
        authors = split_strings_to_set(authors)
        return authors

    @property
    def readers(self):
        readers = {book.reader for book in self.books}
        readers = split_strings_to_set(readers)
        return readers

    @property
    def books(self):
        if not self._books:
            self._load_all_books()

        return self._books

    @property
    def chapters(self) -> Set[Chapter]:
        if not self._chapters:
            self._load_all_chapters()

        return self._chapters

    @property
    def files(self) -> Set[str]:
        if not self._files:
            self._load_all_files()

        return self._files

    def invalidate(self):
        for book in self._books:
            book.destroy()

        self._books = []

        for chapter in self._chapters:
            chapter.destroy()

        self._chapters = set()
        self._files = set()

    @timing
    def rebase_path(self, old_path: str, new_path: str):
        chapter_count = len(self.chapters)
        progress = 0
        with self._db:
            for chapter in self.chapters:
                if chapter.file.startswith(old_path):
                    progress += 1
                    chapter.file = chapter.file.replace(old_path, new_path)
                    self.emit_event_main_thread("rebase-progress",
                                                progress / chapter_count)

        self.emit_event_main_thread("rebase-finished")

    def insert_many(self, media_files: Set[MediaFile]):
        tracks = self._prepare_db_objects(media_files)

        with self._db:
            Track.insert_many(tracks).execute()

    def _prepare_db_objects(self, media_files: Set[MediaFile]) -> Set[object]:
        book_db_objects: Set[BookModel] = set()

        for media_file in media_files:
            if not media_file:
                continue

            with self._db:
                book = next((book for book in book_db_objects
                             if book.name == media_file.book_name), None)

                if not book:
                    book = self._import_or_update_book(media_file)
                    book_db_objects.add(book)

                if len(media_file.chapters) == 1:
                    track = self._get_track_dictionary_for_db(media_file, book)
                else:
                    raise NotImplementedError

                if media_file.path not in self.files:
                    yield track
                else:
                    self._update_track_db_object(media_file, book)

    def _import_or_update_book(self, media_file):
        if BookModel.select(BookModel.name).where(
                BookModel.name == media_file.book_name).count() < 1:
            book = self._create_book_db_object(media_file)
        else:
            book = self._update_book_db_object(media_file)
        return book

    def _get_track_dictionary_for_db(self, media_file: MediaFile,
                                     book: BookModel):
        return {
            "name": media_file.chapters[0].name,
            "number": media_file.track_number,
            "disk": media_file.disk,
            "book": book,
            "file": media_file.path,
            "length": media_file.length,
            "modified": media_file.modified,
            "position": media_file.chapters[0].position
        }

    def _update_track_db_object(self, media_file: MediaFile, book: BookModel):
        Track.update(name=media_file.chapters[0].name,
                     number=media_file.track_number,
                     book=book,
                     disk=media_file.disk,
                     length=media_file.length,
                     modified=media_file.modified) \
            .where(Track.file == media_file.path) \
            .execute()

    def _update_book_db_object(self, media_file: MediaFile) -> BookModel:
        BookModel.update(name=media_file.book_name,
                         author=media_file.author,
                         reader=media_file.reader,
                         cover=media_file.cover) \
            .where(BookModel.name == media_file.book_name) \
            .execute()
        return BookModel.select().where(
            BookModel.name == media_file.book_name).get()

    def _create_book_db_object(self, media_file: MediaFile) -> BookModel:
        return BookModel.create(name=media_file.book_name,
                                author=media_file.author,
                                reader=media_file.reader,
                                cover=media_file.cover,
                                position=0,
                                rating=-1)

    def _load_all_books(self):
        with self._db:
            for book_db_obj in BookModel.select(BookModel.id):
                try:
                    book = Book(self._db, book_db_obj.id)
                    book.add_listener(self._on_book_event)
                    self._books.append(book)
                except BookIsEmpty:
                    pass

    def _load_all_chapters(self):
        self._chapters = {
            chapter
            for book_chapters in [book.chapters for book in self.books]
            for chapter in book_chapters
        }

        for chapter in self._chapters:
            chapter.add_listener(self._on_chapter_event)

    def _load_all_files(self):
        self._files = {chapter.file for chapter in self.chapters}

    def _on_chapter_event(self, event: str, chapter: Chapter):
        if event == "chapter-deleted":
            try:
                self.chapters.remove(chapter)
            except KeyError:
                log.error(
                    "Could not remove chapter from library chapter list.")

            try:
                self.files.remove(chapter.file)
            except KeyError:
                log.error("Could not remove file from library file list.")
                self._files = []

    def _on_book_event(self, event: str, book):
        if event == "book-deleted":
            self.books.remove(book)
class PlaybackControlViewModel(Observable, EventSender):
    _player: Player = inject.attr(Player)

    def __init__(self):
        super().__init__()
        super(Observable, self).__init__()

        self._book: Optional[Book] = None

        self._player.add_listener(self._on_player_event)

        if self._player.loaded_book:
            self.book = self._player.loaded_book

    @property
    def book(self) -> Optional[Book]:
        return self._book

    @book.setter
    def book(self, value: Optional[Book]):
        if self._book:
            self._book.remove_bind("playback_speed",
                                   self._on_playback_speed_changed)

        self._book = value
        if value:
            self._book.bind_to("playback_speed",
                               self._on_playback_speed_changed)

        self._notify("lock_ui")

    @property
    def playing(self) -> bool:
        if not self._player.loaded_book:
            return False

        return self._player.playing

    @property
    def position(self) -> Optional[float]:
        if not self._book:
            return None

        return self._book.current_chapter.position / 1000000000 / self._book.playback_speed

    @position.setter
    def position(self, new_value: int):
        self._player.position = new_value

    @property
    def length(self) -> Optional[float]:
        if not self._player.loaded_book or not self._book:
            return None

        return self._player.loaded_book.current_chapter.length / self._book.playback_speed

    @property
    def lock_ui(self) -> bool:
        return not self._book

    @property
    def volume(self) -> float:
        return self._player.volume

    @volume.setter
    def volume(self, new_value: float):
        self._player.volume = new_value

    def play_pause(self):
        self._player.play_pause()

    def rewind(self):
        self._player.rewind()

    def open_book_detail(self):
        if self.book:
            self.emit_event(OpenView.BOOK, self.book)

    def _on_player_event(self, event, message):
        if event == "play" or event == "pause":
            if self.book:
                self._notify("playing")
        elif event == "position":
            if self.book:
                self._notify("position")
                self._notify("progress_percent")
                self._notify("remaining_text")
        elif event == "track-changed":
            if message:
                self.book = message
                self._notify("book")
                self._notify("position")
                self._notify("length")
                self._notify("volume")
        elif event == "stop":
            self.book = None
            self._notify("book")
            self._notify("position")
            self._notify("length")

    def _on_playback_speed_changed(self):
        self._notify("position")
        self._notify("length")
Пример #19
0
class BookOverview:
    """
    This class contains all logic for the book overview.
    """
    _settings = inject.attr(Settings)
    _artwork_cache: ArtworkCache = inject.attr(ArtworkCache)

    book = None
    current_track_element = None
    switch_signal = None

    def __init__(self):
        self.ui = cozy.ui.main_view.CozyUI()
        builder = self.ui.window_builder
        self.name_label = builder.get_object("info_book_label")
        self.author_label = builder.get_object("info_author_label")
        self.download_box = builder.get_object("info_download_box")
        self.download_label = builder.get_object("info_download_label")
        self.download_image = builder.get_object("info_download_image")
        self.download_switch = builder.get_object("info_download_switch")
        self.published_label = builder.get_object("info_published_label")
        self.last_played_label = builder.get_object("info_last_played_label")
        self.total_label = builder.get_object("info_total_label")
        self.remaining_label = builder.get_object("info_remaining_label")
        self.progress_bar = builder.get_object("book_progress_bar")
        self.cover_img = builder.get_object("info_cover_image")
        self.track_list_container = builder.get_object("track_list_container")
        self.published_text = builder.get_object("info_published_text")
        self.remaining_text = builder.get_object("info_remaining_text")
        self.play_book_button = builder.get_object("play_book_button")
        self.play_book_button.connect("clicked", self.__on_play_clicked)
        self.play_img = builder.get_object("play_img1")
        self.pause_img = builder.get_object("pause_img1")
        self.scroller = builder.get_object("book_overview_scroller")
        if Gtk.get_minor_version() > 20:
            self.scroller.props.propagate_natural_height = True

        self.ui.speed.add_listener(self.__ui_changed)
        player.add_player_listener(self.__player_changed)
        self._settings.add_listener(self.__settings_changed)
        OfflineCache().add_listener(self.__on_offline_cache_changed)

    def set_book(self, book):
        """
        Display the given book in the book overview.
        """
        if self.book and self.book.id == book.id:
            self.update_time()
            return
        self.book = Book.get(Book.id == book.id)

        if player.is_playing(
        ) and self.ui.titlebar.current_book and self.book.id == self.ui.titlebar.current_book.id:
            self.play_book_button.set_image(self.pause_img)
        else:
            self.play_book_button.set_image(self.play_img)

        self.name_label.set_text(book.name)
        self.author_label.set_text(book.author)

        self.update_offline_status()

        pixbuf = self._artwork_cache.get_cover_pixbuf(
            book, self.ui.window.get_scale_factor(), 250)
        if pixbuf:
            surface = Gdk.cairo_surface_create_from_pixbuf(
                pixbuf, self.ui.window.get_scale_factor(), None)
            self.cover_img.set_from_surface(surface)
        else:
            self.cover_img.set_from_icon_name("book-open-variant-symbolic",
                                              Gtk.IconSize.DIALOG)
            self.cover_img.props.pixel_size = 250

        self.duration = get_book_duration(book)
        self.speed = self.book.playback_speed
        self.total_label.set_text(
            tools.seconds_to_human_readable(self.duration / self.speed))

        self.last_played_label.set_text(
            tools.past_date_to_human_readable(book.last_played))

        self.published_label.set_visible(False)
        self.published_text.set_visible(False)

        # track list
        # This box contains all track content
        self.track_box = Gtk.Box()
        self.track_box.set_orientation(Gtk.Orientation.VERTICAL)
        self.track_box.set_halign(Gtk.Align.START)
        self.track_box.set_valign(Gtk.Align.START)
        self.track_box.props.margin = 8

        disk_number = -1
        first_disk_element = None
        disk_count = 0

        for track in get_tracks(book):
            # Insert disk headers
            if track.disk != disk_number:
                disc_element = DiskElement(track.disk)
                self.track_box.add(disc_element)
                if disk_number == -1:
                    first_disk_element = disc_element
                    if track.disk < 2:
                        first_disk_element.set_hidden(True)
                else:
                    first_disk_element.show_all()
                    disc_element.show_all()

                disk_number = track.disk
                disk_count += 1

            track_element = TrackElement(track, self)
            self.track_box.add(track_element)
            track_element.show_all()

        self.track_list_container.remove_all_children()
        self.track_box.show()
        self.track_box.set_halign(Gtk.Align.FILL)
        self.track_list_container.add(self.track_box)

        self._mark_current_track()
        self.update_time()

    def update_offline_status(self):
        """
        Hide/Show download elements depending on whether the book is on an external storage.
        """
        self.book = Book.get(Book.id == self.book.id)
        if self.switch_signal:
            self.download_switch.disconnect(self.switch_signal)
        if is_external(self.book):
            self.download_box.set_visible(True)
            self.download_switch.set_visible(True)
            self.download_switch.set_active(self.book.offline)
        else:
            self.download_box.set_visible(False)
            self.download_switch.set_visible(False)
        self.switch_signal = self.download_switch.connect(
            "notify::active", self.__on_download_switch_changed)

        self._set_book_download_status(self.book.downloaded)

    def update_time(self):
        if self.book is None:
            return

        # update book object
        # TODO: optimize usage by only asking from the db on track change
        query = Book.select().where(Book.id == self.book.id)
        if (query.exists()):
            self.book = query.get()
        else:
            self.book = None
            return
        if self.ui.titlebar.current_book and self.book.id == self.ui.titlebar.current_book.id:
            progress = get_book_progress(self.book, False)
            progress += (player.get_current_duration() / 1000000000)
            remaining = (self.duration - progress)
        else:
            progress = get_book_progress(self.book)
            remaining = get_book_remaining(self.book)
        percentage = progress / self.duration

        self.total_label.set_text(
            tools.seconds_to_human_readable(self.duration / self.speed))

        if percentage > 0.005:
            self.remaining_text.set_visible(True)
            self.remaining_label.set_visible(True)
            self.remaining_label.set_text(
                tools.seconds_to_human_readable(remaining / self.speed))
        else:
            self.remaining_text.set_visible(False)
            self.remaining_label.set_visible(False)

        self.progress_bar.set_fraction(percentage)

    def _set_active_track(self, curr_track, playing):
        if self.book is None or self.ui.titlebar.current_book.id != self.book.id:
            return

        self.deselect_track_element()

        if self.book.position == -1:
            return

        track_box_children = [
            e for e in self.track_box.get_children()
            if isinstance(e, TrackElement)
        ]
        if curr_track:
            self.current_track_element = next(
                filter(lambda x: x.track.id == curr_track.id,
                       track_box_children), None)

        if self.current_track_element is None:
            self.current_track_element = track_box_children[0]

        self.current_track_element.select()
        self.current_track_element.set_playing(playing)

    def deselect_track_element(self):
        if self.current_track_element:
            self.current_track_element.set_playing(False)
            self.current_track_element.deselect()

    def block_ui_elements(self, block):
        """
        Blocks the download button. This gets called when a db scan is active.
        """
        self.download_switch.set_sensitive(not block)

    def _set_book_download_status(self, downloaded):
        if downloaded:
            self.download_image.set_from_icon_name("downloaded-symbolic",
                                                   Gtk.IconSize.LARGE_TOOLBAR)
            self.download_label.set_text(_("Downloaded"))
        else:
            self.download_image.set_from_icon_name("download-symbolic",
                                                   Gtk.IconSize.LARGE_TOOLBAR)
            self.download_label.set_text(_("Download"))

    def _mark_current_track(self):
        """
        Mark the current track position.
        """
        book = Book.get(Book.id == self.book.id)

        if book.position == -1:
            self.deselect_track_element()
            self.current_track_element = None
            return

        for track_element in self.track_box.get_children():
            if isinstance(track_element, DiskElement):
                continue
            elif track_element.track.id == book.position:
                self.current_track_element = track_element
                track_element.select()
            else:
                track_element.deselect()

        if book.position == 0:
            self.current_track_element = self.track_box.get_children()[1]
            self.current_track_element.select()

        if self.ui.titlebar.current_book and self.ui.titlebar.current_book.id == self.book.id:
            self.current_track_element.set_playing(player.is_playing())

    def __ui_changed(self, event, message):
        """
        Handler for events that occur in the main ui.
        """
        if self.book is None or self.ui.titlebar.current_book is None or self.ui.titlebar.current_book.id != self.book.id:
            return

        if event == "playback-speed-changed":
            self.speed = Book.select().where(
                Book.id == self.book.id).get().playback_speed
            if self.ui.main_stack.props.visible_child_name == "book_overview":
                self.update_time()

    def __player_changed(self, event, message):
        """
        React to player changes.
        """
        if self.book is None or self.ui.titlebar.current_book is None or self.ui.titlebar.current_book.id != self.book.id:
            return

        if event == "play":
            self.play_book_button.set_image(self.pause_img)
            self.current_track_element.set_playing(True)
            self.last_played_label.set_text(
                tools.past_date_to_human_readable(message.book.last_played))
        elif event == "pause":
            self.play_book_button.set_image(self.play_img)
            self.current_track_element.set_playing(False)
        elif event == "stop":
            self._mark_current_track()
        elif event == "track-changed":
            track = player.get_current_track()
            self._set_active_track(track, player.is_playing())

    def __settings_changed(self, event, message):
        """
        React to changes in user settings.
        """
        if not self.book:
            return

        if event == "storage-removed" or event == "external-storage-removed":
            if message in get_tracks(self.book).first().file:
                self.download_box.set_visible(False)
                self.download_switch.set_visible(False)
        elif "external-storage-added" or event == "storage-changed" or event == "storage-added":
            self.update_offline_status()

    def __on_play_clicked(self, event):
        """
        Play button clicked.
        Start/pause playback.
        """
        track = get_track_for_playback(self.book)
        current_track = player.get_current_track()

        if current_track and current_track.book.id == self.book.id:
            player.play_pause(None)
            if player.get_gst_player_state() == Gst.State.PLAYING:
                player.jump_to_ns(track.position)
        else:
            player.load_file(track)
            player.play_pause(None, True)

        return True

    def __on_download_switch_changed(self, switch, state):
        if self.download_switch.get_active():
            Book.update(offline=True).where(Book.id == self.book.id).execute()
            OfflineCache().add(self.book)
        else:
            Book.update(
                offline=False,
                downloaded=False).where(Book.id == self.book.id).execute()
            OfflineCache().remove(self.book)
            self._set_book_download_status(False)

    def __on_offline_cache_changed(self, event, message):
        """
        """
        if message is Book and message.id != self.book.id:
            return

        if event == "book-offline":
            self._set_book_download_status(True)
        elif event == "book-offline-removed":
            self._set_book_download_status(False)
Пример #20
0
class Settings(EventSender):
    """
    This class contains all logic for cozys preferences.
    """
    _glib_settings: Gio.Settings = inject.attr(Gio.Settings)

    view_model = None
    ui = None
    default_dark_mode = None

    def __init__(self):
        super().__init__()
        from cozy.control.artwork_cache import ArtworkCache
        self._artwork_cache: ArtworkCache = inject.instance(ArtworkCache)

        self.view_model = SettingsViewModel()

        self.ui = cozy.ui.main_view.CozyUI()
        self.builder = Gtk.Builder.new_from_resource(
            "/com/github/geigi/cozy/settings.ui")

        # get settings window
        self.window = self.builder.get_object("settings_window")
        self.window.set_transient_for(self.ui.window)
        self.window.connect("delete-event", self.ui.hide_window)

        self.add_storage_button = self.builder.get_object("add_location_button")
        self.add_storage_button.connect("clicked", self.__on_add_storage_clicked)
        self.remove_storage_button = self.builder.get_object("remove_location_button")
        self.remove_storage_button.connect("clicked", self.__on_remove_storage_clicked)
        self.external_button = self.builder.get_object("external_button")
        self.external_button_handle_id = self.external_button.connect("clicked", self.__on_external_clicked)
        self.default_storage_button = self.builder.get_object("default_location_button")
        self.default_storage_button.connect("clicked", self.__on_default_storage_clicked)
        self.storage_list_box = self.builder.get_object("storage_list_box")
        self.storage_list_box.connect("row-activated", self.__on_storage_box_changed)

        self.remove_blacklist_button = self.builder.get_object("remove_blacklist_button")
        self.remove_blacklist_button.connect("clicked", self.__on_remove_blacklist_clicked)
        # self.remove_blacklist_button.set_sensitive(False)
        self.blacklist_tree_view = self.builder.get_object("blacklist_tree_view")
        self.blacklist_model = self.builder.get_object("blacklist_store")
        self.blacklist_tree_view.get_selection().connect("changed", self.__on_blacklist_selection_changed)

        self.sleep_fadeout_switch = self.builder.get_object("sleep_fadeout_switch")
        self.sleep_fadeout_switch.connect("notify::active", self.__on_fadeout_switch_changed)

        self.external_cover_switch = self.builder.get_object("external_cover_switch")
        self.external_cover_switch.connect("state-set", self.__on_external_cover_switch_changed)

        self.fadeout_duration_label = self.builder.get_object("fadeout_duration_label")
        self.fadeout_duration_row = self.builder.get_object("fadeout_duration_row")

        self.fadeout_duration_adjustment = self.builder.get_object("fadeout_duration_adjustment")
        self.fadeout_duration_adjustment.connect("value-changed", self.__on_fadeout_adjustment_changed)
        self.__on_fadeout_adjustment_changed(self.fadeout_duration_adjustment)

        self.force_refresh_button = self.builder.get_object("force_refresh_button")
        self.force_refresh_button.connect("clicked", self.__on_force_refresh_clicked)

        self.settings_stack: Gtk.Stack = self.builder.get_object("settings_stack")
        self.settings_stack.connect("notify::visible-child", self._on_settings_stack_changed)

        from cozy.ui.widgets.error_reporting import ErrorReporting
        self.settings_stack.add_titled(ScrollWrapper(ErrorReporting()), "feedback", _("Feedback"))

        self._init_storage()
        self._init_blacklist()
        self.__init_bindings()
        self.__on_storage_box_changed(None, None)

        self.set_darkmode()

    def _init_storage(self):
        """
        Display settings from the database in the ui.
        """
        found_default = False
        self.storage_list_box.remove_all_children()
        for location in Storage.select():
            row = StorageListBoxRow(self, location.id, location.path, location.external, location.default)
            self.storage_list_box.add(row)
            if location.default:
                if found_default:
                    row.set_default(False)
                else:
                    found_default = True
                    self.storage_list_box.select_row(row)

        self.__on_storage_box_changed(None, None)

    def _init_blacklist(self):
        """
        Init the Storage location list.
        """
        for file in StorageBlackList.select():
            self.blacklist_model.append([file.path, file.id])
        self.__on_blacklist_selection_changed(None)

    def __init_bindings(self):
        """
        Bind Gio.Settings to widgets in settings dialog.
        """
        sl_switch = self.builder.get_object("symlinks_switch")
        self._glib_settings.bind("symlinks", sl_switch, "active",
                                 Gio.SettingsBindFlags.DEFAULT)

        auto_scan_switch = self.builder.get_object("auto_scan_switch")
        self._glib_settings.bind("autoscan", auto_scan_switch,
                                 "active", Gio.SettingsBindFlags.DEFAULT)

        timer_suspend_switch = self.builder.get_object(
            "timer_suspend_switch")
        self._glib_settings.bind("suspend", timer_suspend_switch,
                                 "active", Gio.SettingsBindFlags.DEFAULT)

        replay_switch = self.builder.get_object("replay_switch")
        self._glib_settings.bind("replay", replay_switch, "active",
                                 Gio.SettingsBindFlags.DEFAULT)

        titlebar_remaining_time_switch = self.builder.get_object("titlebar_remaining_time_switch")
        self._glib_settings.bind("titlebar-remaining-time", titlebar_remaining_time_switch, "active",
                                 Gio.SettingsBindFlags.DEFAULT)

        dark_mode_switch = self.builder.get_object("dark_mode_switch")
        self._glib_settings.bind("dark-mode", dark_mode_switch, "active",
                                 Gio.SettingsBindFlags.DEFAULT)

        swap_author_reader_switch = self.builder.get_object("swap_author_reader_switch")
        self._glib_settings.bind("swap-author-reader", swap_author_reader_switch, "active",
                                 Gio.SettingsBindFlags.DEFAULT)

        self._glib_settings.bind("prefer-external-cover", self.external_cover_switch, "active",
                                 Gio.SettingsBindFlags.DEFAULT)

        self._glib_settings.bind("sleep-timer-fadeout", self.sleep_fadeout_switch, "active",
                                 Gio.SettingsBindFlags.DEFAULT)

        self._glib_settings.bind("sleep-timer-fadeout-duration", self.fadeout_duration_adjustment,
                                 "value", Gio.SettingsBindFlags.DEFAULT)

        self._glib_settings.connect("changed", self.__on_settings_changed)

    def show(self):
        """
        Show the settings window.
        """
        self.window.show()

    def get_storage_elements_blocked(self):
        """
        Are the location ui elements currently blocked?
        """
        return not self.storage_list_box.get_sensitive()

    def block_ui_elements(self, block):
        """
        Block/unblock UI storage elements.
        """
        sensitive = not block
        self.storage_list_box.set_sensitive(sensitive)
        self.add_storage_button.set_sensitive(sensitive)
        self.external_button.set_sensitive(sensitive)

        row = self.storage_list_box.get_selected_row()
        if row and row.get_default() != True:
            self.remove_storage_button.set_sensitive(sensitive)
            self.default_storage_button.set_sensitive(sensitive)

    def __on_add_storage_clicked(self, widget):
        """
        Add a new storage selector to the ui.
        """
        db_obj = Storage.create(path="")
        self.storage_list_box.add(StorageListBoxRow(self, db_obj.id, "", False, False))

    def __on_remove_storage_clicked(self, widget):
        """
        Remove a storage selector from the ui and database.
        """
        row = self.storage_list_box.get_selected_row()
        Storage.select().where(Storage.path == row.path).get().delete_instance()
        self.storage_list_box.remove(row)
        self.emit_event("storage-removed", row.path)
        thread = Thread(target=remove_tracks_with_path, args=(self.ui, row.path), name=("RemoveStorageFromDB"))
        thread.start()
        self.__on_storage_box_changed(None, None)

    def __on_default_storage_clicked(self, widget):
        """
        Select a location as default storage.
        """
        for row in self.storage_list_box.get_children():
            row.set_default(False)

        self.storage_list_box.get_selected_row().set_default(True)

        self.__on_storage_box_changed(None, None)

    def __on_storage_box_changed(self, widget, row):
        """
        Disable/enable toolbar buttons
        """
        row = self.storage_list_box.get_selected_row()
        if row is None:
            sensitive = False
            default_sensitive = False
        else:
            sensitive = True
            if row.get_default():
                default_sensitive = False
            else:
                default_sensitive = True
            self.external_button.handler_block(self.external_button_handle_id)
            self.external_button.set_active(row.external)
            self.external_button.handler_unblock(self.external_button_handle_id)

        self.remove_storage_button.set_sensitive(default_sensitive)
        self.external_button.set_sensitive(sensitive)
        self.default_storage_button.set_sensitive(default_sensitive)

        for child in self.storage_list_box.get_children():
            if row and child.db_id == row.db_id:
                child.set_selected(True)
            else:
                child.set_selected(False)

    def __on_settings_changed(self, settings, key):
        """
        Updates cozy's ui to changed Gio settings.
        """
        if key == "titlebar-remaining-time":
            self.ui.titlebar._on_progress_setting_changed()
        elif key == "dark-mode":
            self.set_darkmode()

    def __on_remove_blacklist_clicked(self, widget):
        """
        Remove the selected storage from the db and ui.
        TODO: Does this trigger a rescan?
        """
        model, pathlist = self.blacklist_tree_view.get_selection().get_selected_rows()
        if pathlist:
            ids = []
            for path in reversed(pathlist):
                treeiter = model.get_iter(path)
                ids.append(self.blacklist_model.get_value(treeiter, 1))
                self.blacklist_model.remove(treeiter)

            StorageBlackList.delete().where(StorageBlackList.id in ids).execute()

        self.__on_blacklist_selection_changed(self.blacklist_tree_view.get_selection())

    def __on_blacklist_selection_changed(self, tree_selection):
        """
        The user selected a different storage location.
        Here we enable or disable the remove button depending on 
        weather this is the default storage or not.
        """
        if tree_selection is None or len(tree_selection.get_selected_rows()[1]) < 1:
            self.remove_blacklist_button.set_sensitive(False)
        else:
            self.remove_blacklist_button.set_sensitive(True)

    def __on_external_clicked(self, widget):
        """
        The external/internal button was clicked.
        The new setting will be written to the cozy.
        """
        external = self.external_button.get_active()

        row = self.storage_list_box.get_selected_row()
        row.set_external(external)

        if external:
            self.emit_event("external-storage-added", row.path)
        else:
            self.emit_event("external-storage-removed", row.path)

    def __on_fadeout_adjustment_changed(self, adjustment):
        """
        This refreshes the label belonging to the fadeout duration adjustment.
        """
        self.fadeout_duration_label.set_text(str(int(adjustment.get_value())) + " s")

    def __on_fadeout_switch_changed(self, switch, state):
        """
        Enable/Disable sensitivity for the fadeout duration settings row.
        """
        self.fadeout_duration_row.set_sensitive(switch.get_active())

    def __on_external_cover_switch_changed(self, switch, state):
        """
        Set the glib setting prefer-external-cover.
        This is needed because the binding gets called after this function.
        Then refresh the artwork cache.
        """
        # We have to test if everything is initialized before triggering the refresh
        # otherwise this might be just the initial call when starting up
        if self.ui.is_initialized:
            self._glib_settings.set_boolean("prefer-external-cover", state)
            self._artwork_cache.delete_artwork_cache()
            self.ui.refresh_content()

    def __on_force_refresh_clicked(self, widget):
        """
        Start a force refresh of the database.
        """
        self.ui.scan(None, None)

    def _on_settings_stack_changed(self, widget, property):
        page = widget.props.visible_child_name

        if page == "files":
            self.blacklist_model.clear()
            self._init_blacklist()

    def set_darkmode(self):
        """
        Enable or disable the dark gtk theme.
        """
        settings = Gtk.Settings.get_default()

        if self.default_dark_mode is None:
            self.default_dark_mode = settings.get_property("gtk-application-prefer-dark-theme")

        user_enabled = self._glib_settings.get_boolean("dark-mode")
        if user_enabled:
            settings.set_property("gtk-application-prefer-dark-theme", True)
        else:
            settings.set_property("gtk-application-prefer-dark-theme", self.default_dark_mode)
Пример #21
0
class Book(Observable, EventSender):
    _chapters: List[Chapter] = None
    _settings: Settings = inject.attr(Settings)
    _app_settings: ApplicationSettings = inject.attr(ApplicationSettings)

    def __init__(self, db: SqliteDatabase, id: int):
        super().__init__()
        super(Observable, self).__init__()

        self._db: SqliteDatabase = db
        self.id: int = id

        self._get_db_object()

    def _get_db_object(self):
        self._db_object: BookModel = BookModel.get(self.id)

        if TrackModel.select().where(
                TrackModel.book == self._db_object).count() < 1:
            raise BookIsEmpty

    # This property is for the transition time only
    # Because everything is hardwired to the database objects
    # Step by step, you got this...
    @property
    def db_object(self):
        return self._db_object

    @property
    def name(self):
        return self._db_object.name

    @name.setter
    def name(self, new_name: str):
        self._db_object.name = new_name
        self._db_object.save(only=self._db_object.dirty_fields)

    @property
    def author(self):
        if not self._app_settings.swap_author_reader:
            return self._db_object.author
        else:
            return self._db_object.reader

    @author.setter
    def author(self, new_author: str):
        if not self._app_settings.swap_author_reader:
            self._db_object.author = new_author
        else:
            self._db_object.reader = new_author

        self._db_object.save(only=self._db_object.dirty_fields)

    @property
    def reader(self):
        if not self._app_settings.swap_author_reader:
            return self._db_object.reader
        else:
            return self._db_object.author

    @reader.setter
    def reader(self, new_reader: str):
        if not self._app_settings.swap_author_reader:
            self._db_object.reader = new_reader
        else:
            self._db_object.author = new_reader

        self._db_object.save(only=self._db_object.dirty_fields)

    @property
    def position(self) -> int:
        return self._db_object.position

    @position.setter
    def position(self, new_position: int):
        self._db_object.position = new_position
        self._db_object.save(only=self._db_object.dirty_fields)
        self._notify("position")
        self._notify("current_chapter")

    @property
    def rating(self):
        return self._db_object.rating

    @rating.setter
    def rating(self, new_rating: int):
        self._db_object.rating = new_rating
        self._db_object.save(only=self._db_object.dirty_fields)

    @property
    def cover(self):
        return self._db_object.cover

    @cover.setter
    def cover(self, new_cover: bytes):
        self._db_object.cover = new_cover
        self._db_object.save(only=self._db_object.dirty_fields)

    @property
    def playback_speed(self):
        return self._db_object.playback_speed

    @playback_speed.setter
    def playback_speed(self, new_playback_speed: float):
        self._db_object.playback_speed = new_playback_speed
        self._db_object.save(only=self._db_object.dirty_fields)
        self._notify("playback_speed")

    @property
    def last_played(self):
        return self._db_object.last_played

    @last_played.setter
    def last_played(self, new_last_played: int):
        self._db_object.last_played = new_last_played
        self._db_object.save(only=self._db_object.dirty_fields)
        self._notify("last_played")

    @property
    def offline(self):
        return self._db_object.offline

    @offline.setter
    def offline(self, new_offline: bool):
        self._db_object.offline = new_offline
        self._db_object.save(only=self._db_object.dirty_fields)

    @property
    def downloaded(self):
        return self._db_object.downloaded

    @downloaded.setter
    def downloaded(self, new_downloaded: bool):
        self._db_object.downloaded = new_downloaded
        self._db_object.save(only=self._db_object.dirty_fields)

    @property
    def chapters(self):
        if not self._chapters:
            self._fetch_chapters()

        return self._chapters

    @property
    def current_chapter(self):
        return next(
            (chapter
             for chapter in self.chapters if chapter.id == self.position),
            self.chapters[0])

    @property
    def duration(self):
        return sum((chapter.length for chapter in self.chapters))

    @property
    def progress(self):
        progress = 0

        if self.position == 0:
            return 0
        elif self.position == -1:
            return self.duration

        for chapter in self.chapters:
            if chapter.id == self.position:
                relative_position = max(
                    chapter.position - chapter.start_position, 0)
                progress += int(relative_position / 1000000000)
                return progress

            progress += chapter.length

        return progress

    def remove(self):
        if self._settings.last_played_book and self._settings.last_played_book.id == self._db_object.id:
            self._settings.last_played_book = None

        book_tracks = [
            TrackModel.get_by_id(chapter.id) for chapter in self.chapters
        ]
        track_to_files = TrackToFile.select().join(TrackModel).where(
            TrackToFile.track << book_tracks)

        for track in track_to_files:
            try:
                track.file.delete_instance(recursive=True)
            except DoesNotExist:
                track.delete_instance()

        for track in book_tracks:
            track.delete_instance(recursive=True)

        self._db_object.delete_instance(recursive=True)
        self.destroy_listeners()
        self._destroy_observers()

    def _fetch_chapters(self):
        tracks = TrackModel \
            .select(TrackModel.id) \
            .where(TrackModel.book == self._db_object) \
            .order_by(TrackModel.disk, TrackModel.number, TrackModel.name)

        self._chapters = []
        for track in tracks:
            try:
                track_model = Track(self._db, track.id)
                self._chapters.append(track_model)
            except TrackInconsistentData:
                log.warning("Skipping inconsistent model")
            except Exception as e:
                log.error("Could not create chapter object: {}".format(e))

        for chapter in self._chapters:
            chapter.add_listener(self._on_chapter_event)

    def _on_chapter_event(self, event: str, chapter: Chapter):
        if event == "chapter-deleted":
            try:
                self.chapters.remove(chapter)
            except ValueError:
                pass

            if len(self._chapters) < 1:
                if self._settings.last_played_book and self._settings.last_played_book.id == self._db_object.id:
                    self._settings.last_played_book = None

                self._db_object.delete_instance(recursive=True)
                self.emit_event("book-deleted", self)
                self.destroy_listeners()
                self._destroy_observers()
Пример #22
0
class FilesystemMonitor(EventSender):
    external_storage: List[ExternalStorage] = []
    _settings: Settings = inject.attr(Settings)

    def __init__(self):
        super().__init__()
        self.volume_monitor: Gio.VolumeMonitor = Gio.VolumeMonitor.get()
        self.volume_monitor.connect("mount-added", self.__on_mount_added)
        self.volume_monitor.connect("mount-removed", self.__on_mount_removed)

        self.init_offline_mode()

        from cozy.ui.settings import Settings as UISettings
        self._ui_settings = inject.instance(UISettings)
        self._ui_settings.add_listener(self.__on_settings_changed)

    def init_offline_mode(self):
        external_storage = []
        mounts = self.volume_monitor.get_mounts()
        # go through all audiobook locations and test if they can be found in the mounts list

        for storage in self._settings.external_storage_locations:
            online = False
            if any(mount.get_root().get_path() in storage.path
                   for mount in mounts):
                online = True
            self.external_storage.append(
                ExternalStorage(storage=storage, online=online))

    def close(self):
        """
        Free all references.
        """
        # self.volume_monitor.unref()
        pass

    def get_book_online(self, book: Book):
        result = next((storage.online for storage in self.external_storage
                       if storage.storage.path in book.chapters[0].file), True)
        return result

    def is_track_online(self, track):
        """
        """
        result = next((storage.online for storage in self.external_storage
                       if storage.storage.path in track.file), True)
        return (result)

    def get_offline_storages(self):
        return [i.storage.path for i in self.external_storage if not i.online]

    def is_storage_online(self, storage: Storage) -> bool:
        storage = next((storage for storage in self.external_storage
                        if storage.storage == storage), None)

        if not storage:
            raise StorageNotFound

        return storage.online

    def is_external(self, directory: str) -> bool:
        mounts: List[Gio.Mount] = self.volume_monitor.get_mounts()

        for mount in mounts:
            root = mount.get_root()
            if not root:
                log.error(
                    "Failed to test for external drive. Mountpoint has no root object."
                )
                reporter.error(
                    "fs_monitor",
                    "Failed to test for external drive. Mountpoint has no root object."
                )
                return False

            path = root.get_path()
            if not path:
                log.error(
                    "Failed to test for external drive. Root object has no path."
                )
                reporter.error(
                    "fs_monitor",
                    "Failed to test for external drive. Root object has no path."
                )
                return False

            if path in directory and mount.can_unmount():
                return True

        return False

    def __on_mount_added(self, monitor, mount):
        """
        A volume was mounted.
        Disable offline mode for this volume.
        """
        mount_path = mount.get_root().get_path()

        if not mount_path:
            log.warning(
                "Mount added but no mount_path is present. Skipping...")
            return

        log.debug("Volume mounted: " + mount_path)

        storage = next(
            (s for s in self.external_storage if mount_path in s.storage.path),
            None)
        if storage:
            log.info("Storage online: " + mount_path)
            storage.online = True
            self.emit_event("storage-online", storage.storage.path)

    def __on_mount_removed(self, monitor, mount):
        """
        A volume was unmounted.
        Enable offline mode for this volume.
        """
        mount_path = mount.get_root().get_path()

        if not mount_path:
            log.warning(
                "Mount removed but no mount_path is present. Skipping...")
            return

        log.debug("Volume unmounted: " + mount_path)

        storage = next(
            (s for s in self.external_storage if mount_path in s.storage.path),
            None)
        if storage:
            log.info("Storage offline: " + mount_path)
            storage.online = False
            self.emit_event("storage-offline", storage.storage.path)

            # switch to offline version if currently playing

    def __on_settings_changed(self, event, message):
        """
        This method reacts to storage settings changes.
        """
        if event == "external-storage-added" or event == "storage-changed" or (
                event == "storage-added" and message != ""):
            self.init_offline_mode()
        elif event == "storage-removed" or event == "external-storage-removed":
            self.external_storage = [
                item for item in self.external_storage
                if item.storage.path not in message
            ]
Пример #23
0
class Files(EventSender):
    _settings = inject.attr(Settings)
    _importer = inject.attr(Importer)

    _file_count = 0
    _file_progess = 0

    def __init__(self):
        super().__init__()

    def copy(self, selection):
        log.info("Start of copying files")
        self.emit_event_main_thread("start-copy", None)
        uris = selection.get_uris()
        storage_location = self._settings.default_location.path

        self._file_count = 0
        self._file_progess = 0

        self._count_all_files(uris)
        self._copy_all(uris, storage_location)

        log.info("Copying of files finished")
        self._importer.scan()

    def _copy_all(self, sources, destination: str):
        for uri in sources:
            parsed_path = urllib.parse.urlparse(uri)
            path = urllib.parse.unquote(parsed_path.path)

            if os.path.isdir(path):
                self._copy_directory(path, destination)
            else:
                filename = os.path.basename(path)
                file_copy_destination = os.path.join(destination, filename)
                self._copy_file(path, file_copy_destination)

    def _copy_file(self, source_path: str, dest_path: str):
        log.info("Copy file {} to {}".format(source_path, dest_path))

        source = Gio.File.new_for_path(source_path)
        destination = Gio.File.new_for_path(dest_path)
        flags = Gio.FileCopyFlags.OVERWRITE
        self.filecopy_cancel = Gio.Cancellable()
        try:
            copied = source.copy(destination, flags, self.filecopy_cancel,
                                 self._update_copy_status, None)
        except Exception as e:
            if e.code == Gio.IOErrorEnum.CANCELLED:
                pass
            reporter.exception("files", e)
            log.error("Failed to copy file: {}".format(e))
        self._file_progess += 1

    def _copy_directory(self, path, destination):
        main_source_path = os.path.split(path)[0]
        for dirpath, dirnames, filenames in os.walk(path):
            dirname = os.path.relpath(dirpath, main_source_path)
            destination_dir = os.path.join(destination, dirname)
            Path(destination_dir).mkdir(parents=True, exist_ok=True)

            for file in filenames:
                source = os.path.join(dirpath, file)
                file_copy_destination = os.path.join(destination, dirname,
                                                     file)
                self._copy_file(source, file_copy_destination)

    def _count_all_files(self, uris):
        for uri in uris:
            parsed_path = urllib.parse.urlparse(uri)
            path = urllib.parse.unquote(parsed_path.path)
            if os.path.isdir(path):
                self._file_count += self._count_files_in_folder(path)
            else:
                self._file_count += 1

    def _update_copy_status(self, current_num_bytes, total_num_bytes, _):
        if total_num_bytes == 0:
            total_num_bytes = 1

        if self._file_count == 0:
            progress = 1.0
        else:
            progress = ((self._file_progess - 1) / self._file_count) + (
                (current_num_bytes / total_num_bytes) / self._file_count)
        self.emit_event_main_thread("copy-progress", progress)

    def _count_files_in_folder(self, path: str) -> int:
        return sum([len(files) for r, d, files in os.walk(path)])
Пример #24
0
class AlbumElement(Gtk.Box):
    """
    This class represents a clickable album art widget for a book.
    """

    artwork_cache: ArtworkCache = inject.attr(ArtworkCache)

    def __init__(self, book, size, scale, bordered=False, square=False):
        """
        :param size: the size for the longer side of the image
        :param bordered: should there be a border around the album art?
        :param square: should the widget be always a square?
        """
        super().__init__()
        self.props.height_request = size

        self.book: Book = book
        self.selected = False
        self.signal_ids = []
        self.play_signal_ids = []

        # the event box is used for mouse enter and leave signals
        self.event_box = Gtk.EventBox()
        self.event_box.set_property("halign", Gtk.Align.CENTER)
        self.event_box.set_property("valign", Gtk.Align.CENTER)

        # scale the book cover to a fix size.
        pixbuf = self.artwork_cache.get_cover_pixbuf(book.db_object, scale,
                                                     size)

        # box is the main container for the album art
        self.set_halign(Gtk.Align.CENTER)
        self.set_valign(Gtk.Align.CENTER)

        # img contains the album art
        img = Gtk.Image()
        img.set_halign(Gtk.Align.CENTER)
        img.set_valign(Gtk.Align.CENTER)
        if pixbuf:
            if bordered:
                img.get_style_context().add_class("bordered")
            surface = Gdk.cairo_surface_create_from_pixbuf(pixbuf, scale, None)
            img.set_from_surface(surface)
        else:
            img.set_from_icon_name("book-open-variant-symbolic",
                                   Gtk.IconSize.DIALOG)
            img.props.pixel_size = size

        self.play_box = Gtk.EventBox()

        # on click we want to play the audio book
        self.play_signal_ids.append(
            self.play_box.connect("button-press-event",
                                  self._on_play_button_press))
        self.play_box.set_property("halign", Gtk.Align.CENTER)
        self.play_box.set_property("valign", Gtk.Align.CENTER)
        self.play_box.set_tooltip_text(_("Play this book"))

        # play_color is an overlay for the play button
        # with this it should be visible on any album art color
        play_image = GdkPixbuf.Pixbuf.new_from_resource(
            "/com/github/geigi/cozy/play_background.svg")
        if square:
            play_image = play_image.scale_simple(size - 10, size - 10,
                                                 GdkPixbuf.InterpType.BILINEAR)
        if size < 100:
            self.icon_size = Gtk.IconSize.LARGE_TOOLBAR
        else:
            self.icon_size = Gtk.IconSize.DIALOG
        self.play_button = Gtk.Image.new_from_icon_name(
            "media-playback-start-symbolic", self.icon_size)
        self.play_button.set_property("halign", Gtk.Align.CENTER)
        self.play_button.set_property("valign", Gtk.Align.CENTER)
        self.play_button.get_style_context().add_class("white")

        # this is the main overlay for the album art
        # we need to create field for the overlays
        # to change the opacity of them on mouse over/leave events
        self.overlay = Gtk.Overlay.new()

        # this is the play symbol overlay
        self.play_overlay = Gtk.Overlay.new()

        # this is for the play button animation
        self.play_revealer = Gtk.Revealer()
        self.play_revealer.set_transition_type(
            Gtk.RevealerTransitionType.CROSSFADE)
        self.play_revealer.set_transition_duration(300)
        self.play_revealer.add(self.play_overlay)
        self.play_revealer.add_events(Gdk.EventMask.ENTER_NOTIFY_MASK)
        self.play_revealer.add_events(Gdk.EventMask.LEAVE_NOTIFY_MASK)

        # this grid has a background color to act as a visible overlay
        color = Gtk.Grid()
        color.set_property("halign", Gtk.Align.CENTER)
        color.set_property("valign", Gtk.Align.CENTER)

        if square:
            self.set_size_request(size, size)

            smaller_width = size - pixbuf.get_width()
            if smaller_width > 1:
                self.event_box.set_margin_left(smaller_width / 2)

        # assemble play overlay
        self.play_box.add(self.play_button)
        self.play_overlay.add(self.play_box)

        # assemble overlay with album art
        self.overlay.add(img)
        self.overlay.add_overlay(self.play_revealer)

        # assemble overlay color
        color.add(self.overlay)
        self.event_box.add(color)
        self.add(self.event_box)

        # connect signals
        self.play_signal_ids.append(
            self.play_box.connect("enter-notify-event",
                                  self._on_play_enter_notify))
        self.play_signal_ids.append(
            self.play_box.connect("leave-notify-event",
                                  self._on_play_leave_notify))
        # connect mouse events to the event box
        self.signal_ids.append(
            self.event_box.connect("enter-notify-event",
                                   self._on_enter_notify))
        self.signal_ids.append(
            self.event_box.connect("leave-notify-event",
                                   self._on_leave_notify))

    def disconnect_signals(self):
        """
        Disconnect all signals from this element.
        """
        [self.event_box.disconnect(sig) for sig in self.signal_ids]
        [self.play_box.disconnect(sig) for sig in self.play_signal_ids]

    def _on_enter_notify(self, widget, event):
        """
        On enter notify change overlay opacity
        :param widget: as Gtk.EventBox
        :param event: as Gdk.Event
        """
        self.play_revealer.set_reveal_child(True)

    def _on_leave_notify(self, widget, event):
        """
        On leave notify change overlay opacity
        :param widget: as Gtk.EventBox (can be None)
        :param event: as Gdk.Event (can be None)
        """
        if not self.selected:
            self.play_revealer.set_reveal_child(False)

    def _on_play_enter_notify(self, widget, event):
        """
        Change the cursor to pointing hand
        """
        self.props.window.set_cursor(
            Gdk.Cursor.new_from_name(self.get_display(), "pointer"))

    def _on_play_leave_notify(self, widget, event):
        """
        Reset the cursor.
        """
        self.props.window.set_cursor(None)

    def _on_play_button_press(self, widget, event):
        """
        Play this book.
        """
        if event.type == Gdk.EventType.BUTTON_PRESS and event.button != 1:
            return False

        self.emit("play-pause-clicked", self.book.db_object)
        track = get_track_for_playback(self.book.db_object)
        current_track = player.get_current_track()

        if current_track and current_track.book.id == self.book.db_object.id:
            player.play_pause(None)
            if player.get_gst_player_state() == Gst.State.PLAYING:
                player.jump_to_ns(track.position)
        else:
            player.load_file(track)
            player.play_pause(None, True)

        return True
Пример #25
0
class SleepTimer(Gtk.Popover):
    __gtype_name__ = "SleepTimer"

    _view_model = inject.attr(SleepTimerViewModel)

    timer_scale: Gtk.Scale = Gtk.Template.Child()
    timer_label: Gtk.Label = Gtk.Template.Child()
    timer_grid: Gtk.Grid = Gtk.Template.Child()
    min_label: Gtk.Label = Gtk.Template.Child()
    chapter_switch: Gtk.Switch = Gtk.Template.Child()

    power_control_switch: Gtk.Switch = Gtk.Template.Child()
    power_control_options: Gtk.Box = Gtk.Template.Child()
    system_shutdown_radiob: Gtk.RadioButton = Gtk.Template.Child()
    system_suspend_radiob: Gtk.RadioButton = Gtk.Template.Child()

    def __init__(self, timer_image: Gtk.Image):
        super().__init__()

        self._timer_image: Gtk.Image = timer_image

        self._init_timer_scale()
        self._connect_widgets()

        self._connect_view_model()

        self._on_timer_scale_changed(self.timer_scale)

    def _connect_widgets(self):
        self.timer_scale.connect("value-changed", self._on_timer_scale_changed)
        self.chapter_switch.connect("state-set",
                                    self._on_chapter_switch_changed)
        self.power_control_switch.connect(
            "state-set", self._on_power_options_switch_changed)
        self.system_suspend_radiob.connect(
            "toggled", self._on_system_action_radio_button_changed)
        self.system_shutdown_radiob.connect(
            "toggled", self._on_system_action_radio_button_changed)

    def _connect_view_model(self):
        self._view_model.bind_to("stop_after_chapter",
                                 self._on_stop_after_chapter_changed)
        self._view_model.bind_to("remaining_seconds",
                                 self._on_remaining_seconds_changed)
        self._view_model.bind_to("timer_enabled",
                                 self._on_timer_enabled_changed)

    def _init_timer_scale(self):
        for i in range(0, 121, 30):
            self.timer_scale.add_mark(i, Gtk.PositionType.RIGHT, None)

    def _on_timer_scale_changed(self, scale: Gtk.Scale):
        value = scale.get_value()

        if value > 0:
            self.timer_label.set_visible(True)
            self.min_label.set_text(_("min"))
            text = str(int(value))
            self.timer_label.set_text(text)
            self._view_model.remaining_seconds = value * 60
        else:
            self.min_label.set_text(_("Off"))
            self.timer_label.set_visible(False)
            self._view_model.remaining_seconds = 0

    def _on_chapter_switch_changed(self, _, state):
        self.timer_grid.set_sensitive(not state)
        self._view_model.stop_after_chapter = state

    def _on_remaining_seconds_changed(self):
        if self._view_model.remaining_seconds < 1:
            value = 0
        else:
            value = int((self._view_model.remaining_seconds / 60)) + 1

        self.timer_scale.set_value(value)

    def _on_power_options_switch_changed(self, _, state):
        self.power_control_options.set_sensitive(state)

        if not state:
            self._view_model.system_power_control = SystemPowerControl.OFF
        else:
            self._on_system_action_radio_button_changed(None)

    def _on_system_action_radio_button_changed(self, _):
        if self.system_suspend_radiob.get_active():
            self._view_model.system_power_control = SystemPowerControl.SUSPEND
        else:
            self._view_model.system_power_control = SystemPowerControl.SHUTDOWN

    def _on_stop_after_chapter_changed(self):
        self.chapter_switch.set_active(self._view_model.stop_after_chapter)

    def _on_timer_enabled_changed(self):
        if self._view_model.timer_enabled:
            icon = "timer-on-symbolic"
        else:
            icon = "timer-off-symbolic"

        self._timer_image.set_from_icon_name(icon, Gtk.IconSize.BUTTON)
Пример #26
0
class LibraryViewModel(Observable, EventSender):
    _application_settings: ApplicationSettings = inject.attr(ApplicationSettings)
    _fs_monitor: FilesystemMonitor = inject.attr("FilesystemMonitor")
    _model = inject.attr(Library)
    _importer: Importer = inject.attr(Importer)
    _player: Player = inject.attr(Player)

    def __init__(self):
        super().__init__()
        super(Observable, self).__init__()

        self._library_view_mode: LibraryViewMode = LibraryViewMode.CURRENT
        self._selected_filter: str = _("All")

        self._connect()

    def _connect(self):
        self._fs_monitor.add_listener(self._on_fs_monitor_event)
        self._application_settings.add_listener(self._on_application_setting_changed)
        self._importer.add_listener(self._on_importer_event)
        self._player.add_listener(self._on_player_event)
        self._model.add_listener(self._on_model_event)

    @property
    def books(self):
        return self._model.books

    @property
    def library_view_mode(self):
        return self._library_view_mode

    @library_view_mode.setter
    def library_view_mode(self, value):
        self._library_view_mode = value
        self._notify("library_view_mode")

    @property
    def selected_filter(self):
        return self._selected_filter

    @selected_filter.setter
    def selected_filter(self, value):
        self._selected_filter = value
        self._notify("selected_filter")

    @property
    def is_any_book_in_progress(self):
        return any(book.position > 0 for book in self.books)

    @property
    def authors(self):
        is_book_online = self._fs_monitor.get_book_online
        show_offline_books = not self._application_settings.hide_offline
        swap_author_reader = self._application_settings.swap_author_reader

        authors = {
            book.author if not swap_author_reader else book.reader
            for book
            in self._model.books
            if is_book_online(book) or show_offline_books or book.downloaded
        }

        return sorted(split_strings_to_set(authors))

    @property
    def readers(self):
        is_book_online = self._fs_monitor.get_book_online
        show_offline_books = not self._application_settings.hide_offline
        swap_author_reader = self._application_settings.swap_author_reader

        readers = {
            book.reader if not swap_author_reader else book.author
            for book
            in self._model.books
            if is_book_online(book) or show_offline_books or book.downloaded
        }

        return sorted(split_strings_to_set(readers))

    def playback_book(self, book: Book):
        # Pause/Play book here
        pass

    def remove_book(self, book: Book):
        book.remove()
        self._model.invalidate()
        self._notify("authors")
        self._notify("readers")
        self._notify("books")
        self._notify("books-filter")

    def display_book_filter(self, book_element: BookElement):
        book = book_element.book
        swap_author_reader = self._application_settings.swap_author_reader
        author = book.author if not swap_author_reader else book.reader
        reader = book.reader if not swap_author_reader else book.author

        hide_offline_books = self._application_settings.hide_offline
        book_is_online = self._fs_monitor.get_book_online(book)

        if hide_offline_books and not book_is_online and not book.downloaded:
            return False

        if self.library_view_mode == LibraryViewMode.CURRENT:
            return True if book.last_played > 0 else False

        if self.selected_filter == _("All"):
            return True
        elif self.library_view_mode == LibraryViewMode.AUTHOR:
            return True if self.selected_filter in author else False
        elif self.library_view_mode == LibraryViewMode.READER:
            return True if self.selected_filter in reader else False

    def display_book_sort(self, book_element1, book_element2):
        if self._library_view_mode == LibraryViewMode.CURRENT:
            return book_element1.book.last_played < book_element2.book.last_played
        else:
            return book_element1.book.name.lower() > book_element2.book.name.lower()

    def _on_fs_monitor_event(self, event, _):
        if event == "storage-online":
            self._notify("authors")
            self._notify("readers")
            self._notify("books-filter")
        elif event == "storage-offline":
            self._notify("authors")
            self._notify("readers")
            self._notify("books-filter")
        elif event == "external-storage-added":
            pass
        elif event == "external-storage-removed":
            pass

    def _on_application_setting_changed(self, event, _):
        if event == "hide-offline":
            self._notify("authors")
            self._notify("readers")
            self._notify("books-filter")
        elif event == "swap-author-reader":
            self._notify("authors")
            self._notify("readers")

    def _on_importer_event(self, event, message):
        if event == "scan" and message == ScanStatus.SUCCESS:
            self._notify("authors")
            self._notify("readers")
            self._notify("books")
            self._notify("books-filter")
            self._notify("library_view_mode")
        if event == "import-failed":
            dialog = ImportFailedDialog(message)
            dialog.show()

    def _on_player_event(self, event, message):
        if event == "play":
            track_id = message
            book = None

            for b in self._model.books:
                if any(chapter.id == track_id for chapter in b.chapters):
                    book = b
                    break

            if book:
                book.reload()
                self._notify("books-filter")

    def _on_model_event(self, event: str, message):
        if event == "rebase-finished":
            self.emit_event("work-done")
Пример #27
0
class SearchView:
    view_model = inject.attr(SearchViewModel)

    search_thread = None
    search_thread_stop = None

    def __init__(self, main_window_builder: Gtk.Builder):
        self.builder = Gtk.Builder.new_from_resource(
            "/com/github/geigi/cozy/search_popover.ui")
        self.main_window_builder: Gtk.Builder = main_window_builder

        self.popover = self.builder.get_object("search_popover")

        self.search_button = self.main_window_builder.get_object(
            "search_button")
        self.book_label = self.builder.get_object("book_label")
        self.track_label = self.builder.get_object("track_label")
        self.author_label = self.builder.get_object("author_label")
        self.reader_label = self.builder.get_object("reader_label")
        self.reader_box = self.builder.get_object("reader_result_box")
        self.author_box = self.builder.get_object("author_result_box")
        self.book_box = self.builder.get_object("book_result_box")
        self.track_box = self.builder.get_object("track_result_box")
        self.entry = self.builder.get_object("search_entry")
        self.scroller = self.builder.get_object("search_scroller")
        self.book_separator = self.builder.get_object("book_separator")
        self.author_separator = self.builder.get_object("author_separator")
        self.reader_separator = self.builder.get_object("reader_separator")
        self.stack = self.builder.get_object("search_stack")

        self.search_button.set_popover(self.popover)
        self.entry.connect("search-changed", self.__on_search_changed)

        if Gtk.get_minor_version() > 20:
            self.scroller.set_max_content_width(400)
            self.scroller.set_max_content_height(600)
            self.scroller.set_propagate_natural_height(True)
            self.scroller.set_propagate_natural_width(True)

        self.search_thread = Thread(target=self.search, name="SearchThread")
        self.search_thread_stop = threading.Event()

        self._connect_view_model()

    def _connect_view_model(self):
        self.view_model.bind_to("search_open", self._on_search_open_changed)

    def search(self, user_search: str):
        # we need the main context to call methods in the main thread after the search is finished
        main_context = GLib.MainContext.default()

        books = list({
            book
            for book in self.view_model.books
            if user_search.lower() in book.name.lower()
            or user_search.lower() in book.author.lower()
            or user_search.lower() in book.reader.lower()
        })
        books = sorted(books, key=lambda book: book.name.lower())
        if self.search_thread_stop.is_set():
            return
        main_context.invoke_full(GLib.PRIORITY_DEFAULT,
                                 self.__on_book_search_finished, books)

        authors = sorted({
            author
            for author in self.view_model.authors
            if user_search.lower() in author.lower()
        })
        if self.search_thread_stop.is_set():
            return
        main_context.invoke_full(GLib.PRIORITY_DEFAULT,
                                 self.__on_author_search_finished, authors)

        readers = sorted({
            reader
            for reader in self.view_model.readers
            if user_search.lower() in reader.lower()
        })
        if self.search_thread_stop.is_set():
            return
        main_context.invoke_full(GLib.PRIORITY_DEFAULT,
                                 self.__on_reader_search_finished, readers)

        if len(readers) < 1 and len(authors) < 1 and len(books) < 1:
            main_context.invoke_full(GLib.PRIORITY_DEFAULT,
                                     self.stack.set_visible_child_name,
                                     "nothing")

    def close(self, object=None):
        if Gtk.get_minor_version() < 22:
            self.popover.hide()
        else:
            self.popover.popdown()

    def __on_search_changed(self, sender):
        self.search_thread_stop.set()

        # we want to avoid flickering of the search box size
        # as we remove widgets and add them again
        # so we get the current search popup size and set it as
        # the preferred size until the search is finished
        # this helps only a bit, the widgets are still flickering
        self.popover.set_size_request(self.popover.get_allocated_width(),
                                      self.popover.get_allocated_height())

        # hide nothing found
        self.stack.set_visible_child_name("main")

        # First clear the boxes
        self.book_box.remove_all_children()
        self.author_box.remove_all_children()
        self.reader_box.remove_all_children()

        # Hide all the labels & separators
        self.book_label.set_visible(False)
        self.author_label.set_visible(False)
        self.reader_label.set_visible(False)
        self.book_separator.set_visible(False)
        self.author_separator.set_visible(False)
        self.reader_separator.set_visible(False)
        self.track_label.set_visible(False)

        user_search = self.entry.get_text()
        if user_search:
            if self.search_thread.is_alive():
                self.search_thread.join(timeout=0.2)
            self.search_thread_stop.clear()
            self.search_thread = Thread(target=self.search,
                                        args=(user_search, ))
            self.search_thread.start()
        else:
            self.stack.set_visible_child_name("start")
            self.popover.set_size_request(-1, -1)

    def _on_search_open_changed(self):
        if self.view_model.search_open == False:
            self.close()

    def __on_book_search_finished(self, books):
        if len(books) > 0:
            self.stack.set_visible_child_name("main")
            self.book_label.set_visible(True)
            self.book_separator.set_visible(True)

            for book in books:
                if self.search_thread_stop.is_set():
                    return

                book_result = BookSearchResult(book,
                                               self.view_model.jump_to_book)
                self.book_box.add(book_result)

    def __on_author_search_finished(self, authors):
        if len(authors) > 0:
            self.stack.set_visible_child_name("main")
            self.author_label.set_visible(True)
            self.author_separator.set_visible(True)

            for author in authors:
                if self.search_thread_stop.is_set():
                    return

                author_result = ArtistSearchResult(
                    self.view_model.jump_to_author, author, True)
                self.author_box.add(author_result)

    def __on_reader_search_finished(self, readers):
        if len(readers) > 0:
            self.stack.set_visible_child_name("main")
            self.reader_label.set_visible(True)
            self.reader_separator.set_visible(True)

            for reader in readers:
                if self.search_thread_stop.is_set():
                    return

                reader_result = ArtistSearchResult(
                    self.view_model.jump_to_reader, reader, False)
                self.reader_box.add(reader_result)

        self.popover.set_size_request(-1, -1)
Пример #28
0
class DatabaseImporter:
    _db = inject.attr(SqliteDatabase)

    def __init__(self):
        self._book_update_positions: List[BookUpdatePositionRequest] = []

    def insert_many(self, media_files: Set[MediaFile]):
        self._book_update_positions = []

        files = self._prepare_files_db_objects(media_files)
        File.insert_many(files).execute()
        tracks = self._prepare_track_db_objects(media_files)
        self._insert_tracks(tracks)
        self._update_book_positions()

    def _prepare_files_db_objects(self,
                                  media_files: Set[MediaFile]) -> List[object]:
        files = []

        for media_file in media_files:
            query = File.select().where(File.path == media_file.path)

            if query.exists():
                self._update_files_in_db(query.get(), media_file)
                continue

            file_already_in_list = any(f["path"] == media_file.path
                                       for f in files)

            if not file_already_in_list:
                files.append({
                    "path": media_file.path,
                    "modified": media_file.modified
                })

        return files

    def _update_files_in_db(self, file: File, media_file: MediaFile):
        file.modified = media_file.modified
        file.save(only=file.dirty_fields)

    def _prepare_track_db_objects(
            self, media_files: Set[MediaFile]) -> Set[TrackInsertRequest]:
        book_db_objects: Set[BookModel] = set()

        for media_file in media_files:
            if not media_file:
                continue

            book = next((book for book in book_db_objects
                         if is_same_book(book.name, media_file.book_name)),
                        None)
            file_query = File.select().where(File.path == media_file.path)
            if not file_query.exists():
                log.error("No file object with path present: {}".format(
                    media_file.path))
                continue

            file = file_query.get()

            if not book:
                book = self._import_or_update_book(media_file)
                book_db_objects.add(book)

            try:
                book_model = Book(self._db, book.id)
                progress = book_model.progress
            except BookIsEmpty:
                progress = 0

            self._delete_tracks_from_db(media_file)
            tracks = self._get_track_list_for_db(media_file, book)

            for track in tracks:
                start_at = track.pop("startAt")
                yield TrackInsertRequest(track, file, start_at)

            update_position_request_present = any(
                b.book_id == book.id for b in self._book_update_positions)
            if progress > 0 and not update_position_request_present:
                self._book_update_positions.append(
                    BookUpdatePositionRequest(book.id, progress))

    def _import_or_update_book(self, media_file):
        if BookModel.select(BookModel.name).where(
                self._matches_db_book(media_file.book_name)).count() < 1:
            book = self._create_book_db_object(media_file)
        else:
            book = self._update_book_db_object(media_file)
        return book

    def _matches_db_book(self, book_name: str) -> bool:
        return fn.Lower(BookModel.name) == book_name.lower()

    def _get_track_list_for_db(self, media_file: MediaFile, book: BookModel):
        tracks = []

        for chapter in media_file.chapters:
            tracks.append({
                "name": chapter.name,
                "number": chapter.number,
                "disk": media_file.disk,
                "book": book,
                "length": chapter.length,
                "startAt": chapter.position,
                "position": 0
            })

        return tracks

    def _update_book_db_object(self, media_file: MediaFile) -> BookModel:
        BookModel.update(name=media_file.book_name,
                         author=media_file.author,
                         reader=media_file.reader,
                         cover=media_file.cover) \
            .where(self._matches_db_book(media_file.book_name)) \
            .execute()
        return BookModel.select().where(
            self._matches_db_book(media_file.book_name)).get()

    def _create_book_db_object(self, media_file: MediaFile) -> BookModel:
        return BookModel.create(name=media_file.book_name,
                                author=media_file.author,
                                reader=media_file.reader,
                                cover=media_file.cover,
                                position=0,
                                rating=-1)

    def _get_track_db_objects_for_media_file(
            self, media_file: MediaFile) -> List[Track]:
        all_track_mappings = TrackToFile.select().join(File).where(
            TrackToFile.file.path == media_file.path)

        for item in all_track_mappings:
            yield item.track

    def _delete_tracks_from_db(self, media_file: MediaFile):
        for track in self._get_track_db_objects_for_media_file(media_file):
            track.delete_instance(recursive=True)

    def _is_chapter_count_in_db_different(self, media_file: MediaFile) -> bool:
        all_track_mappings = self._get_chapter_count_in_db(media_file)

        if all_track_mappings != len(media_file.chapters):
            return True
        else:
            return False

    def _get_chapter_count_in_db(self, media_file: MediaFile) -> int:
        all_track_mappings = TrackToFile.select().join(File).where(
            TrackToFile.file.path == media_file.path)

        return all_track_mappings.count()

    def _insert_tracks(self, tracks: Set[TrackInsertRequest]):
        for track in tracks:
            track_db = Track.insert(track.track_data).execute()
            TrackToFile.create(track=track_db,
                               file=track.file,
                               start_at=track.start_at)

    def _update_book_positions(self):
        for book_position in self._book_update_positions:
            book = BookModel.get_or_none(book_position.book_id)

            if not book:
                log.error(
                    "Could not restore book position because book is not present"
                )
                continue

            self._update_book_position(book, book_position.progress)

        self._book_update_positions = []

    def _update_book_position(self, book: BookModel, progress: int):
        try:
            book_model = Book(self._db, book.id)
        except BookIsEmpty:
            log.error("Could not restore book position because book is empty")
            return

        completed_chapter_length = 0
        for chapter in book_model.chapters:
            old_position = progress
            if completed_chapter_length + chapter.length > old_position:
                chapter.position = chapter.start_position + (
                    (old_position - completed_chapter_length) * 10**9)
                book_model.position = chapter.id
                return
            else:
                completed_chapter_length += chapter.length

        book_model.position = 0
Пример #29
0
class BookDetailViewModel(Observable, EventSender):
    _player: Player = inject.attr(Player)
    _fs_monitor: FilesystemMonitor = inject.attr("FilesystemMonitor")
    _offline_cache: OfflineCache = inject.attr(OfflineCache)
    _settings: Settings = inject.attr(Settings)
    _library = Library = inject.attr(Library)

    def __init__(self):
        super().__init__()
        super(Observable, self).__init__()

        self._play = False
        self._current_chapter = None
        self._book: Book = None
        self._lock_ui: bool = False

        self._player.add_listener(self._on_player_event)
        self._fs_monitor.add_listener(self._on_fs_monitor_event)
        self._offline_cache.add_listener(self._on_offline_cache_event)

    @property
    def playing(self) -> bool:
        if not self._player.loaded_book or self._player.loaded_book != self._book:
            return False

        return self._player.playing

    @property
    def current_chapter(self) -> Optional[Chapter]:
        if not self.book:
            return None

        return self.book.current_chapter

    @property
    def book(self) -> Optional[Book]:
        return self._book

    @book.setter
    def book(self, value: Book):
        if self._book:
            self._book.remove_bind("current_chapter",
                                   self._on_book_current_chapter_changed)
            self._book.remove_bind("last_played",
                                   self._on_book_last_played_changed)
            self._book.remove_bind("duration", self._on_book_duration_changed)
            self._book.remove_bind("progress", self._on_book_progress_changed)
            self._book.remove_bind("playback_speed",
                                   self._on_playback_speed_changed)

        self._book = value
        self._current_chapter = None
        self._book.bind_to("current_chapter",
                           self._on_book_current_chapter_changed)
        self._book.bind_to("last_played", self._on_book_last_played_changed)
        self._book.bind_to("duration", self._on_book_duration_changed)
        self._book.bind_to("progress", self._on_book_progress_changed)
        self._book.bind_to("playback_speed", self._on_playback_speed_changed)
        self._notify("book")

    @property
    def last_played_text(self) -> Optional[str]:
        if not self._book:
            return None

        return tools.past_date_to_human_readable(self._book.last_played)

    @property
    def total_text(self) -> Optional[str]:
        if not self._book:
            return None

        return tools.seconds_to_human_readable(self._book.duration /
                                               self._book.playback_speed)

    @property
    def remaining_text(self) -> Optional[str]:
        if not self._book:
            return None

        remaining = self._book.duration / self._book.playback_speed - self._book.progress / self._book.playback_speed
        return tools.seconds_to_human_readable(remaining)

    @property
    def progress_percent(self) -> Optional[float]:
        if not self._book:
            return None

        if self._book.duration < 1:
            return 1.0

        return self._book.progress / self._book.duration

    @property
    def disk_count(self) -> int:
        if not self._book:
            return 0

        return len({chapter.disk for chapter in self._book.chapters})

    @property
    def is_book_available(self) -> bool:
        return self._fs_monitor.get_book_online(self._book)

    @property
    def is_book_external(self) -> bool:
        first_chapter_path = self._book.chapters[0].file
        return any(storage.path in first_chapter_path
                   for storage in self._settings.external_storage_locations)

    @property
    def lock_ui(self) -> bool:
        return self._lock_ui

    @lock_ui.setter
    def lock_ui(self, new_value: bool):
        self._lock_ui = new_value
        self._notify("lock_ui")

    def download_book(self, download: bool):
        self._book.offline = download

        if download:
            self._offline_cache.add(self._book.db_object)
        else:
            self._offline_cache.remove(self._book.db_object)

    def open_library(self):
        self.emit_event(OpenView.LIBRARY)

    def play_book(self):
        self._player.play_pause_book(self.book)

    def play_chapter(self, chapter: Chapter):
        self._player.play_pause_chapter(self._book, chapter)

    def _on_player_event(self, event, message):
        if not self.book:
            return

        if event == "play" or event == "pause":
            self._notify("playing")
        elif event == "position":
            self._notify("progress_percent")
            self._notify("remaining_text")

    def _on_fs_monitor_event(self, event, _):
        if event == "storage-online":
            self._notify("is_book_available")
        elif event == "storage-offline":
            self._notify("is_book_available")

    def _on_book_current_chapter_changed(self):
        self._notify("current_chapter")

    def _on_book_last_played_changed(self):
        self._notify("last_played_text")

    def _on_book_progress_changed(self):
        self._notify("remaining_text")
        self._notify("progress_percent")

    def _on_book_duration_changed(self):
        self._notify("progress_percent")
        self._notify("remaining_text")
        self._notify("total_text")

    def _on_playback_speed_changed(self):
        self._notify("progress_percent")
        self._notify("remaining_text")
        self._notify("total_text")

    def _on_offline_cache_event(self, event, message):
        try:
            if message.id != self._book.db_object.id:
                return
        except Exception as e:
            return

        if event == "book-offline-removed":
            # TODO: Remove this when offline cache refactoring is complete
            self._book.downloaded = False
            self._notify("downloaded")
        elif event == "book-offline":
            self._book.downloaded = True
            self._notify("downloaded")
Пример #30
0
class CozyUI(EventSender, metaclass=Singleton):
    """
    CozyUI is the main ui class.
    """
    # Is currently an dialog open?
    dialog_open = False
    is_initialized = False
    __inhibit_cookie = None
    fs_monitor = inject.attr(fs_monitor.FilesystemMonitor)
    offline_cache = inject.attr(OfflineCache)
    settings = inject.attr(Settings)
    application_settings = inject.attr(ApplicationSettings)
    _importer: Importer = inject.attr(Importer)
    _settings: SettingsModel = inject.attr(SettingsModel)
    _files: Files = inject.attr(Files)
    _player: Player = inject.attr(Player)

    def __init__(self, pkgdatadir, app, version):
        super().__init__()
        self.pkgdir = pkgdatadir
        self.app = app
        self.version = version

        self._library_view: LibraryView = None

    def activate(self, library_view: LibraryView):
        self.first_play = True

        self.__init_actions()
        self.__init_components()

        self._library_view = library_view

        self.auto_import()
        self.refresh_content()
        self.check_for_tracks()

        self.is_initialized = True

    def startup(self):
        self.__init_resources()
        self.__init_css()

        self.__init_window()

    def __init_resources(self):
        """
        Initialize all resources like gresource and glade windows.
        """
        resource = Gio.resource_load(
            os.path.join(self.pkgdir, 'com.github.geigi.cozy.ui.gresource'))
        Gio.Resource._register(resource)

        resource = Gio.resource_load(
            os.path.join(self.pkgdir, 'com.github.geigi.cozy.img.gresource'))
        Gio.Resource._register(resource)

        self.window_builder = Gtk.Builder.new_from_resource(
            "/com/github/geigi/cozy/main_window.ui")

        self.about_builder = Gtk.Builder.new_from_resource(
            "/com/github/geigi/cozy/about.ui")

    def __init_css(self):
        """
        Initialize the main css files and providers.
        Add css classes to the default screen style context.
        """
        main_cssProviderFile = Gio.File.new_for_uri(
            "resource:///com/github/geigi/cozy/application.css")
        main_cssProvider = Gtk.CssProvider()
        main_cssProvider.load_from_file(main_cssProviderFile)

        if Gtk.get_minor_version() > 18:
            log.debug("Fanciest design possible")
            cssProviderFile = Gio.File.new_for_uri(
                "resource:///com/github/geigi/cozy/application_default.css")
        else:
            log.debug("Using legacy css file")
            cssProviderFile = Gio.File.new_for_uri(
                "resource:///com/github/geigi/cozy/application_legacy.css")
        cssProvider = Gtk.CssProvider()
        cssProvider.load_from_file(cssProviderFile)

        # add the bordered css class to the default screen for the borders around album art
        screen = Gdk.Screen.get_default()
        styleContext = Gtk.StyleContext()
        styleContext.add_provider_for_screen(
            screen, main_cssProvider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
        styleContext.add_provider_for_screen(
            screen, cssProvider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
        styleContext.add_class("bordered")

    def __init_window(self):
        """
        Add fields for all ui objects we need to access from code.
        Initialize everything we can't do from glade like events and other stuff.
        """
        log.info("Initialize main window")
        self.window: Gtk.Window = self.window_builder.get_object("app_window")
        self.window.set_default_size(1100, 700)
        self.window.set_application(self.app)
        self.window.show_all()
        self.window.present()
        self.window.connect("delete-event", self.on_close)
        self.window.connect("drag_data_received", self.__on_drag_data_received)
        self.window.drag_dest_set(Gtk.DestDefaults.MOTION | Gtk.DestDefaults.HIGHLIGHT | Gtk.DestDefaults.DROP,
                                  [Gtk.TargetEntry.new("text/uri-list", 0, 80)], Gdk.DragAction.COPY)
        self.window.title = "Cozy"

        self.book_box = self.window_builder.get_object("book_box")
        self.sort_stack = self.window_builder.get_object("sort_stack")
        self.main_stack: Gtk.Stack = self.window_builder.get_object("main_stack")

        self.category_toolbar = self.window_builder.get_object(
            "category_toolbar")

        self.sort_stack_revealer = self.window_builder.get_object(
            "sort_stack_revealer")
        # This fixes a bug where otherwise expand is
        # somehow set to true internally
        # but is still showing false in the inspector
        self.sort_stack_revealer.props.expand = True
        self.sort_stack_revealer.props.expand = False

        self.sort_stack_switcher = self.window_builder.get_object(
            "sort_stack_switcher")
        self.no_media_file_chooser = self.window_builder.get_object(
            "no_media_file_chooser")
        self.no_media_file_chooser.connect(
            "file-set", self.__on_no_media_folder_changed)

        self.auto_scan_switch = self.window_builder.get_object(
            "auto_scan_switch")

        # some visual stuff
        self.category_toolbar_separator = self.window_builder.get_object("category_toolbar_separator")
        if tools.is_elementary():
            self.category_toolbar.set_visible(False)

        # get about dialog
        self.about_dialog = self.about_builder.get_object("about_dialog")
        self.about_dialog.set_modal(self.window)
        self.about_dialog.connect("delete-event", self.hide_window)
        self.about_dialog.set_version(self.version)

        # shortcuts
        self.accel = Gtk.AccelGroup()

        try:
            about_close_button = self.about_builder.get_object(
                "button_box").get_children()[2]

            if about_close_button:
                about_close_button.connect(
                    "clicked", self.__about_close_clicked)
        except Exception as e:
            log.info("Not connecting about close button.")

    def __init_actions(self):
        """
        Init all app actions.
        """
        self.accel = Gtk.AccelGroup()

        help_action = Gio.SimpleAction.new("help", None)
        help_action.connect("activate", self.help)
        self.app.add_action(help_action)

        about_action = Gio.SimpleAction.new("about", None)
        about_action.connect("activate", self.about)
        self.app.add_action(about_action)

        quit_action = Gio.SimpleAction.new("quit", None)
        quit_action.connect("activate", self.quit)
        self.app.add_action(quit_action)
        self.app.set_accels_for_action(
            "app.quit", ["<Control>q", "<Control>w"])

        pref_action = Gio.SimpleAction.new("prefs", None)
        pref_action.connect("activate", self.show_prefs)
        self.app.add_action(pref_action)
        self.app.set_accels_for_action("app.prefs", ["<Control>comma"])

        self.scan_action = Gio.SimpleAction.new("scan", None)
        self.scan_action.connect("activate", self.scan)
        self.app.add_action(self.scan_action)

        self.play_pause_action = Gio.SimpleAction.new("play_pause", None)
        self.play_pause_action.connect("activate", self.play_pause)
        self.app.add_action(self.play_pause_action)
        self.app.set_accels_for_action("app.play_pause", ["space"])

        back_action = Gio.SimpleAction.new("back", None)
        back_action.connect("activate", self.back)
        self.app.add_action(back_action)
        self.app.set_accels_for_action("app.back", ["Escape"])

        self.hide_offline_action = Gio.SimpleAction.new_stateful("hide_offline",
                                                                 None,
                                                                 GLib.Variant.new_boolean(
                                                                     self.application_settings.hide_offline))
        self.hide_offline_action.connect("change-state", self.__on_hide_offline)
        self.app.add_action(self.hide_offline_action)

    def __init_components(self):
        if not self._player.loaded_book:
            self.block_ui_buttons(True)

        self._importer.add_listener(self._on_importer_event)


    def get_object(self, name):
        return self.window_builder.get_object(name)

    def help(self, action, parameter):
        """
        Show app help.
        """
        webbrowser.open("https://github.com/geigi/cozy/issues", new=2)

    def quit(self, action, parameter):
        """
        Quit app.
        """
        self.on_close(None)
        self.app.quit()

    def about(self, action, parameter):
        """
        Show about window.
        """
        self.about_dialog.show()

    def show_prefs(self, action, parameter):
        """
        Show preferences window.
        """
        self.settings.show()

    def hide_window(self, widget, data=None):
        """
        Hide a given window. This is used for the about and settings dialog
        as they will never be closed only hidden.

        param widget: The widget that will be hidden.
        """
        widget.hide()

        # we handeled the close event so the window must not get destroyed.
        return True

    def play_pause(self, action, parameter):
        self._player.play_pause()

    def block_ui_buttons(self, block, scan=False):
        """
        Makes the buttons to interact with the player insensetive.
        :param block: Boolean
        """
        sensitive = not block
        try:
            self.play_pause_action.set_enabled(sensitive)
            if scan:
                self.scan_action.set_enabled(sensitive)
                self.hide_offline_action.set_enabled(sensitive)
                self.settings.block_ui_elements(block)
        except:
            pass

    def switch_to_playing(self):
        """
        Switch the UI state back to playing.
        This enables all UI functionality for the user.
        """
        if self.main_stack.props.visible_child_name != "book_overview" and self.main_stack.props.visible_child_name != "nothing_here" and self.main_stack.props.visible_child_name != "no_media":
            self.main_stack.props.visible_child_name = "main"
        if self.main_stack.props.visible_child_name != "no_media" and self.main_stack.props.visible_child_name != "book_overview":
            self.category_toolbar.set_visible(True)
        if self._player.loaded_book:
            self.block_ui_buttons(False, True)
        else:
            # we want to only block the player controls
            self.block_ui_buttons(False, True)
            self.block_ui_buttons(True, False)
        self.window.props.window.set_cursor(None)
        self.emit_event_main_thread("working", False)

    def check_for_tracks(self):
        """
        Check if there are any imported files.
        If there aren't display a welcome screen.
        """
        if books().count() < 1:
            path = ""
            if len(self._settings.storage_locations) > 0:
                path = self._settings.default_location.path

            self.no_media_file_chooser.set_current_folder(path)
            self.main_stack.props.visible_child_name = "no_media"
            self.block_ui_buttons(True)
            self.category_toolbar.set_visible(False)

    def scan(self, _, __):
        thread = Thread(target=self._importer.scan, name="ScanMediaThread")
        thread.start()

    def auto_import(self):
        if self.application_settings.autoscan:
            self.scan(None, None)

    def back(self, action, parameter):
        self.emit_event("open_view", OpenView.LIBRARY)

    def refresh_content(self):
        """
        Refresh all content.
        """
        # First clear the boxes
        childs = self.book_box.get_children()
        for element in childs:
            self.book_box.remove(element)

        self._library_view.populate_author()
        self._library_view.populate_reader()
        self._library_view.populate_book_box()

        self.book_box.show_all()

        return False

    def display_failed_imports(self, files):
        """
        Displays a dialog with a list of files that could not be imported.
        """
        dialog = ImportFailedDialog(files)
        dialog.show()

    def __on_hide_offline(self, action, value):
        """
        Show/Hide offline books action handler.
        """
        action.set_state(value)
        self.application_settings.hide_offline = value.get_boolean()

    def __on_drag_data_received(self, widget, context, x, y, selection, target_type, timestamp):
        """
        We want to import the files that are dragged onto the window.
        inspired by https://stackoverflow.com/questions/24094186/drag-and-drop-file-example-in-pygobject
        """
        if target_type == 80:
            thread = Thread(target=self._files.copy, args=[selection], name="DragDropImportThread")
            thread.start()

    def __on_no_media_folder_changed(self, sender):
        """
        Get's called when the user changes the audiobook location from
        the no media screen. Now we want to do a first scan instead of a rebase.
        """
        location = self.no_media_file_chooser.get_file().get_path()
        external = self.fs_monitor.is_external(location)
        Storage.delete().where(Storage.path != "").execute()
        Storage.create(path=location, default=True, external=external)
        self._settings.invalidate()
        self.main_stack.props.visible_child_name = "import"
        self.scan(None, None)
        self.settings._init_storage()
        self.fs_monitor.init_offline_mode()

    def track_changed(self):
        self.block_ui_buttons(False, True)

    def __window_resized(self, window):
        """
        Resize the progress scale to expand to the window size
        for older gtk versions.
        """
        width, height = self.window.get_size()
        value = width - 850
        if value < 80:
            value = 80

    def __about_close_clicked(self, widget):
        self.about_dialog.hide()

    def on_close(self, widget, data=None):
        """
        Close and dispose everything that needs to be when window is closed.
        """
        log.info("Closing.")
        self.fs_monitor.close()

        self._player.destroy()

        close_db()

        report.close()

        log.info("Closing app.")
        self.app.quit()
        log.info("App closed.")

    def get_builder(self):
        return self.window_builder

    def _on_importer_event(self, event: str, message):
        if event == "scan" and message == ScanStatus.SUCCESS:
            self.check_for_tracks()