# 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)