Exemple #1
0
class Main(object):
    def __init__(self, window):
        self.rm_client = RemarkableClient()

        # Define app settings
        scale = cfg.get("scaling", 1 / window.tk.call('tk', 'scaling'))
        window.tk.call('tk', 'scaling', 1 / scale)
        window_width = 750 * scale
        window_height = 700 * scale

        # Subscribe to events
        self.rm_client.listen_sign_in_event(self)

        # Window settings
        window.title("RemaPy Explorer")

        # Try to start remapy always on the first screen and in the middle.
        # We assume a resolution width of 1920... if 1920 is too large use
        # the real resolution
        x = min(window.winfo_screenwidth(), 1920) / 2 - (window_width / 2)
        y = (window.winfo_screenheight() / 2) - (window_height / 2)
        window.geometry("%dx%d+%d+%d" % (window_width, window_height, x, y))

        # Create different tabs on notebook
        self.notebook = ttk.Notebook(window)
        self.notebook.pack(expand=1, fill="both")

        frame = ttk.Frame(self.notebook)
        self.file_explorer = FileExplorer(frame, window)
        self.notebook.add(frame, text="File Explorer")

        frame = ttk.Frame(self.notebook)
        self.notebook.add(frame, text="Backup", state="hidden")

        frame = ttk.Frame(self.notebook)
        self.notebook.add(frame, text="Zotero", state="hidden")

        frame = ttk.Frame(self.notebook)
        self.notebook.add(frame, text="Mirror", state="hidden")

        frame = ttk.Frame(self.notebook)
        self.notebook.add(frame, text="SSH", state="hidden")

        frame = ttk.Frame(self.notebook)
        self.settings = Settings(frame)
        self.notebook.add(frame, text="Settings")

        frame = ttk.Frame(self.notebook)
        self.about = About(frame)
        self.notebook.add(frame, text="About")

        # Try to sign in to the rm cloud without a onetime code i.e. we
        # assume that the user token is already available. If it is not
        # possible we get a signal to disable "My remarkable" and settings
        # are shown...
        self.rm_client.sign_in()

    #
    # EVENT HANDLER
    #
    def sign_in_event_handler(self, event, data):
        # If we fail to get a user token, we are e.g. offline. So we continue
        # and try if we can get it later; otherwise we go into an offline mode
        if event == api.remarkable_client.EVENT_SUCCESS or event == api.remarkable_client.EVENT_USER_TOKEN_FAILED:
            self.notebook.tab(0, state="normal")
        else:
            self.notebook.tab(0, state="disabled")
