Exemplo n.º 1
0
    def __init__(self, window):
        self.rm_client = RemarkableClient()

        # Define app settings
        font_size = 38
        row_height = 30
        window_width = 750
        window_height = 650

        # 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,
                                          font_size=font_size,
                                          row_height=row_height)
        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, font_size)
        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()
Exemplo n.º 2
0
    def __init__(self, metadata, parent=None):
        self.metadata = metadata
        self._parent = parent
        self._children = []
        self.path = get_path(self.id())
        self.path_remapy = get_path_remapy(self.id())
        self.path_metadata_local = get_path_metadata_local(self.id())

        self.rm_client = RemarkableClient()
        self.state_listener = []
Exemplo n.º 3
0
    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)
Exemplo n.º 4
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()
Exemplo n.º 5
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")
Exemplo n.º 6
0
 def __init__(self, ):
     self.rm_client = RemarkableClient()
     self.root = None
     self.trash = None
Exemplo n.º 7
0
class ItemManager(metaclass=Singleton):
    """ The ItemManager keeps track of all the collections and documents
        that are stored in your rm cloud. Load and create items through 
        this class. It is a singleton such that it is ensured that access to 
        items goes through the same tree structure.
    """
    def __init__(self, ):
        self.rm_client = RemarkableClient()
        self.root = None
        self.trash = None

    def get_root(self, force=False):
        """ Get root node of tree from cache or download it from the rm cloud. 
            If you are offline, we load the stored tree from last time.
            Note that if we are online we sync all files with the rm cloud 
            i.e. delete old local files.
        """
        if not self.root is None and not force:
            return self.root

        metadata_list, is_online = self._get_metadata_list()

        self._clean_local_items(metadata_list)
        self.root, self.trash = self._create_tree(metadata_list)
        return self.root, is_online

    def get_item(self, id, item=None):
        """ Get item object for given id. If item metadata is not already
            downloaded, it is downloaded beforehand.
        """
        self.get_root()

        item = self.root if item is None else item

        if item.id() == id:
            return item

        for child in item.children():
            found = self.get_item(id, child)
            if found != None:
                return found

        return None

    def create_backup(self, backup_path):
        # Create folder structure
        self.traverse_tree(fun=lambda item: item.create_backup(backup_path),
                           document=False,
                           collection=True)

        # And copy files into it
        self.traverse_tree(fun=lambda item: item.create_backup(backup_path),
                           document=True,
                           collection=False)

    def upload_file(self,
                    id,
                    parent_id,
                    name,
                    filetype,
                    data,
                    state_listener=None):
        metadata, mf = self._prepare_new_document_zip(id,
                                                      name,
                                                      data,
                                                      file_type=filetype,
                                                      parent_id=parent_id)

        # Upload file into cloud
        metadata = self.rm_client.upload(id, metadata, mf)

        # Download again to ensure that metadata is correct
        parent = self.get_item(parent_id)
        item = self._create_item(metadata, parent)

        if state_listener != None:
            item.add_state_listener(state_listener)

        # Download again to get it correctly
        item.sync()
        return item

    def traverse_tree(self, fun, item=None, document=True, collection=True):
        """ Traverse item tree (bottom up) and call fun for item depending on 
            whether document=True and colleciton=True.
        """
        item = self.get_root() if item == None else item

        for child in item.children():
            self.traverse_tree(fun, child, document, collection)

        if (item.is_document() and document) or (item.is_collection()
                                                 and collection):
            fun(item)

    def _create_item(self, metadata, parent):
        if metadata["Type"] == "CollectionType":
            new_object = Collection(metadata, parent)

        elif metadata["Type"] == "DocumentType":
            new_object = Document(metadata, parent)

        else:
            raise Exception("Unknown type %s" % metadata["Type"])

        parent.add_child(new_object)
        return new_object

    def _get_metadata_list(self):
        try:
            metadata_list = self.rm_client.list_items()
            return metadata_list, metadata_list != None
        except:
            metadata_list = []
            for local_id in os.listdir(utils.config.PATH):
                metadata_path = model.item.get_path_metadata_local(local_id)
                with open(metadata_path, 'r') as file:
                    metadata_content = file.read().replace('\n', '')

                metadata = json.loads(metadata_content)
                metadata_list.append(metadata)

        return metadata_list, False

    def _clean_local_items(self, metadata_list):
        online_ids = [metadata["ID"] for metadata in metadata_list]
        for local_id in os.listdir(utils.config.PATH):
            if local_id in online_ids:
                continue

            local_file_or_folder = "%s/%s" % (utils.config.PATH, local_id)
            if os.path.isfile(local_file_or_folder):
                os.remove(local_file_or_folder)
            else:
                shutil.rmtree(local_file_or_folder)
            print("Deleted local item %s" % local_id)

    def _create_tree(self, metadata_list):

        # Create a dummy root object where everything starts with parent
        # "". This parent "" should not be changed as it is also used in
        # the rm cloud. Additionally with version 2.2 of the software
        # every tablet implicitly includes a trash.
        root = Collection(None, None)

        trash_metadata = {
            "ID": "trash",
            "Parent": "",
            "VissibleName": "Trash",
            "Version": 1,
            "Bookmarked": False,
            "Type": "CollectionType",
            "ModifiedClient": "2000-01-01T00:00:00.000000Z"
        }
        trash = self._create_item(trash_metadata, root)
        metadata_list.append(trash_metadata)

        # We do this for every element, because _create_item_and_parents
        # only ensures that all parents already exist
        items = {"": root, "trash": trash}
        lookup_table = {}
        for i in range(len(metadata_list)):
            lookup_table[metadata_list[i]["ID"]] = i

        for i in range(len(metadata_list)):
            self._create_item_and_parents(i, metadata_list, items,
                                          lookup_table)

        return root, trash

    def _create_item_and_parents(self, i, metadata_list, items, lookup_table):
        metadata = metadata_list[i]
        parent_id = metadata["Parent"]

        if i < 0 or len(metadata_list) <= 0 or metadata["ID"] in items:
            return

        if not parent_id in items:
            if not parent_id in lookup_table:
                print("(Warning) No parent for item %s" %
                      metadata["VissibleName"])
                parent_id = ""
            else:
                parent_pos = lookup_table[parent_id]
                self._create_item_and_parents(parent_pos, metadata_list, items,
                                              lookup_table)

        parent = items[parent_id]
        new_object = self._create_item(metadata, parent)
        items[new_object.id()] = new_object

    def _prepare_new_document_zip(self,
                                  id,
                                  name,
                                  data,
                                  file_type,
                                  parent_id=""):

        # .content file
        content_file = json.dumps({
            "dummyDocument": False,
            "extraMetadata": {
                "LastBrushColor": "Black",
                "LastBrushThicknessScale": "2",
                "LastColor": "Black",
                "LastEraserThicknessScale": "2",
                "LastEraserTool": "Eraser",
                "LastPen": "Finelinerv2",
                "LastPenColor": "Black",
                "LastPenThicknessScale": "2",
                "LastPencil": "SharpPencil",
                "LastPencilColor": "Black",
                "LastPencilThicknessScale": "2",
                "LastTool": "Finelinerv2",
                "ThicknessScale": "2",
                "LastFinelinerv2Size": "1",
            },
            "fileType": file_type,
            "pageCount": 0,
            "pages": [],
            "fontName": "",
            "lastOpenedPage": 0,
            "lineHeight": -1,
            "margins": 180,
            "orientation": "portrait",
            "textScale": 1,
            "transform": {
                "m11": 1,
                "m12": 0,
                "m13": 0,
                "m21": 0,
                "m22": 1,
                "m23": 0,
                "m31": 0,
                "m32": 0,
                "m33": 1,
            }
        })

        # metadata
        metadata = {
            "ID": id,
            "Parent": parent_id,
            "VissibleName": name,
            "Type": "DocumentType",
            "Version": 1,
            "ModifiedClient": model.item.now_rfc3339(),
            "CurrentPage": 0,
            "Bookmarked": False,
            # "Message": "",
            # "Success": True,
            # "BlobURLGet": "",
            # "BlobURLGetExpires": "",
            # "BlobURLPut": "",
            # "BlobURLPutExpires": ""
        }

        mf = BytesIO()
        mf.seek(0)
        with ZipFile(mf, mode='w', compression=zipfile.ZIP_DEFLATED) as zf:
            zf.writestr("%s.%s" % (id, file_type), data)
            zf.writestr("%s.content" % id, content_file)
            zf.writestr("%s.pagedata" % id, "")

        # with open("test.zip", "wb") as f:
        #     f.write(mf.getvalue())
        mf.seek(0)
        return metadata, mf
