# main.py # # Copyright 2020 Morgan McMillian # # 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 . import sys import gi import os import pnutpy import logging gi.require_version('Gdk', '3.0') gi.require_version('Gtk', '3.0') from gi.repository import GObject, Gdk, Gtk, Gio, GLib gi.require_version('Handy', '1') from gi.repository import Handy Handy.init() class Application(Gtk.Application): def __init__(self): super().__init__(application_id='dev.thrrgilag.squeak', flags=Gio.ApplicationFlags.FLAGS_NONE) def do_startup(self): Gtk.Application.do_startup(self) self.authorized = False self.keyfile = GLib.KeyFile() self.config_dir = GLib.get_user_config_dir() self.config_file = os.path.join(self.config_dir, "squeak") try: self.keyfile.load_from_file(self.config_file, GLib.KeyFileFlags.KEEP_COMMENTS) access_token = self.keyfile.get_string("GENERAL", "ACCESS_TOKEN") if len(access_token) > 0: pnutpy.api.add_authorization_token(access_token) self.authorized = True except GLib.Error: pass def do_activate(self): win = self.props.active_window if not win: win = Gtk.ApplicationWindow(application=self) win.set_default_size(320, 480) title_bar = Handy.TitleBar() win.set_titlebar(title_bar) self.header = Handy.HeaderBar(show_close_button=True) title_bar.add(self.header) self.stack = Gtk.Stack() stack_switcher_bar = Handy.ViewSwitcherBar() stack_switcher_bar.set_stack(self.stack) switcher_title = Handy.ViewSwitcherTitle() switcher_title.set_stack(self.stack) self.header.set_custom_title(switcher_title) self.header.bind_property("title", switcher_title, "title", GObject.BindingFlags.SYNC_CREATE) self.header.bind_property("subtitle", switcher_title, "subtitle", GObject.BindingFlags.SYNC_CREATE) switcher_title.bind_property("title-visible", stack_switcher_bar, "reveal", GObject.BindingFlags.SYNC_CREATE) if self.authorized: self.show_main_page() else: self.show_login_page() vbox = Gtk.Box(orientation='vertical') vbox.pack_start(self.stack, True, True, 0) vbox.pack_end(stack_switcher_bar, False, False, 0) win.add(vbox) win.show_all() def blarp(self, args=None): logging.debug("BLARP") logging.debug(self.keyfile) def handle_login(self, args, code): # TODO: should do some actual error handling here self.keyfile.set_string("GENERAL", "ACCESS_TOKEN", code) self.keyfile.save_to_file(self.config_file) pnutpy.api.add_authorization_token(code) self.authorized = True self.show_main_page() def show_login_page(self): login_page = LoginPage() login_page.connect("login", self.handle_login) self.stack.add_titled(login_page, "login", "Login") self.header.show_all() def show_main_page(self): login_page = self.stack.get_child_by_name("login") if login_page is not None: self.stack.remove(login_page) unified = Timeline('unified') self.stack.add_titled(unified, "unified", "Timeline") mentions = Timeline('mentions') self.stack.add_titled(mentions, "mentions", "Mentions") bookmarks = Timeline('bookmarks') self.stack.add_titled(bookmarks, "bookmarks", "Bookmarks") global_tl = Timeline('global') self.stack.add_titled(global_tl, "global", "Global") reload_button = Gtk.Button.new_from_icon_name('view-refresh-symbolic', 1) reload_button.connect('clicked', self.emit_refresh) self.header.pack_start(reload_button) new_post_button = Gtk.Button.new_from_icon_name('list-add-symbolic', 1) self.header.pack_start(new_post_button) self.header.show_all() self.stack.show_all() def emit_refresh(self, button): timeline = self.stack.get_visible_child() timeline.emit('refresh') class LoginPage(Gtk.Box): __gsignals__ = { 'login': (GObject.SIGNAL_RUN_FIRST, None, (str,)) } def __init__(self): super().__init__(orientation='vertical') self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) client_id = "1PiUzxfX_CQxKvtz93lUzPX9-FMtz-va" redirect_uri = "urn:ietf:wg:oauth:2.0:oob" scope = "basic,stream,write_post,follow,presence,messages,files,polls" uri = "https://pnut.io/oauth/authenticate" uri += "?client_id=" + client_id uri += "&redirect_uri=" + redirect_uri uri += "&scope=" + scope uri += "&response_type=token" self.login_button = Gtk.LinkButton.new_with_label(uri, "Log In to pnut.io") self.login_button.connect("clicked", self.prompt_code) self.set_center_widget(self.login_button) def prompt_code(self, button): self.remove(self.login_button) label = Gtk.Label() label.set_markup('Enter authorization code') self.code = Gtk.Entry() paste_button = Gtk.Button(label="Paste from clipboard") paste_button.connect("clicked", self.paste_code) cancel_button = Gtk.Button(label="Cancel") cancel_button.connect("clicked", self.cancel_login) confirm_button = Gtk.Button(label="Confirm") confirm_button.connect("clicked", self.confirm_login) lbox = Gtk.Box(orientation='horizontal') lbox.pack_start(cancel_button, True, True, 0) lbox.pack_start(confirm_button, True, True, 0) vbox = Gtk.Box(orientation='vertical') vbox.pack_start(label, False, False, 10) vbox.pack_start(self.code, False, False, 10) vbox.pack_start(paste_button, False, False, 10) vbox.add(lbox) hbox = Gtk.Box(orientation='horizontal') hbox.set_center_widget(vbox) self.set_center_widget(hbox) self.show_all() def paste_code(self, button): text = self.clipboard.wait_for_text() if text is not None: self.code.set_text(text) def cancel_login(self, button): # TODO: something actually useful here logging.debug("uh cancel i guess") def confirm_login(self, button): code = self.code.get_text() self.emit('login', code) class Timeline(Gtk.Box): __gsignals__ = { 'refresh': (GObject.SIGNAL_RUN_FIRST, None, ()) } def __init__(self, stream): super().__init__(orientation='vertical') scroller = Gtk.ScrolledWindow( halign='fill', kinetic_scrolling=True ) self.view = Gtk.ListBox( selection_mode=Gtk.SelectionMode.NONE ) scroller.add(self.view) self.pack_start(scroller, True, True, 0) self.stream = stream self.load_timeline() def load_timeline(self): if self.stream == 'unified': posts, meta = pnutpy.api.users_post_streams_unified() elif self.stream == 'mentions': posts, meta = pnutpy.api.users_mentioned_posts('me') elif self.stream == 'bookmarks': posts, meta = pnutpy.api.users_bookmarked_posts('me') else: posts, meta = pnutpy.api.posts_streams_global() for item in posts: if 'is_deleted' in item: continue self.view.add(PostItem(item)) def do_refresh(self): rows = self.view.get_children() for item in rows: self.view.remove(item) self.load_timeline() self.show_all() class PostItem(Gtk.ListBoxRow): def __init__(self, post): super(Gtk.ListBoxRow, self).__init__() self.post = post self.box = Gtk.Box(orientation='vertical') self.add(self.box) # name container self.name_box = Gtk.Box(orientation='vertical') self.username = Gtk.Label(label="@" + post.user.username, xalign=0) self.name = Gtk.Label(xalign=0) if 'name' in post.user: self.name.set_markup(f"{post.user.name}") self.name_box.pack_start(self.name, True, True, 0) self.name_box.pack_start(self.username, True, True, 0) # header container self.h_box = Gtk.Box(orientation='horizontal') self.avatar = Handy.Avatar(size=32) # TODO: get the actual image self.h_box.pack_start(self.avatar, False, False, 18) self.h_box.pack_start(self.name_box, False, False, 0) # content container self.c_box = Gtk.Box(orientation='horizontal') self.content = Gtk.Label(wrap=True, xalign=0) # TODO: parse content links if 'content' in post: self.content.set_text(post.content.text) # TODO: add media self.c_box.pack_start(self.content, True, True, 18) self.box.pack_start(self.h_box, True, True, 10) self.box.pack_start(self.c_box, True, True, 10) def main(version): logging.basicConfig(level=logging.DEBUG) app = Application() return app.run(sys.argv)