Exemple #2
0
class FileExplorer(object):
    """ Main window of RemaPy which displays the tree structure of
        all your rm documents and collections.
    """

    def __init__(self, root, window, font_size=14, row_height=14):
        
        self.root = root
        self.window = window
        self.app_dir = os.path.dirname(__file__)
        self.icon_dir = os.path.join(self.app_dir, 'icons/')
        self._cached_icons = {}
        self._icon_lock = threading.Lock()
        self.row_height = row_height

        # Create tkinter elements
        self.nodes = dict()
        self.rm_client = RemarkableClient()
        self.item_manager = ItemManager()

        self.tree_style = ttk.Style()
        self.tree_style.configure("remapy.style.Treeview", highlightthickness=0, bd=0, font=font_size, rowheight=row_height)
        self.tree_style.configure("remapy.style.Treeview.Heading", font=font_size)
        self.tree_style.layout("remapy.style.Treeview", [('remapy.style.Treeview.treearea', {'sticky': 'nswe'})])
        
        self.upper_frame = tk.Frame(root)
        self.upper_frame.pack(expand=True, fill=tk.BOTH)

        self.label_offline = tk.Label(window, fg="#f44336", font='Arial 13 bold')
        self.label_offline.place(relx=0.5, y=12, anchor="center")

        self.entry_filter = None
        self.entry_filter_var = tk.StringVar()
        self.entry_filter_var.trace("w", self.filter_changed_event_handler)
        self.entry_filter = EntryWithPlaceholder(window, "Filter...", textvariable=self.entry_filter_var)
        self.entry_filter.place(relx=1.0, y=12, anchor="e")

        # Add tree and scrollbars
        self.tree = ttk.Treeview(self.upper_frame, style="remapy.style.Treeview")
        self.tree.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)
        self.tree.bind("<FocusOut>", self.tree_focus_out_event_handler)
        self.tree.bind("<FocusIn>", self.tree_focus_in_event_handler)
        
        self.vsb = ttk.Scrollbar(self.upper_frame, orient="vertical", command=self.tree.yview)
        self.vsb.pack(side=tk.LEFT, fill='y')
        self.tree.configure(yscrollcommand=self.vsb.set)

        self.hsb = ttk.Scrollbar(root, orient="horizontal", command=self.tree.xview)
        self.hsb.pack(fill='x')
        self.tree.configure(xscrollcommand=self.hsb.set)

        self.tree["columns"]=("#1","#2")
        self.tree.column("#0", minwidth=250)
        self.tree.column("#1", width=180, minwidth=180, stretch=tk.NO)
        self.tree.column("#2", width=150, minwidth=150, anchor="center", stretch=tk.NO)

        self.tree.heading("#0",text="Name", anchor="center")
        self.tree.heading("#1", text="Date modified", anchor="center")
        self.tree.heading("#2", text="Current Page", anchor="center")

        self.tree.tag_configure('move', background='#FF9800')    
        self.tree.focus_set()

        self.window.bind("<Escape>", self.key_binding_escape)
        
        # Context menu on right click
        # Check out drag and drop: https://stackoverflow.com/questions/44887576/how-can-i-create-a-drag-and-drop-interface
        self.tree.bind("<Button-3>", self.tree_right_click)
        self.context_menu =tk.Menu(root, tearoff=0, font=font_size)
        self.context_menu.add_command(label='Open', accelerator="Return", command=self.btn_open_item_click)
        self.context_menu.add_command(label='Open annotated pages    ', command=self.btn_open_oap_item_click)
        self.context_menu.add_command(label='Open raw', command=self.btn_open_item_original_click)
        self.context_menu.add_command(label='Open folder', command=self.btn_open_in_file_explorer)
        self.context_menu.add_separator()
        self.context_menu.add_command(label='ReSync', accelerator="F5", command=self.btn_resync_item_click)
        self.context_menu.add_command(label='Toggle bookmark', accelerator="Ctrl+B", command=self.btn_toggle_bookmark)
        self.context_menu.add_command(label='Rename...', accelerator="F2", command=self.btn_rename_item_click)
        self.context_menu.add_command(label='Delete', accelerator="Del", command=self.btn_delete_item_click)
        self.context_menu.add_command(label='Restore', command=self.btn_restore_item_click)
        self.context_menu.add_separator()
        self.context_menu.add_command(label='Paste', accelerator="Ctrl+V", command=self.btn_paste_async_click)

        self.tree.bind("<Double-1>", self.tree_double_click)

        # Footer
        self.lower_frame = tk.Frame(root)
        self.lower_frame.pack(side=tk.BOTTOM, anchor="w", fill=tk.X)

        self.lower_frame_left = tk.Frame(self.lower_frame)
        self.lower_frame_left.pack(side=tk.LEFT)

        self.btn_sync = tk.Button(self.lower_frame_left, text="Sync", width=10, command=self.btn_sync_click)
        self.btn_sync.pack(anchor="w")

        self.btn_resync = tk.Button(self.lower_frame_left, text="ReSync", width=10, command=self.btn_resync_click)
        self.btn_resync.pack(anchor="w")

        self.lower_frame_right = tk.Frame(self.lower_frame)
        self.lower_frame_right.pack(side=tk.LEFT, expand=True, fill=tk.X)

        self.log_widget = tk.scrolledtext.ScrolledText(self.lower_frame_right, height=3)
        self.log_widget.insert(tk.END, "RemaPy Explorer v0.1")
        self.log_widget.config(state=tk.DISABLED)
        self.log_widget.pack(expand=True, fill=tk.X)
        
        self.rm_client.listen_sign_in_event(self)
    

    def _set_online_mode(self, mode):
        self.btn_sync.config(state=mode)
        self.btn_resync.config(state=mode)
        self.context_menu.entryconfig(5, state=mode)
        self.context_menu.entryconfig(6, state=mode)
        self.context_menu.entryconfig(7, state=mode)
        self.context_menu.entryconfig(8, state=mode)
        self.context_menu.entryconfig(9, state=mode)
        self.context_menu.entryconfig(11, state=mode)

        bg = "#ffffff" if mode == "normal" else "#bdbdbd"
        self.tree_style.configure("remapy.style.Treeview", background=bg)

        if mode == "normal":
            self.label_offline.config(text="")
        else:
            self.label_offline.config(text="Offline")


    def log_console(self, text):
        now = strftime("%H:%M:%S", gmtime())
        self.log_widget.config(state=tk.NORMAL)
        self.log_widget.insert(tk.END, "\n[%s] %s" % (str(now), text))
        self.log_widget.config(state=tk.DISABLED)
        self.log_widget.see(tk.END)
    
    #
    # Tree
    #
    def tree_focus_out_event_handler(self, *args):
        self.window.unbind("<Control-v>")
        self.window.unbind("<Return>")
        self.window.unbind("<Delete>")
        self.window.unbind("<Control-f>")
        self.window.unbind("<Control-b>")
        self.window.unbind("<F5>")
        self.window.unbind("<F2>")
    

    def tree_focus_in_event_handler(self, *args):
        self.window.bind("<Control-f>", self.key_binding_filter)
        self.window.bind("<Control-v>", self.key_binding_paste)
        self.window.bind("<Control-b>", self.key_binding_toggle_bookmark)
        self.window.bind("<F5>", self.key_binding_resync)
        self.window.bind("<Return>", self.key_binding_return)
        self.window.bind("<Delete>", self.key_binding_delete)
        self.window.bind("<F2>", self.key_binding_rename)


    def sign_in_event_handler(self, event, data):
        # Also if the login failed (e.g. we are offline) we try again 
        # if we can sync the items (e.g. with old user key) and otherwise 
        # we switch to the offline mode
        if event == api.remarkable_client.EVENT_SUCCESS or event == api.remarkable_client.EVENT_USER_TOKEN_FAILED:
            self.btn_sync_click()


    def key_binding_filter(self, event):
        self.entry_filter.focus_set()
    

    def key_binding_escape(self, event):
        self.tree.focus_set()
        children = self.tree.get_children()
        if len(children) > 0:
            self.tree.focus(children[0])
        else:
            self.tree.focus()
            

    def filter_changed_event_handler(self, placeholder, *args):
        if self.entry_filter is None:
            return 

        filter_text = self.entry_filter_var.get()
        if filter_text == self.entry_filter.placeholder:
            filter_text = None
        
        root = self.item_manager.get_root()
        self.tree.delete(*self.tree.get_children())
        self._update_tree(root, filter_text)


    def _update_tree(self, item, filter=None):
        try:
            is_direct_match = False
            if not item.is_root():
                is_match, is_direct_match = self._match_filter(item, filter)
                if is_match:
                    self.tree.insert(
                        item.parent().id(), 
                        0 if item.id() != "trash" else 99999, 
                        item.id(),
                        open=filter!=None)

                    self._update_tree_item(item)
                    item.add_state_listener(self._update_tree_item)
                    include_all_childs = item.is_collection() and is_direct_match
                    if include_all_childs:
                        filter = None

            # Sort by name and item type
            sorted_children = item.children()
            sorted_children.sort(key=lambda x: str.lower(x.name()), reverse=True)
            sorted_children.sort(key=lambda x: int(x.is_document()), reverse=True)
            sorted_children.sort(key=lambda x: int(x.id()=="trash"), reverse=True)
            for child in sorted_children:
                self._update_tree(child, filter)
        except Exception as e:
            self.log_console("(Warning) Failed to add item %s" % item.id())
            print(e)
            # Try to remove wrong item from tree
            try:
                self.tree.delete(item.id())
            except:
                pass
    

    def _match_filter(self, item, filter):  
        """ Returns whether we have a match on this path (to include all 
            parent folders) and whether the given item was the matching one.
        """
        if filter is None or filter == "":
            return True, True
        
        if filter.startswith("!b "):
            bookmarked_only = True
            text_filter = filter[3:]
        elif filter == "!b":
            bookmarked_only = True
            text_filter = ""
        else:
            bookmarked_only = False
            text_filter = filter

        is_match = (text_filter.lower() in item.name().lower())
        
        if bookmarked_only:
            is_match = is_match and item.bookmarked()

        if is_match:
            return is_match, True

        for child in item.children():
            child_match, _ = self._match_filter(child, filter)
            if child_match:
                return child_match, False

        return False, False
    

    def tree_right_click(self, event):
        selected_ids = self.tree.selection()
        if selected_ids:
            items = [self.item_manager.get_item(id) for id in selected_ids]
            for item in items:
                for possile_child in items:
                    if not item.is_parent_of(possile_child):
                        continue 
                    
                    messagebox.showerror(
                        "Invalid operation", 
                        "Your selection is invalid. You can not perform an \
                            action on a folder and one of its child items.")
                    return

            self.context_menu.tk_popup(event.x_root, event.y_root)   
            pass         
        else:
            # mouse pointer not over item
            pass


    def _update_tree_item(self, item):
        if item.state == model.item.STATE_DELETED:
            self.tree.delete(item.id())
        else:
            icon = self._get_icon(item)
            self.tree.item(
                item.id(), 
                image=icon, 
                text=" " + item.name(),
                values=(
                    item.modified_time().strftime("%Y-%m-%d %H:%M:%S"), 
                    item.current_page()))


    def _get_icon(self, item):
        if item.is_collection():
            if item.id() == "trash":
                return self._create_tree_icon("trash")    

            if item.state == model.item.STATE_SYNCED:
                return self._create_tree_icon("collection", item.bookmarked())
            else:
                return self._create_tree_icon("collection_syncing")
        
        if item.state == model.document.STATE_NOT_SYNCED:
            return self._create_tree_icon("cloud")
        
        elif item.state == model.item.STATE_SYNCING:
            return self._create_tree_icon("document_syncing")

        if item.state == model.item.STATE_SYNCED:
            if item.type == model.document.TYPE_PDF:
                return self._create_tree_icon("pdf", item.bookmarked())
            elif item.type == model.document.TYPE_EPUB:
                return self._create_tree_icon("epub", item.bookmarked())
            else: 
                return self._create_tree_icon("notebook", item.bookmarked())

        if item.state == model.document.STATE_OUT_OF_SYNC:
            if item.type == model.document.TYPE_PDF:
                return self._create_tree_icon("pdf_out_of_sync")
            elif item.type == model.document.TYPE_EPUB:
                return self._create_tree_icon("epub_out_of_sync")
            else: 
                return self._create_tree_icon("notebook_out_of_sync")
        
        return self._create_tree_icon("weird")

    
    def _create_tree_icon(self, name, bookmarked=False):
        key = "%s_%s" % (name, bookmarked)
        if key in self._cached_icons:
                return self._cached_icons[key]

        # If possible return icon from cache
        with self._icon_lock:

            # Double check if key is in cached icons
            if key in self._cached_icons:
                return self._cached_icons[key]
        
            icon_size = self.row_height-4
            path = "%s%s.png" % (self.icon_dir, name)
            icon = Image.open(path)
            icon = icon.resize((icon_size, icon_size))

            if bookmarked:
                icon_star = Image.open("%s%s.png" % (self.icon_dir, "star"))
                icon_star = icon_star.resize((icon_size, icon_size))
                icon.paste(icon_star, None, icon_star)
            
            self._cached_icons[key] = itk.PhotoImage(icon)
            return self._cached_icons[key]


    def key_binding_rename(self, event):
        self.btn_rename_item_click()


    def btn_rename_item_click(self):
        selected_ids = self.tree.selection()
        if len(selected_ids) != 1:
            messagebox.showerror("Error", "Select exactly one item to rename.", icon='error')
            return
        
        item = self.item_manager.get_item(selected_ids[0])

        if item.name() == "Quick sheets" and item.parent().is_root():
            messagebox.showerror("Error", "You can not rename the Quick sheets.", icon='error')
            return
        
        if item.name() == "Trash" and item.parent().is_root():
            messagebox.showerror("Error", "You can not rename the Trash.", icon='error')
            return

        name = simpledialog.askstring('Rename', 'Enter new name', initialvalue=item.name())
        if name is None:
            return

        item.rename(name)

    def key_binding_resync(self, event):
        self.btn_resync_item_click()

    def btn_resync_item_click(self):
        self._sync_selection_async(
                force=True, 
                open_file=False, 
                open_original=False)


    def tree_double_click(self, event):
        selected_ids = self.tree.selection()
        item = self.item_manager.get_item(selected_ids[0])
        
        if item.is_document():
            self._sync_selection_async(
                force=False, 
                open_file=True, 
                open_original=False)


    def key_binding_return(self, event):
        self.btn_open_item_click()


    def btn_open_item_click(self):
        self._sync_selection_async(
                force=False, 
                open_file=True, 
                open_original=False)
    

    def btn_open_oap_item_click(self):
        self._sync_selection_async(
                        force=False, 
                        open_file=True, 
                        open_oap=True)


    def btn_open_item_original_click(self):
        self._sync_selection_async(
                force=False, 
                open_file=True, 
                open_original=True)


    def btn_resync_click(self):

        if self.is_online:
            message = "Do you really want to delete ALL local files and download ALL documents again?"
        else:
            message = "Do you really want resync without a connection to the remarkable cloud?"
        
        result = messagebox.askquestion("Warning", message, icon='warning')

        if result != "yes":
            return 

        # Clean everything, also if some (old) things exist
        shutil.rmtree(utils.config.PATH, ignore_errors=True)
        Path(utils.config.PATH).mkdir(parents=True, exist_ok=True)

        self.item_manager.traverse_tree(
            fun=lambda item: item.update_state()
        )

        # And sync again
        self.btn_sync_click()


    def btn_sync_click(self):
        self.log_console("Syncing all documents...")
        root, self.is_online = self.item_manager.get_root(force=True)

        if self.is_online:
            self._set_online_mode("normal")
        else:
            self.log_console("OFFLINE MODE: No connection to the remarkable cloud")
            self._set_online_mode("disabled")

        self.tree.delete(*self.tree.get_children())
        self._update_tree(root)

        self._sync_items_async([self.item_manager.get_root()],
                force=False, 
                open_file=False, 
                open_original=False,
                open_oap=False)


    def _sync_selection_async(self, force=False, open_file=False, open_original=False, open_oap=False):
        selected_ids = self.tree.selection()
        items = [self.item_manager.get_item(id) for id in selected_ids]
        self._sync_items_async(items, force, open_file, open_original, open_oap)


    def _sync_items_async(self, items, force, open_file, open_original, open_oap):
        """ To keep the gui responsive...
        """
        thread = threading.Thread(target=self._sync_items, args=(items, force, open_file, open_original, open_oap))
        thread.start()


    def _sync_items(self, items, force, open_file, open_original, open_oap):
        q = queue.Queue()
        threads = []

        def worker():
            while True:
                item = q.get()
                if item is None:
                    break
                
                try:
                    self._sync_and_open_item(item, force, open_file, open_original, open_oap)
                except Exception as e:
                    if open_file:
                        self.log_console("(Error) Could not open '%s'" % item.name())
                    else:
                        self.log_console("(Error) Could not sync '%s'" % item.name())
                    print(e)
                    
                q.task_done()

        num_worker_threads = 10
        for i in range(num_worker_threads):
            t = threading.Thread(target=worker)
            t.start()
            threads.append(t)
        
        # Add all items and child items
        for item in items:
            self.item_manager.traverse_tree(fun=q.put, item = item)
            
        q.join()

        # stop workers
        for i in range(num_worker_threads):
            q.put(None)
        for t in threads:
            t.join()
    

    def _sync_and_open_item(self, item, force, open_file, open_original, open_oap):   
        
        if item.state == model.item.STATE_SYNCING:
            self.log_console("Already syncing '%s'" %  item.full_name())
            return

        if (force or item.state != model.item.STATE_SYNCED) and not item.is_root():
            item.sync()

            if item.is_document():
                self.log_console("Synced '%s'" %  item.full_name())

        if open_file and item.is_document():
            if open_original:
                file_to_open = item.orig_file()
            elif open_oap:
                file_to_open = item.oap_file()
                if file_to_open == None:
                    messagebox.showinfo("Information", "Document is not annotated.", icon='info')
                    return
            else: 
                file_to_open = item.ann_or_orig_file()
                
            if sys.platform == "win32":
                os.startfile(os.path.normpath(file_to_open))
            else:
                if file_to_open.endswith(".pdf"):
                    try:
                        current_page = 0 if open_oap else item.current_page()
                        subprocess.call(["evince", "-i", str(current_page), file_to_open])
                    except:
                        subprocess.call(["xdg-open", file_to_open])    
                else:
                    subprocess.call(["xdg-open", file_to_open])


    #
    # Delete
    #
    def key_binding_delete(self, event):
        self.btn_delete_item_click()


    def btn_delete_item_click(self):
        selected_ids = self.tree.selection()
        items = [self.item_manager.get_item(id) for id in selected_ids]
        
        count = [0, 0]
        for item in items:
            if item.is_document():
                count[0] += 1
                continue
            
            child_count = item.get_exact_children_count()
            count = np.add(count, child_count)
        
        message = "Do you really want to delete (or trash) %d collection(s) and %d file(s)?" % (count[1], count[0])
        result = messagebox.askquestion("Delete", message, icon='warning')

        if result != "yes":
            return

        def run():
            for item in items:
                if item.name() == "Quick sheets" and item.parent().is_root():
                    self.log_console("(Warning) You can not delete the Quick sheets.")
                    continue
                    
                if item.name() == "Trash" and item.parent().is_root():
                    self.log_console("(Warning) You can not delete the trash.")
                    continue
                
                if item.parent().id() == "trash":
                    item.delete()
                    self.log_console("Deleted %s" % item.full_name())
                else: 
                    trash = self.item_manager.trash
                    self._move(item, trash)
                    
        threading.Thread(target=run).start()


    #
    # RESTORE
    #
    def btn_restore_item_click(self):
        selected_ids = self.tree.selection()
        items = [self.item_manager.get_item(id) for id in selected_ids]

        def run():
            for item in items:
                if item.parent().id() != "trash":
                    self.log_console("(Warning) Restore of '%s' not necessary." % item.full_name())
                    continue
                
                self._move(item, self.item_manager.root)
                    
        threading.Thread(target=run).start()


    def _move(self, item, new_parent):
        old_parent_id = item.parent().id()

        # Move on cloud
        item.move(new_parent)

        # Remove from old parent (tree view)
        old_parent_children = list(self.tree.get_children(old_parent_id))
        old_parent_children.remove(item.id())
        self.tree.set_children(old_parent_id, *old_parent_children)
        
        # Add to trash (tree view)
        new_parent_children = list(self.tree.get_children(new_parent.id()))
        new_parent_children.append(item.id())
        self.tree.set_children(new_parent.id(), *new_parent_children)
        
        self.log_console("Moved '%s' into '%s'" % (item.full_name(), new_parent.full_name()))

    #
    # Copy, Paste, Cut
    #
    def key_binding_paste(self, event):
        self.btn_paste_async_click()


    def btn_paste_async_click(self):
        selected_ids = self.tree.selection()
        
        if len(selected_ids) > 1:
            messagebox.showerror("Paste error", "Can paste only into one collection.")
            return
        
        elif len(selected_ids) == 1:
            item = self.item_manager.get_item(selected_ids[0])
            parent_id = str(item.parent().id() if item.is_document() else item.id())
        
        else:
            parent_id = ""      
    
        def is_file(path):
            if not os.path.exists(path):
                return None
            elif path.endswith(".pdf"):
                return "pdf"
            elif path.endswith(".epub"):
                return "epub"
            return None            
        
        def is_url(url):
            return url.startswith("http")

        # Some versions of nautilus include "x-special/nautilus-clipboard file://..." 
        # Or dolphin simple adds "file://..."
        # See also issue #11
        paths = self.root.clipboard_get().split("\n")
        paths = [path.replace("file://", "") for path in paths]
        paths = list(filter(lambda path: is_file(path) != None or is_url(path), paths))

        if len(paths) <= 0:
            messagebox.showerror(
                        "Failed to copy from clipboard", 
                        "The given clipboard is invalid. Only .pdf, .epub and urls are supported.\n\n%s" % self.root.clipboard_get())
            return
       
        def run(clipboard):       
            id = str(uuid.uuid4())         
            filetype = is_file(clipboard)
            if filetype != None:
                name = os.path.splitext(os.path.basename(clipboard))[0]
                self.tree.insert(
                    parent_id, 9999, id,
                    text= " " + name,
                    image=self._create_tree_icon("document_upload"))

                with open(clipboard, "rb") as f:
                    data = f.read()

            elif is_url(path):
                try:
                    import pdfkit
                    self.log_console("Converting webpage '%s'. This could take a few minutes." % clipboard)
                    name = clipboard
                    self.tree.insert(
                        parent_id, 9999, id,
                        text= " " + name,
                        image=self._create_tree_icon("document_upload"))

                    options = {
                        # Here we can manually set some cookies to 
                        # for example automatically accept terms of usage etc.
                        'cookie': [
                            ('DSGVO_ZUSAGE_V1', 'true')
                        ]
                    }
                    data = pdfkit.from_url(clipboard, False, options=options)
                    filetype = "pdf"
                except Exception as e:
                    messagebox.showerror(
                        "Failed to convert html to pdf", 
                        "Please ensure that you installed pdfkit and wkhtmltopdf correctly https://pypi.org/project/pdfkit/")
                    self.tree.delete(id)
                    return

            # Show new item in tree
            self.log_console("Upload document %s..." % name)

            # Upload
            item = self.item_manager.upload_file(
                id, parent_id, name, 
                filetype, data,
                self._update_tree_item)
            self.log_console("Successfully uploaded %s" % item.full_name())

        for path in paths:
            threading.Thread(target=run, args=[path]).start()


    def btn_open_in_file_explorer(self):
        selected_ids = self.tree.selection()
        items = [self.item_manager.get_item(id) for id in selected_ids]

        for item in items:
            if item.is_collection():
                continue

            if sys.platform == "win32":
                os.startfile(os.path.normpath(item.path_remapy))
            else:
                subprocess.call(('xdg-open', item.path_remapy))
    
    def key_binding_toggle_bookmark(self, event):
        self.btn_toggle_bookmark()
        
    def btn_toggle_bookmark(self):
        selected_ids = self.tree.selection()
        items = [self.item_manager.get_item(id) for id in selected_ids]

        def run():
            for item in items:
                item.set_bookmarked(not item.bookmarked())
        threading.Thread(target=run).start()
