Initial commit

The only thing working is showing the users calendars in the sidebar.
This commit is contained in:
Jeena 2024-01-26 16:49:29 +09:00
commit b07ce820cd
30 changed files with 2314 additions and 0 deletions

0
src/__init__.py Normal file
View file

85
src/gsettings.py Normal file
View file

@ -0,0 +1,85 @@
# Copyright 2023 Vlad Krupinskii <mrvladus@yandex.ru>
# SPDX-License-Identifier: MIT
from gi.repository import GLib, Gio, Gtk
import gi
gi.require_version('Secret', '1')
from gi.repository import Secret
APP_ID = "net.jeena.jnotes"
SECRETS_SCHEMA = Secret.Schema.new(
APP_ID,
Secret.SchemaFlags.NONE,
{
"account": Secret.SchemaAttributeType.STRING,
},
)
class GSettings:
"""Class for accessing gsettings"""
gsettings: Gio.Settings = None
initialized: bool = False
def _check_init(self):
if not self.initialized:
self.init()
@classmethod
def bind(
self, setting: str, obj: Gtk.Widget, prop: str, invert: bool = False
) -> None:
self._check_init(self)
self.gsettings.bind(
setting,
obj,
prop,
Gio.SettingsBindFlags.INVERT_BOOLEAN
if invert
else Gio.SettingsBindFlags.DEFAULT,
)
@classmethod
def get(self, setting: str):
self._check_init(self)
return self.gsettings.get_value(setting).unpack()
@classmethod
def set(self, setting: str, gvariant: str, value) -> None:
self._check_init(self)
self.gsettings.set_value(setting, GLib.Variant(gvariant, value))
@classmethod
def get_secret(self, account: str):
self._check_init(self)
return Secret.password_lookup_sync(SECRETS_SCHEMA, {"account": account}, None)
@classmethod
def set_secret(self, account: str, secret: str) -> None:
self._check_init(self)
return Secret.password_store_sync(
SECRETS_SCHEMA,
{
"account": account,
},
Secret.COLLECTION_DEFAULT,
f"Errands account credentials for {account}",
secret,
None,
)
@classmethod
def init(self) -> None:
self.initialized = True
self.gsettings = Gio.Settings.new(APP_ID)
# Migrate old password
try:
password = self.gsettings.get_string("password")
if password:
self.set_secret("CalDAV", password)
self.gsettings.set_string("password", "") # Clean pass
except:
pass

9
src/jnotes.gresource.xml Normal file
View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/net/jeena/jnotes">
<file preprocess="xml-stripblanks">ui/window.ui</file>
<file preprocess="xml-stripblanks">ui/preferences.ui</file>
<file preprocess="xml-stripblanks">ui/help-overlay.ui</file>
<file preprocess="xml-stripblanks">ui/sidebar.ui</file>
</gresource>
</gresources>

46
src/jnotes.in Executable file
View file

@ -0,0 +1,46 @@
#!@PYTHON@
# jnotes.in
#
# Copyright 2024 Jeena
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later
import os
import sys
import signal
import locale
import gettext
VERSION = '@VERSION@'
pkgdatadir = '@pkgdatadir@'
localedir = '@localedir@'
sys.path.insert(1, pkgdatadir)
signal.signal(signal.SIGINT, signal.SIG_DFL)
locale.bindtextdomain('jnotes', localedir)
locale.textdomain('jnotes')
gettext.install('jnotes', localedir)
if __name__ == '__main__':
import gi
from gi.repository import Gio
resource = Gio.Resource.load(os.path.join(pkgdatadir, 'jnotes.gresource'))
resource._register()
from jnotes import main
sys.exit(main.main(VERSION))

97
src/main.py Normal file
View file

