diff --git a/build-aux/python3-modules.json b/build-aux/python3-modules.json new file mode 100644 index 00000000..a7fc096b --- /dev/null +++ b/build-aux/python3-modules.json @@ -0,0 +1,100 @@ +{ + "name": "python3-modules", + "buildsystem": "simple", + "build-commands": [], + "modules": [ + { + "name": "python3-caldav", + "buildsystem": "simple", + "build-commands": [ + "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"caldav\" --no-build-isolation" + ], + "sources": [ + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/fa/23/c3ca4d5c91326f891c2352a77d0bb0a7adc4b6f93b4b32edfba8d766ebb5/caldav-1.3.6-py3-none-any.whl", + "sha256": "ec48bae60d23ad8c409508048033fbe40f7bf4e6e3bc1112eaac71085a92f9e7" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/4c/dd/2234eab22353ffc7d94e8d13177aaa050113286e93e7b40eae01fbf7c3d9/certifi-2023.7.22-py3-none-any.whl", + "sha256": "92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/2a/53/cf0a48de1bdcf6ff6e1c9a023f5f523dfe303e4024f216feac64b6eb7f67/charset-normalizer-3.2.0.tar.gz", + "sha256": "3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/b2/50/36ade5c5b635807b566506aac89543c23be49e9657d1ba7edb3f0ee9b35b/icalendar-5.0.9-py3-none-any.whl", + "sha256": "f473fb5c1b255f53c9296b56887307cce4f215a65491b35c4f08f67ac05ac95b" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/fc/34/3030de6f1370931b9dbb4dad48f6ab1015ab1d32447850b9fc94e60097be/idna-3.4-py3-none-any.whl", + "sha256": "90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/30/39/7305428d1c4f28282a4f5bdbef24e0f905d351f34cf351ceb131f5cddf78/lxml-4.9.3.tar.gz", + "sha256": "48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/36/7a/87837f39d0296e723bb9b62bbb257d0355c7f6128853c78955f57342a56d/python_dateutil-2.8.2-py2.py3-none-any.whl", + "sha256": "961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/32/4d/aaf7eff5deb402fd9a24a1449a8119f00d74ae9c2efa79f8ef9994261fc2/pytz-2023.3.post1-py2.py3-none-any.whl", + "sha256": "ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/67/dd/b71c3e0319ec14103db7ea9c6b5e997b27abe99057200ed1b1e9ae9466d1/recurring_ical_events-2.1.0-py3-none-any.whl", + "sha256": "9cddb5ac83b08f06c836c76211eb5f7804fda224cdf9f2cbb3efbb2a5ce07947" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl", + "sha256": "58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/84/d2/730a87f0dbf184760394a85088d0d2366a5a8a32bc32ffd869a83f1de854/tzlocal-5.0.1-py3-none-any.whl", + "sha256": "f3596e180296aaf2dbd97d124fe76ae3a0e3d32b258447de7b939b3fd4be992f" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/37/dc/399e63f5d1d96bb643404ee830657f4dfcf8503f5ba8fa3c6d465d0c57fe/urllib3-2.0.5-py3-none-any.whl", + "sha256": "ef16afa8ba34a1f989db38e1dbbe0c302e4289a47856990d0682e374563ce35e" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/da/ce/27c48c0e39cc69ffe7f6e3751734f6073539bf18a0cfe564e973a3709a52/vobject-0.9.6.1.tar.gz", + "sha256": "96512aec74b90abb71f6b53898dd7fe47300cc940104c4f79148f0671f790101" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/c6/38/05bcb43df0db48922426845380eeaea88875807050b9ac4aff099d775ae5/x_wr_timezone-0.0.5-py3-none-any.whl", + "sha256": "e438b27b96635f5f712a4fb5dda4c82597a53a412fe834c9fe8409fddb3fc2b1" + } + ] + }, + { + "name": "python3-lxml", + "buildsystem": "simple", + "build-commands": [ + "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"lxml\" --ignore-installed --no-build-isolation" + ], + "sources": [ + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/30/39/7305428d1c4f28282a4f5bdbef24e0f905d351f34cf351ceb131f5cddf78/lxml-4.9.3.tar.gz", + "sha256": "48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c" + } + ] + } + ] +} \ No newline at end of file diff --git a/build-aux/python3-nextcloud-tasks-api.json b/build-aux/python3-nextcloud-tasks-api.json deleted file mode 100644 index d003b9e9..00000000 --- a/build-aux/python3-nextcloud-tasks-api.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "python3-nextcloud-tasks-api", - "buildsystem": "simple", - "build-commands": [ - "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"nextcloud-tasks-api\" --no-build-isolation" - ], - "sources": [ - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/4c/dd/2234eab22353ffc7d94e8d13177aaa050113286e93e7b40eae01fbf7c3d9/certifi-2023.7.22-py3-none-any.whl", - "sha256": "92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/2a/53/cf0a48de1bdcf6ff6e1c9a023f5f523dfe303e4024f216feac64b6eb7f67/charset-normalizer-3.2.0.tar.gz", - "sha256": "3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/fc/34/3030de6f1370931b9dbb4dad48f6ab1015ab1d32447850b9fc94e60097be/idna-3.4-py3-none-any.whl", - "sha256": "90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/db/77/73b90042bacd18233bf3ed305ea130255cd99edb966b17b92705c32211c1/nextcloud_tasks_api-0.0.5-py3-none-any.whl", - "sha256": "e341373805de776e23e00c6ebd62b7df72cec4d1d13a19157c747a58433384c7" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/eb/37/791f1a6edd13c61cac85282368aa68cb0f3f164440fdf60032f2cc6ca34e/prompt_toolkit-3.0.36-py3-none-any.whl", - "sha256": "aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/0b/e7/2dd8f59d1d328773505f78b85405ddb1cfe74126425d076ce72e65540b8b/questionary-2.0.1-py3-none-any.whl", - "sha256": "8ab9a01d0b91b68444dff7f6652c1e754105533f083cbe27597c8110ecc230a2" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl", - "sha256": "58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/37/dc/399e63f5d1d96bb643404ee830657f4dfcf8503f5ba8fa3c6d465d0c57fe/urllib3-2.0.5-py3-none-any.whl", - "sha256": "ef16afa8ba34a1f989db38e1dbbe0c302e4289a47856990d0682e374563ce35e" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/20/f4/c0584a25144ce20bfcf1aecd041768b8c762c1eb0aa77502a3f0baa83f11/wcwidth-0.2.6-py2.py3-none-any.whl", - "sha256": "795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e" - } - ] -} \ No newline at end of file diff --git a/io.github.mrvladus.List.Devel.json b/io.github.mrvladus.List.Devel.json index 1852061b..870cbf59 100644 --- a/io.github.mrvladus.List.Devel.json +++ b/io.github.mrvladus.List.Devel.json @@ -12,7 +12,7 @@ "--share=network" ], "modules": [ - "build-aux/python3-nextcloud-tasks-api.json", + "build-aux/python3-modules.json", { "name": "errands", "buildsystem": "meson", diff --git a/src/sync.py b/src/sync.py index f82d2955..e9d34e95 100644 --- a/src/sync.py +++ b/src/sync.py @@ -1,8 +1,14 @@ from gi.repository import Adw - +from caldav import Calendar, DAVClient from .utils import GSettings, Log, TaskUtils, UserData, threaded -from nextcloud_tasks_api import NextcloudTasksApi, TaskFile, get_nextcloud_tasks_api -from nextcloud_tasks_api.ical import Task + +# from nextcloud_tasks_api import ( +# NextcloudTasksApi, +# TaskFile, +# TaskList, +# get_nextcloud_tasks_api, +# ) +# from nextcloud_tasks_api.ical import Task class Sync: @@ -21,22 +27,22 @@ def sync(self) -> None: if not self.window.can_sync: return for provider in self.providers: - provider.sync() + if provider.can_sync: + provider.sync() def _setup_providers(self) -> None: pass class SyncProviderNextcloud: - connected: bool = False + can_sync: bool = False + tasks_list: list[Calendar] = [] - def __init__(self) -> None: + def __init__(self): if not GSettings.get("nc-enabled"): Log.debug("Nextcloud sync disabled") return - Log.debug("Initialize Nextcloud sync provider") - self.url = GSettings.get("nc-url") self.username = GSettings.get("nc-username") self.password = GSettings.get("nc-password") @@ -45,113 +51,162 @@ def __init__(self) -> None: Log.error("Not all Nextcloud credentials provided") return - self.connect() + self.url = f"{self.url}/remote.php/dav/" - def connect(self) -> None: - Log.info(f"Connecting to Nextcloud at '{self.url}' as user '{self.username}'") - self.api: NextcloudTasksApi = get_nextcloud_tasks_api( - self.url, self.username, self.password - ) + client = DAVClient(url=self.url, username=self.username, password=self.password) try: - self.errands_task_list = None - for task_list in self.api.get_lists(): - if task_list.name == "Errands": - self.errands_task_list = task_list - - if not self.errands_task_list: - Log.debug("Creating new list 'Errands'") - self.errands_task_list = self.api.create_list("Errands") - - Log.info("Connected to Nextcloud") + principal = client.principal() + Log.debug(f"Connected to Nextcloud DAV server at '{self.url}'") + self.can_sync = True except: - Log.error("Can't connect to Nextcloud server") - return None - - def get_tasks(self) -> list[TaskFile] | None: - if not GSettings.get("nc-enabled"): + Log.error(f"Can't connect to Nextcloud DAV server at '{self.url}'") + self.can_sync = False return - try: - Log.debug("Getting tasks from Nextcloud") - tasks = self.api.get_list(self.errands_task_list) - return [task for task in tasks] - except: - Log.error("Can't connect to Nextcloud server") - return None - def sync(self) -> None: - if not GSettings.get("nc-enabled"): - return - Log.info("Sync tasks with Nextcloud") - - data: dict = UserData.get() - nc_ids: list[str] = [Task(t.content).uid for t in self.get_tasks()] - to_delete: list[dict] = [] - - for task in data["tasks"]: - # Create new task on NC that was created offline - if task["id"] not in nc_ids and not task["synced_nc"]: - new_task = Task() - new_task.summary = task["text"] - new_task.related_to = task["parent"] - if task["completed"]: - new_task.data.upsert_value("STATUS", "COMPLETED") - new_task.data.upsert_value("ERRANDS-COLOR", task["color"]) - created_task = self.api.create( - self.errands_task_list, new_task.to_string() - ) - task["id"] = Task(created_task.content).uid - task["synced"] = True - - # Delete local task that was deleted on NC - elif task["id"] not in nc_ids and task["synced_nc"]: - to_delete.append(task) - - # Update task that was changed locally - elif task["id"] in nc_ids and not task["synced_nc"]: - updated_task = Task() - updated_task.summary = task["text"] - updated_task.related_to = task["parent"] - if task["completed"]: - updated_task.data.upsert_value("STATUS", "COMPLETED") - updated_task.data.upsert_value("ERRANDS-COLOR", task["color"]) - for nc_task in self.get_tasks(): - if Task(nc_task.content).uid == task["id"]: - nc_task.content = updated_task.to_string() - self.api.update(nc_task) - break - task["synced_nc"] = True - - # Update task that was changed on NC - elif task["id"] in nc_ids and task["synced_nc"]: - for nc_task in self.get_tasks(): - task_obj = Task(nc_task.content) - if task_obj.uid == task["id"]: - task["text"] = task_obj.summary - task["parent"] = task_obj.related_to - task["completed"] = ( - task_obj.data.find_value("STATUS") == "COMPLETED" - ) - task["color"] = task_obj.data.find_value("ERRANDS-COLOR") - break - - # Remove deleted tasks from data - for task in to_delete: - data["tasks"].remove(task) - - # Create new local task that was created on NC - l_ids: list = [t["id"] for t in data["tasks"]] - for nc_task in self.get_tasks(): - task_obj = Task(nc_task.content) - if task_obj.uid not in l_ids: - new_task = TaskUtils.new_task( - task_obj.summary, - task_obj.uid, - task_obj.related_to, - task_obj.data.find_value("STATUS") == "COMPLETED", - ) - data["tasks"].append(new_task) - - UserData.set(data) + calendars = principal.calendars() + errands_cal_exists: bool = False + for cal in calendars: + if cal.name == "Errands": + self.tasks_list = cal + errands_cal_exists = True + if not errands_cal_exists: + Log.debug("Create new calendar 'Errands' on Nextcloud") + self.tasks_list = principal.make_calendar( + "Errands", supported_calendar_component_set=["VTODO"] + ) + + +# class SyncProviderNextcloud: +# connected: bool = False +# disabled: bool = False + +# def __init__(self) -> None: +# if not GSettings.get("nc-enabled"): +# Log.debug("Nextcloud sync disabled") +# return + +# Log.debug("Initialize Nextcloud sync provider") + +# self.url = GSettings.get("nc-url") +# self.username = GSettings.get("nc-username") +# self.password = GSettings.get("nc-password") + +# if self.url == "" or self.username == "" or self.password == "": +# Log.error("Not all Nextcloud credentials provided") +# return + +# self.connect() + +# def connect(self) -> None: +# Log.info(f"Connecting to Nextcloud at '{self.url}' as user '{self.username}'") +# self.api: NextcloudTasksApi = get_nextcloud_tasks_api( +# self.url, self.username, self.password +# ) +# try: +# self.errands_task_list: TaskList = None +# for task_list in self.api.get_lists(): +# if task_list.name == "Errands": +# self.errands_task_list = task_list + +# if not self.errands_task_list: +# Log.debug("Creating new list 'Errands'") +# self.errands_task_list = self.api.create_list("Errands") + +# self.connected = True +# Log.info("Connected to Nextcloud") +# except: +# Log.error("Can't connect to Nextcloud server") +# return None + +# def get_tasks(self) -> list[TaskFile] | None: +# if self.disabled or not self.connected: +# return + +# try: +# Log.debug("Getting tasks from Nextcloud") +# tasks = self.api.get_list(self.errands_task_list) +# return [task for task in tasks] +# except: +# Log.error("Can't connect to Nextcloud server") +# return None + +# def sync(self) -> None: +# if self.disabled or not self.connected: +# return + +# Log.info("Sync tasks with Nextcloud") + +# data: dict = UserData.get() +# nc_ids: list[str] = [Task(t.content).uid for t in self.get_tasks()] +# to_delete: list[dict] = [] + +# for task in data["tasks"]: +# # Create new task on NC that was created offline +# if task["id"] not in nc_ids and not task["synced_nc"]: +# new_task = Task() +# new_task.summary = task["text"] +# new_task.related_to = task["parent"] +# if task["completed"]: +# new_task.data.upsert_value("STATUS", "COMPLETED") +# new_task.data.upsert_value("ERRANDS-COLOR", task["color"]) +# created_task = self.api.create( +# self.errands_task_list, new_task.to_string() +# ) +# task["id"] = Task(created_task.content).uid +# task["synced"] = True + +# # Delete local task that was deleted on NC +# elif task["id"] not in nc_ids and task["synced_nc"]: +# to_delete.append(task) + +# # Update task that was changed locally +# elif task["id"] in nc_ids and not task["synced_nc"]: +# updated_task = Task() +# updated_task.summary = task["text"] +# updated_task.related_to = task["parent"] +# if task["completed"]: +# updated_task.data.upsert_value("STATUS", "COMPLETED") +# updated_task.data.upsert_value("ERRANDS-COLOR", task["color"]) +# for nc_task in self.get_tasks(): +# if Task(nc_task.content).uid == task["id"]: +# nc_task.content = updated_task.to_string() +# self.api.update(nc_task) +# break +# task["synced_nc"] = True + +# # Update task that was changed on NC +# elif task["id"] in nc_ids and task["synced_nc"]: +# for nc_task in self.get_tasks(): +# task_obj = Task(nc_task.content) +# if task_obj.uid == task["id"]: +# task["text"] = task_obj.summary +# task["parent"] = task_obj.related_to +# task["completed"] = ( +# task_obj.data.find_value("STATUS") == "COMPLETED" +# ) +# task["color"] = task_obj.data.find_value("ERRANDS-COLOR") +# break + +# # Remove deleted tasks from data +# for task in to_delete: +# data["tasks"].remove(task) + +# # Create new local task that was created on NC +# l_ids: list = [t["id"] for t in data["tasks"]] +# for nc_task in self.get_tasks(): +# task_obj = Task(nc_task.content) +# if task_obj.uid not in l_ids: +# new_task = TaskUtils.new_task( +# task_obj.summary, +# task_obj.uid, +# task_obj.related_to or "", +# task_obj.data.find_value("STATUS") == "COMPLETED", +# False, +# True, +# ) +# data["tasks"].append(new_task) + +# UserData.set(data) class SyncProviderTodoist: diff --git a/src/task.py b/src/task.py index a3a52ad9..cfc8e345 100644 --- a/src/task.py +++ b/src/task.py @@ -188,6 +188,7 @@ def on_completed_btn_toggled(self, btn: Gtk.Button) -> None: # Update data self.task["completed"] = btn.props.active + self.task["synced_nc"] = False self.update_data() # Update children for task in get_children(self.tasks_list): @@ -204,6 +205,7 @@ def on_completed_btn_toggled(self, btn: Gtk.Button) -> None: self.text = Markup.rm_crossline(self.text) self.remove_css_class("task-completed") self.task_row.props.title = self.text + Sync.sync() @Gtk.Template.Callback() def on_expand(self, *_) -> None: @@ -265,6 +267,7 @@ def on_task_edit(self, entry: Gtk.Entry) -> None: self.task_row.props.title = self.text # Toggle checkbox self.completed_btn.props.active = self.task["completed"] = False + self.task["synced_nc"] = False self.update_data() # Exit edit mode self.toggle_edit_mode() @@ -289,7 +292,9 @@ def on_style_selected(self, btn: Gtk.Button) -> None: self.main_box.add_css_class(f"task-{color}") # Set new color self.task["color"] = color + self.task["synced_nc"] = False self.update_data() + Sync.sync() # --- Drag and Drop --- # @@ -340,6 +345,7 @@ def on_task_top_drop(self, _drop, task, _x, _y) -> bool: # Change parent if different parents task.task["parent"] = self.task["parent"] + task.task["synced_nc"] = False task.update_data() task.purge() # Add new task widget @@ -351,6 +357,7 @@ def on_task_top_drop(self, _drop, task, _x, _y) -> bool: # Update status self.parent.update_status() task.parent.update_status() + Sync.sync() return True @@ -365,6 +372,7 @@ def on_drop(self, _drop, task, _x, _y) -> None: # Change parent task.task["parent"] = self.task["id"] + task.task["synced_nc"] = False task.update_data() # Move data data = UserData.get() @@ -388,5 +396,6 @@ def on_drop(self, _drop, task, _x, _y) -> None: # Update status task.parent.update_status() self.update_status() + Sync.sync() return True diff --git a/src/utils.py b/src/utils.py index c943ade6..e3755f2d 100644 --- a/src/utils.py +++ b/src/utils.py @@ -224,7 +224,8 @@ def new_task( pid: str = "", cmpd: bool = False, dltd: bool = False, - synced: bool = False, + synced_nc: bool = False, + synced_td: bool = False, ) -> dict: return { "id": self.generate_id() if not id else id, @@ -233,8 +234,8 @@ def new_task( "color": "", "completed": cmpd, "deleted": dltd, - "synced_nc": synced, - "synced_td": synced, + "synced_nc": synced_nc, + "synced_td": synced_td, }