Exemple #3
0
class Settings(object):
    def __init__(self, root, font_size):
        self.rm_client = RemarkableClient()
        self.item_manager = ItemManager()

        root.grid_columnconfigure(4, minsize=180)
        root.grid_rowconfigure(1, minsize=50)
        root.grid_rowconfigure(2, minsize=30)
        root.grid_rowconfigure(3, minsize=30)
        root.grid_rowconfigure(4, minsize=30)
        root.grid_rowconfigure(6, minsize=50)
        root.grid_rowconfigure(7, minsize=30)
        root.grid_rowconfigure(8, minsize=30)
        root.grid_rowconfigure(9, minsize=50)

        # gaps between columns
        label = tk.Label(root, text="    ")
        label.grid(row=1, column=1)
        label = tk.Label(root, text="    ")
        label.grid(row=1, column=3)
        label = tk.Label(root, text="  ")
        label.grid(row=1, column=5)

        label = tk.Label(root, text="Authentication", font="Helvetica 14 bold")
        label.grid(row=1, column=2, sticky="W")

        self.onetime_code_link = "https://my.remarkable.com#desktop"
        self.label_onetime_code = tk.Label(
            root,
            justify="left",
            anchor="w",
            fg="blue",
            cursor="hand2",
            text="\nDownload one-time code from \n" + self.onetime_code_link)
        self.label_onetime_code.grid(row=2, column=7, sticky="SW")
        self.label_onetime_code.bind(
            "<Button-1>",
            lambda e: webbrowser.open_new(self.onetime_code_link))

        label = tk.Label(root, justify="left", anchor="w", text="Status: ")
        label.grid(row=2, column=2, sticky="W")
        self.label_auth_status = tk.Label(root, text="Unknown")
        self.label_auth_status.grid(row=2, column=4, sticky="W")

        label = tk.Label(root,
                         justify="left",
                         anchor="w",
                         text="One-time code:")
        label.grid(row=3, column=2, sticky="W")
        self.entry_onetime_code_text = tk.StringVar()
        self.entry_onetime_code = tk.Entry(
            root, textvariable=self.entry_onetime_code_text)
        self.entry_onetime_code.grid(row=3, column=4, sticky="W")

        self.btn_sign_in = tk.Button(root,
                                     text="Sign In",
                                     command=self.btn_sign_in_click,
                                     width=17)
        self.btn_sign_in.grid(row=4, column=4, sticky="W")

        label = tk.Label(root, text="General", font="Helvetica 14 bold")
        label.grid(row=6, column=2, sticky="W")

        label = tk.Label(root, text="Templates path:")
        label.grid(row=7, column=2, sticky="W")
        self.entry_templates_text = tk.StringVar()
        self.entry_templates_text.set(cfg.get("general.templates", default=""))
        self.entry_templates = tk.Entry(root,
                                        textvariable=self.entry_templates_text)
        self.entry_templates.grid(row=7, column=4, sticky="W")

        label = tk.Label(
            root,
            justify="left",
            anchor="w",
            text=
            "A local folder that contains all template PNG files. \nYou can copy the template files from your tablet: \n'/usr/share/remarkable'"
        )
        label.grid(row=7, column=7, sticky="W")

        label = tk.Label(root, text="Backup root path:")
        label.grid(row=8, column=2, sticky="W")
        self.backup_root_text = tk.StringVar()
        backup_root_default = Path.joinpath(Path.home(), "Backup",
                                            "Remarkable")
        backup_root = cfg.get("general.backuproot",
                              default=str(backup_root_default))
        self.backup_root_text.set(backup_root)
        self.entry_backup_root = tk.Entry(root,
                                          textvariable=self.backup_root_text)
        self.entry_backup_root.grid(row=8, column=4, sticky="W")

        label = tk.Label(
            root,
            justify="left",
            anchor="w",
            text=
            "A local folder that will be used as the root folder for backups.")
        label.grid(row=8, column=7, sticky="W")

        self.btn_save = tk.Button(root,
                                  text="Save",
                                  command=self.btn_save_click,
                                  width=17)
        self.btn_save.grid(row=9, column=4, sticky="W")

        label = tk.Label(root, text="Backup", font="Helvetica 14 bold")
        label.grid(row=10, column=2, sticky="W")

        label = tk.Label(root, text="Backup path:")
        label.grid(row=11, column=2, sticky="W")
        self.backup_folder_text = tk.StringVar()

        backup_folder = str(date.today().strftime("%Y-%m-%d"))
        self.backup_folder_text.set(backup_folder)
        self.entry_backup_folder = tk.Entry(
            root, textvariable=self.backup_folder_text)
        self.entry_backup_folder.grid(row=11, column=4, sticky="W")

        self.label_backup_progress = tk.Label(root)
        self.label_backup_progress.grid(row=11, column=6)

        label = tk.Label(
            root,
            justify="left",
            anchor="w",
            text=
            "Copy currently downloaded and annotated PDF files \ninto the given directory. Note that those files can not \nbe restored on the tablet."
        )
        label.grid(row=11, column=7, sticky="W")

        self.btn_create_backup = tk.Button(root,
                                           text="Create backup",
                                           command=self.btn_create_backup,
                                           width=17)
        self.btn_create_backup.grid(row=12, column=4, sticky="W")

        # Subscribe to sign in event. Outer logic (i.e. main) can try to
        # sign in automatically...
        self.rm_client.listen_sign_in_event(self)

    #
    # EVENT HANDLER
    #
    def sign_in_event_handler(self, event, config):

        self.btn_sign_in.config(state="normal")
        self.entry_onetime_code.config(state="normal")
        self.btn_create_backup.config(state="disabled")
        self.btn_save.config(state="disabled")
        self.entry_backup_root.config(state="disabled")
        self.entry_backup_folder.config(state="disabled")
        self.entry_templates.config(state="disabled")

        if event == api.remarkable_client.EVENT_SUCCESS:
            self.btn_sign_in.config(state="disabled")
            self.entry_onetime_code.config(state="disabled")
            self.btn_create_backup.config(state="normal")
            self.btn_save.config(state="normal")
            self.entry_backup_root.config(state="normal")
            self.entry_backup_folder.config(state="normal")
            self.entry_templates.config(state="normal")
            self.label_auth_status.config(text="Successfully signed in",
                                          fg="green")

        elif event == api.remarkable_client.EVENT_USER_TOKEN_FAILED:
            self.label_auth_status.config(
                text="Could not renew user token\n(please try again).",
                fg="red")
            self.entry_onetime_code.config(state="disabled")

        elif event == api.remarkable_client.EVENT_ONETIMECODE_NEEDED:
            self.label_auth_status.config(text="Enter one-time code.",
                                          fg="red")

        else:
            self.label_auth_status.config(text="Could not sign in.", fg="red")

    def btn_sign_in_click(self):
        onetime_code = self.entry_onetime_code_text.get()
        self.rm_client.sign_in(onetime_code)

    def btn_save_click(self):
        general = {
            "templates": self.entry_templates_text.get(),
            "backuproot": self.backup_root_text.get()
        }
        cfg.save({"general": general})

    def btn_create_backup(self):
        message = "If your explorer is not synchronized, some files are not included in the backup. Should we continue?"
        result = messagebox.askquestion("Info", message, icon='warning')

        if result != "yes":
            return

        backup_root = self.backup_root_text.get()
        backup_folder = self.backup_folder_text.get()
        backup_path = Path.joinpath(Path(backup_root), backup_folder)
        self.label_backup_progress.config(text="Writing backup '%s'" %
                                          backup_path)

        def run():
            self.item_manager.create_backup(backup_path)
            self.label_backup_progress.config(text="")
            messagebox.showinfo(
                "Info", "Successfully created backup '%s'" % backup_path)

        threading.Thread(target=run).start()