@ -0,0 +1,97 @@
# main.py
#
# Copyright 2024 Jeena
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later
import sys
import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Gio, Adw
from .window import JnotesWindow
from .preferences import PreferencesWindow
from .sidebar import Sidebar
from .sync import Sync
class JnotesApplication(Adw.Application):
"""The main application singleton class."""
calendar_set = None
def __init__(self):
super().__init__(application_id='net.jeena.jnotes',
flags=Gio.ApplicationFlags.DEFAULT_FLAGS)
self.create_action('quit', lambda *_: self.quit(), ['<primary>q'])
self.create_action('about', self.on_about_action)
self.create_action('preferences', self.on_preferences_action)
def do_activate(self):
"""Called when the application is activated.
We raise the application's main window, creating it if
necessary.
"""
win = self.props.active_window
if not win:
win = JnotesWindow(application=self)
win.present()
self.calendar_set = Sync.get_calendar_set()
if not self.calendar_set:
self.on_preferences_action(win, False)
else:
win.sidebar.set_calendars(self.calendar_set)
def on_about_action(self, widget, _):
"""Callback for the app.about action."""
about = Adw.AboutWindow(transient_for=self.props.active_window,
application_name='jnotes',
application_icon='net.jeena.jnotes',
developer_name='Jeena',
version='0.1.0',
developers=['Jeena'],
copyright='© 2024 Jeena')
about.present()
def on_preferences_action(self, widget, _):
"""Callback for the app.preferences action."""
preferences_window = PreferencesWindow(transient_for=self.props.active_window)
preferences_window.present()
def create_action(self, name, callback, shortcuts=None):
"""Add an application action.
Args:
name: the name of the action
callback: the function to be called when the action is
activated
shortcuts: an optional list of accelerators
"""
action = Gio.SimpleAction.new(name, None)
action.connect("activate", callback)
self.add_action(action)
if shortcuts:
self.set_accels_for_action(f"app.{name}", shortcuts)
def main(version):
"""The application's entry point."""
app = JnotesApplication()
return app.run(sys.argv)

39
src/meson.build Normal file
View file

@ -0,0 +1,39 @@
pkgdatadir = get_option('prefix') / get_option('datadir') / meson.project_name()
moduledir = pkgdatadir / 'jnotes'
gnome = import('gnome')
gnome.compile_resources('jnotes',
'jnotes.gresource.xml',
gresource_bundle: true,
install: true,
install_dir: pkgdatadir,
)
python = import('python')
conf = configuration_data()
conf.set('PYTHON', python.find_installation('python3').path())
conf.set('VERSION', meson.project_version())
conf.set('localedir', get_option('prefix') / get_option('localedir'))
conf.set('pkgdatadir', pkgdatadir)
configure_file(
input: 'jnotes.in',
output: 'jnotes',
configuration: conf,
install: true,
install_dir: get_option('bindir'),
install_mode: 'r-xr--r--'
)
jnotes_sources = [
'__init__.py',
'main.py',
'window.py',
'preferences.py',
'gsettings.py',
'sync.py',
'sidebar.py',
]
install_data(jnotes_sources, install_dir: moduledir)

80
src/preferences.py Normal file
View file

@ -0,0 +1,80 @@
# preferences.py
#
# Copyright 2024 Jeena
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later
from gi.repository import Adw
from gi.repository import Gtk
from gi.repository import GLib
from .gsettings import GSettings
from urllib.parse import urlparse
from .sync import Sync
@Gtk.Template(resource_path='/net/jeena/jnotes/ui/preferences.ui')
class PreferencesWindow(Adw.PreferencesWindow):
__gtype_name__ = 'PreferencesWindow'
server_url = Gtk.Template.Child()
username = Gtk.Template.Child()
password = Gtk.Template.Child()
spinner = Gtk.Template.Child()
test_connection_row = Gtk.Template.Child()
test_connection = Gtk.Template.Child()
def __init__(self, **kwargs):
super().__init__(**kwargs)
GSettings.bind("server-url", self.server_url, "text")
GSettings.bind("username", self.username, "text")
self.password.set_text(GSettings.get_secret("CalDAV"))
@Gtk.Template.Callback()
def on_test_connection_button_clicked(self, _btn):
server_url = self.server_url.get_text()
username = self.username.get_text()
password = self.password.get_text()
GLib.idle_add(self.spinner.start)
GLib.idle_add(self.deactivate_test_button)
Sync.test_connection(server_url, username, password, self.sync_ok_callback)
@Gtk.Template.Callback()
def on_validate(self, _lbl):
server_url = urlparse(self.server_url.get_text())
server_url_ok = bool(server_url.scheme and server_url.hostname)
username_ok = len(self.username.get_text()) > 0
password_ok = len(self.password.get_text()) > 0
if (server_url_ok and username_ok and password_ok):
self.activate_test_button()
else:
self.deactivate_test_button()
def activate_test_button(self):
self.test_connection.add_css_class("suggested-action")
self.test_connection.set_sensitive(True)
def deactivate_test_button(self):
self.test_connection.remove_css_class("suggested-action")
self.test_connection.set_sensitive(False)
def sync_ok_callback(self, ok, e=None):
self.activate_test_button()
self.spinner.stop()
if ok:
self.test_connection_row.set_icon_name("emblem-ok-symbolic")
else:
self.test_connection_row.set_icon_name("network-error-symbolic")