Exemplo n.º 8
0
    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)
Exemplo n.º 9
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()
Exemplo n.º 10
0
class Item(object):

    #
    # CTOR
    #
    def __init__(self, metadata, parent=None):
        self.metadata = metadata
        self._parent = parent
        self._children = []
        self.path = get_path(self.id())
        self.path_remapy = get_path_remapy(self.id())
        self.path_metadata_local = get_path_metadata_local(self.id())

        self.rm_client = RemarkableClient()
        self.state_listener = []

    #
    # Getter and setter
    #
    def is_trash(self):
        return self.id() == "trash"

    def is_root(self):
        return self.metadata is None

    def id(self):
        return self._meta_value("ID")

    def name(self):
        return self._meta_value("VissibleName")

    def version(self):
        return self._meta_value("Version", -1)

    def bookmarked(self):
        return self._meta_value("Bookmarked", False)

    def is_document(self):
        return self._meta_value("Type", "CollectionType") == "DocumentType"

    def is_collection(self):
        return self._meta_value("Type", "CollectionType") != "DocumentType"

    def modified_time(self):
        modified = self.metadata["ModifiedClient"]
        if modified == None:
            return None

        try:
            utc = datetime.strptime(modified, "%Y-%m-%dT%H:%M:%S.%fZ")
        except:
            utc = datetime.strptime(modified, "%Y-%m-%dT%H:%M:%SZ")

        try:
            epoch = time.mktime(utc.timetuple())
            offset = datetime.fromtimestamp(epoch) - datetime.utcfromtimestamp(
                epoch)
        except:
            print("(Warning) Failed to parse datetime for item %s" % self.id())
            return datetime(1970, 1, 1, 0, 0, 0)

        return utc + offset

    def parent(self):
        return self._parent

    def children(self):
        return self._children

    def _meta_value(self, key, root_value=""):
        if self.is_root():
            return root_value
        return self.metadata[key]

    #
    # Functions
    #
    def set_bookmarked(self, bookmarked):
        if self.is_trash() or self.is_root():
            return

        self.metadata["Bookmarked"] = bookmarked
        self.metadata["ModifiedClient"] = now_rfc3339()
        self.metadata["Version"] += 1
        self.rm_client.update_metadata(self.metadata)
        self._write_remapy_file()
        self._update_state_listener()

    def rename(self, new_name):
        if self.is_trash() or self.is_root():
            return

        self.metadata["VissibleName"] = new_name
        self.metadata["ModifiedClient"] = now_rfc3339()
        self.metadata["Version"] += 1
        self.rm_client.update_metadata(self.metadata)
        self._write_remapy_file()
        self._update_state_listener()

    def move(self, new_parent):
        if self.is_trash() or self.is_root():
            return

        self._parent = new_parent
        self.metadata["Parent"] = new_parent.id()
        self.metadata["ModifiedClient"] = now_rfc3339()
        self.metadata["Version"] += 1
        self.rm_client.update_metadata(self.metadata)
        self._write_remapy_file()
        self._update_state_listener()

    def add_state_listener(self, listener):
        self.state_listener.append(listener)

    def _update_state_listener(self):
        for listener in self.state_listener:
            listener(self)

    def _write_remapy_file(self):
        if self.is_root():
            return

        Path(self.path_remapy).mkdir(parents=True, exist_ok=True)
        with open(self.path_metadata_local, "w") as out:
            out.write(json.dumps(self.metadata, indent=4))