38
src/sidebar.py Normal file
View file

@ -0,0 +1,38 @@
# sidebar.py
#
# Copyright 2024 Jeena
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later
from gi.repository import Adw
from gi.repository import Gtk
@Gtk.Template(resource_path='/net/jeena/jnotes/ui/sidebar.ui')
class Sidebar(Adw.NavigationPage):
__gtype_name__ = 'Sidebar'
calendar_set = Gtk.Template.Child()
def __init__(self, **kwargs):
super().__init__(**kwargs)
def set_calendars(self, calendars):
self.calendar_set.bind_model(calendars, self.create_item_widget)
def create_item_widget(self, calendar):
return Gtk.Label(label=calendar.displayname, halign="start",
hexpand=True, ellipsize=3)

119
src/sync.py Normal file
View file

@ -0,0 +1,119 @@
# sync.py
#
# Copyright 2024 Jeena
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later
import caldav
import logging, requests
from gi.repository import Gtk, Gio, GObject
from threading import Thread
from typing import Callable
from .gsettings import GSettings
def threaded(function: Callable):
"""
Decorator to run function in thread.
Use GLib.idle_add(func) as callback at the end of threaded function
if you need to change UI from thread.
It's needed to be called to make changes in UI thread safe.
"""
def wrapper(*args, **kwargs):
Thread(target=function, args=args, kwargs=kwargs, daemon=True).start()
return wrapper
class Sync():
client = None
principal = None
@classmethod
@threaded
def test_connection(self, server_url, username, password, callback):
try:
client = caldav.DAVClient(server_url, username=username, password=password)
client.principal()
callback(True)
except caldav.lib.error.AuthorizationError as e:
logging.warning(e)
callback(False, e)
except requests.exceptions.ConnectionError as e:
logging.warning(e)
callback(False, e)
@classmethod
def init(self):
server_url = GSettings.get("server-url")
username = GSettings.get("username")
password = GSettings.get_secret("CalDAV")
try:
self.client = caldav.DAVClient(server_url, username=username, password=password)
self.principal = self.client.principal()
except caldav.lib.error.AuthorizationError as e:
logging.warning(e)
except requests.exceptions.ConnectionError as e:
logging.warning(e)
@classmethod
def get_calendar_set(self):
calendar_set = CalendarSet()
if not self.client:
self.init()
if self.principal:
remote_calendars = self.principal.calendars()
for remote_calendar in remote_calendars:
if "VJOURNAL" in remote_calendar.get_supported_components():
calendar = Calendar(remote_calendar.get_display_name(), remote_calendar.url)
for journal in remote_calendar.journals():
summary = journal.icalendar_component.get("summary", "")
description = journal.icalendar_component.get("description", "")
calendar.add_note(Note(calendar, summary, description))
calendar_set.add_calendar(calendar)
return calendar_set
class CalendarSet(Gio.ListStore):
def add_calendar(self, calendar):
self.append(calendar)
def remove_calendar(self, calendar):
self.remove(calendar)
class Calendar(Gio.ListStore):
displayname = None
cal_id = None
def __init__(self, displayname, cal_id):
super().__init__()
self.displayname = displayname
self.cal_id = cal_id
def add_note(self, note):
self.append(note)
class Note(GObject.GObject):
uid = None
calendar = None
summary = None
description = None
def __init__(self, calendar, summary, description):
super().__init__()
self.calendar = calendar
self.summary = summary
self.description = description

29
src/ui/help-overlay.ui Normal file
View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<object class="GtkShortcutsWindow" id="help_overlay">
<property name="modal">True</property>
<child>
<object class="GtkShortcutsSection">
<property name="section-name">shortcuts</property>
<property name="max-height">10</property>
<child>
<object class="GtkShortcutsGroup">
<property name="title" translatable="yes" context="shortcut window">General</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes" context="shortcut window">Show Shortcuts</property>
<property name="action-name">win.show-help-overlay</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes" context="shortcut window">Quit</property>
<property name="action-name">app.quit</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</interface>

54
src/ui/preferences.ui Normal file
View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0" />
<template class="PreferencesWindow" parent="AdwPreferencesWindow">
<property name="search_enabled">False</property>
<child>
<object class="AdwPreferencesPage">
<property name="name" translatable="yes">Sync</property>
<property name="title" translatable="yes">Sync information</property>
<child>
<object class="AdwPreferencesGroup">
<property name="name" translatable="yes">CalDav</property>
<property name="title" translatable="yes">CalDav Sync Server</property>
<child>
<object class="AdwEntryRow" id="server_url">
<property name="title" translatable="yes">Server URL</property>
<signal name="changed" handler="on_validate" swapped="no" />
</object>
</child>
<child>
<object class="AdwEntryRow" id="username">
<property name="title" translatable="yes">Username</property>
<signal name="changed" handler="on_validate" swapped="no" />
</object>
</child>
<child>
<object class="AdwPasswordEntryRow" id="password">
<property name="title" translatable="yes">Password</property>
<signal name="changed" handler="on_validate" swapped="no" />
</object>
</child>
<child>
<object class="AdwActionRow" id="test_connection_row">
<property name="title">Test Connection</property>
<child>
<object class="GtkSpinner" id="spinner"></object>
</child>
<child>
<object class="GtkButton" id="test_connection">
<property name="label" translatable="yes">_Test</property>
<property name="valign">center</property>
<property name="use_underline">True</property>
<property name="sensitive">False</property>
<signal name="clicked" handler="on_test_connection_button_clicked" swapped="no" />
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</template>
</interface>

34
src/ui/sidebar.ui Normal file
View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<template class="Sidebar" parent="AdwNavigationPage">
<property name="title">Notes</property>
<property name="child">
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar">
<child type="start">
<object class="GtkToggleButton">
<property name="icon-name">list-add-symbolic</property>
<property name="tooltip-text" translatable="yes">New Collection</property>
<property name="action-name">win.new-collection</property>
</object>
</child>
</object>
</child>
<property name="content">
<object class="GtkScrolledWindow">
<property name="child">
<object class="GtkListBox" id="calendar_set">
<style>
<class name="navigation-sidebar"/>
</style>
</object>
</property>
</object>
</property>
</object>
</property>
</template>
</interface>

101
src/ui/window.ui Normal file
View file

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<menu id="main-menu">
<section>
<item>
<attribute name="label" translatable="yes">_Preferences</attribute>
<attribute name="action">app.preferences</attribute>
<attribute name="accel">&lt;Primary&gt;comma</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Keyboard Shortcuts</attribute>
<attribute name="action">win.show-help-overlay</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_About</attribute>
<attribute name="action">app.about</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Quit</attribute>
<attribute name="action">app.quit</attribute>
<attribute name="accel">&lt;Primary&gt;q</attribute>
</item>
</section>
</menu>
<template class="JnotesWindow" parent="AdwApplicationWindow">
<property name="title" translatable="yes">To-Do</property>
<property name="width-request">800</property>
<property name="height-request">800</property>
<child>
<object class="AdwBreakpoint">
<condition>max-width: 500sp</condition>
<setter object="split_view" property="collapsed">True</setter>
</object>
</child>
<property name="content">
<object class="AdwNavigationSplitView" id="split_view">
<property name="min-sidebar-width">200</property>
<property name="sidebar">
<object class="Sidebar" id="sidebar">
</object>
</property>
<property name="content">
<object class="AdwNavigationPage">
<property name="title" translatable="yes">Note</property>
<property name="child">
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar">
<property name="show-title">False</property>
<child type="end">
<object class="GtkMenuButton">
<property name="icon-name">open-menu-symbolic</property>
<property name="menu-model">main-menu</property>
<property name="tooltip-text" translatable="yes">Main Menu</property>
</object>
</child>
</object>
</child>
<property name="content">
<object class="GtkScrolledWindow">
<property name="child">
<object class="AdwClamp">
<property name="maximum-size">400</property>
<property name="tightening-threshold">300</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="spacing">12</property>
<child>
<object class="GtkEntry" id="entry">
<property name="placeholder-text" translatable="yes">Create new Note…</property>
<property name="secondary-icon-name">list-add-symbolic</property>
</object>
</child>
<child>
<object class="GtkListBox" id="tasks_list">
<property name="visible">False</property>
<property name="selection-mode">none</property>
<style>
<class name="boxed-list" />
</style>
</object>
</child>
</object>
</property>
</object>
</property>
</object>
</property>
</object>
</property>
</object>
</property>
</object>
</property>
</template>
</interface>

31
src/window.py Normal file
View file

@ -0,0 +1,31 @@
# window.py
#
# Copyright 2024 Jeena
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later
from gi.repository import Adw
from gi.repository import Gtk
@Gtk.Template(resource_path='/net/jeena/jnotes/ui/window.ui')
class JnotesWindow(Adw.ApplicationWindow):
__gtype_name__ = 'JnotesWindow'
sidebar = Gtk.Template.Child()
def __init__(self, **kwargs):
super().__init__(**kwargs)