diff --git a/client/assets/bg.png b/client/assets/bg.png new file mode 100644 index 0000000..8fe50c7 Binary files /dev/null and b/client/assets/bg.png differ diff --git a/client/assets/komrade-peek-2.gif b/client/assets/komrade-peek-2.gif new file mode 100644 index 0000000..2d39204 Binary files /dev/null and b/client/assets/komrade-peek-2.gif differ diff --git a/client/main.py b/client/main.py index 2cca867..56566c0 100644 --- a/client/main.py +++ b/client/main.py @@ -35,6 +35,7 @@ from kivy.storage.jsonstore import JsonStore from kivy.core.window import Window from kivy.core.text import LabelBase import shutil +from kivy.uix.image import Image Window.size = WINDOW_SIZE @@ -92,6 +93,19 @@ def get_tor_python_session(): with tor_requests.get_session() as s: return s +def draw_background(widget, img_fn='assets/bg.png'): + from kivy.core.image import Image as CoreImage + from kivy.graphics import Color, Rectangle + widget.canvas.before.clear() + with widget.canvas.before: + Color(.4, .4, .4, 1) + texture = CoreImage(img_fn).texture + texture.wrap = 'repeat' + nx = float(widget.width) / texture.width + ny = float(widget.height) / texture.height + Rectangle(pos=widget.pos, size=widget.size, texture=texture, + tex_coords=(0, 0, nx, 0, nx, ny, 0, ny)) + class MainApp(MDApp): title = 'Komrade' @@ -102,6 +116,8 @@ class MainApp(MDApp): store = JsonStore('komrade.json') login_expiry = 60 * 60 * 24 * 7 # once a week #login_expiry = 5 # 5 seconds + texture = ObjectProperty() + def get_session(self): # return get_async_tor_proxy_session() @@ -115,6 +131,11 @@ class MainApp(MDApp): return '' def build(self): + # bind bg texture + # self.texture = Image(source='assets/bg.png').texture + # self.texture.wrap = 'clamp_to_edge' + # self.texture.uvsize = (-2, -2) + self.username='' # bind global app,root @@ -122,6 +143,7 @@ class MainApp(MDApp): #self.username = self.store.get('userd').get('username') self.load_store() self.root = root = Builder.load_file('root.kv') + draw_background(self.root) # edit logo logo=root.ids.toolbar.ids.label_title @@ -179,7 +201,7 @@ class MainApp(MDApp): self.logged_in=True self.username=un # self.store.put('username',un) - self.store.put('user',username=un,logged_in=True,logged_in_when=time.time()) + # self.store.put('user',username=un,logged_in=True,logged_in_when=time.time()) self.root.change_screen('feed') @@ -291,6 +313,14 @@ class MainApp(MDApp): return jsond['posts'] return [] + def get_my_posts(self): + with self.get_session() as sess: + with sess.get(self.api+'/posts/'+self.username) as r: + log(r.text) + jsond=r.json() + return jsond['posts'] + return [] + def get_posts_async(self): result=[] with self.get_session() as sess: diff --git a/client/misc.py b/client/misc.py new file mode 100644 index 0000000..03175ed --- /dev/null +++ b/client/misc.py @@ -0,0 +1,186 @@ +from kivy.animation import Animation +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import ( + BooleanProperty, + ListProperty, + NumericProperty, + ObjectProperty, + StringProperty, +) +from kivy.uix.boxlayout import BoxLayout + +from kivymd.theming import ThemableBehavior +from kivymd.uix.button import MDIconButton +from kivymd.uix.stacklayout import MDStackLayout + + +Builder.load_string( + """ +#:import DEVICE_TYPE kivymd.material_resources.DEVICE_TYPE + + + + adaptive_height: True + spacing: "5dp" + + + + size_hint: None, None + height: "26dp" + padding: 0, 0, "5dp", 0 + width: + self.minimum_width - (dp(10) if DEVICE_TYPE == "desktop" else dp(20)) \ + if root.icon != 'checkbox-blank-circle' else self.minimum_width + theme_text_color: 'Custom' + text_color:1,0,0,1 + + # canvas: + # Color: + # rgba: root.color + # RoundedRectangle: + # pos: self.pos + # size: self.size + # radius: [root.radius] + + + + + MDBoxLayout: + id: box_check + adaptive_size: True + pos_hint: {'center_y': .5} + + MDBoxLayout: + adaptive_width: True + padding: dp(0) + + MDIconButton: + id: icon + icon: root.icon + size_hint_y: None + height: "20dp" + pos_hint: {"center_y": .5} + user_font_size: "20dp" + disabled: True + md_bg_color_disabled: 0, 0, 0, 0 + theme_text_color: "Custom" + text_color: 1,0,0,1 + + Label: + id: label + text: root.label + size_hint_x: None + width: self.texture_size[0] + color: root.text_color if root.text_color else (root.theme_cls.text_color) + font_name: "assets/font.otf" + font_size: "18sp" + +""" +) + + +class MyChip(BoxLayout, ThemableBehavior): + label = StringProperty() + """Chip text. + + :attr:`label` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + icon = StringProperty("checkbox-blank-circle") + """Chip icon. + + :attr:`icon` is an :class:`~kivy.properties.StringProperty` + and defaults to `'checkbox-blank-circle'`. + """ + + color = ListProperty() + """Chip color in ``rgba`` format. + + :attr:`color` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + text_color = ListProperty() + """Chip's text color in ``rgba`` format. + + :attr:`text_color` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + check = BooleanProperty(False) + """ + If True, a checkmark is added to the left when touch to the chip. + + :attr:`check` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + callback = ObjectProperty() + """Custom method. + + :attr:`callback` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + radius = NumericProperty("12dp") + """Corner radius values. + + :attr:`radius` is an :class:`~kivy.properties.NumericProperty` + and defaults to `'12dp'`. + """ + + selected_chip_color = ListProperty() + """The color of the chip that is currently selected in ``rgba`` format. + + :attr:`selected_chip_color` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + if not self.color: + self.color = (1,0,0,1) #self.theme_cls.primary_color + + def on_icon(self, instance, value): + if value == "": + self.icon = "checkbox-blank-circle" + self.remove_widget(self.ids.icon) + + def on_touch_down(self, touch): + if self.collide_point(*touch.pos): + md_choose_chip = self.parent + if self.selected_chip_color: + Animation( + color=self.theme_cls.primary_dark + if not self.selected_chip_color + else self.selected_chip_color, + d=0.3, + ).start(self) + if issubclass(md_choose_chip.__class__, MDChooseChip): + for chip in md_choose_chip.children: + if chip is not self: + chip.color = self.theme_cls.primary_color + if self.check: + if not len(self.ids.box_check.children): + self.ids.box_check.add_widget( + MDIconButton( + icon="check", + size_hint_y=None, + height=dp(20), + disabled=True, + user_font_size=dp(20), + pos_hint={"center_y": 0.5}, + ) + ) + else: + check = self.ids.box_check.children[0] + self.ids.box_check.remove_widget(check) + if self.callback: + self.callback(self, self.label) + +class MDChooseChip(MDStackLayout): + def add_widget(self, widget, index=0, canvas=None): + if isinstance(widget, MyChip): + return super().add_widget(widget) diff --git a/client/root.kv b/client/root.kv index aeef9e7..f82264a 100644 --- a/client/root.kv +++ b/client/root.kv @@ -38,10 +38,11 @@ : theme_text_color: 'Custom' text_color: (1,0,0,1) - pos_hint: {'center_y': 0.5} + # pos_hint: {'center_y': 0.5} halign: 'center' height: self.texture_size[1] - font_family: 'Courier' + font_name: 'assets/font.otf' + size_hint:1,None @@ -56,15 +57,19 @@ MyLayout: scr_mngr: scr_mngr orientation: 'vertical' height: self.minimum_height + md_bg_color:0,0,0,1 canvas: Color: - rgba: 0.925,0.925,0.925,1 #get_color_from_hex(colors['Gray']['900']) + rgba: 0.925,0.925,0.925,0.99 #get_color_from_hex(colors['Gray']['900']) + # rgba: 0,0,0,0.9 #get_color_from_hex(colors['Gray']['900']) + Rectangle: pos: self.pos size: self.size - source: 'assets/komrade2.png' + source: 'assets/bg.png' + # texture: app.texture MDToolbar: diff --git a/client/screens/feed/feed.kv b/client/screens/feed/feed.kv index 5d3823a..8b76109 100644 --- a/client/screens/feed/feed.kv +++ b/client/screens/feed/feed.kv @@ -157,15 +157,12 @@ height: self.minimum_height radius:[20,] border_radius:20 - # canvas: - # Color: - # rgb: 1,0,0,1 - # Line: - # width: 1 - # rectangle: (self.x, self.y, self.width, self.height) - # # radius:[20,] - # # border_radius:20 - + canvas: + Color: + rgba: 1,0,0,0.5 + Line: + width: 1 + rounded_rectangle: (self.x, self.y, self.width, self.height, 20, 20, 20, 20) diff --git a/client/screens/profile/profile.kv b/client/screens/profile/profile.kv index 3129cd3..71a6d21 100644 --- a/client/screens/profile/profile.kv +++ b/client/screens/profile/profile.kv @@ -59,32 +59,56 @@ : cols:1 orientation:'vertical' - size_hint:None,None + size_hint:0.6666,None md_bg_color:0,0,0,1 - width: '300dp' + # width: '300dp' + height: self.minimum_height pos_hint: {'center_x':0.5} + spacing:'10sp' radius:[20,] border_radius:20 + padding:'10sp' + canvas: + Color: + rgba: 1,0,0,0.5 + Line: + width: 1 + rounded_rectangle: (self.x, self.y, self.width, self.height, 20, 20, 20, 20) : pos_hint: {'center_x':0.5} + theme_text_color:'Custom' + text_color: 1,0,0,1 + halign:'center' : pos_hint: {'center_x':0.5} theme_text_color:'Custom' text_color: 1,0,0,1 halign:'center' - font_size:'28sp' : + halign:'left' : + theme_text_color: 'Custom' + text_color:1,0,0,1 : + theme_text_color: 'Custom' + text_color:1,0,0,1 + : + theme_text_color: 'Custom' + text_color:1,0,0,1 + size_hint:None,None + pos_hint:{'right':1} : + theme_text_color: 'Custom' + text_color:1,0,0,1 + pos_hint:{'left':1} : cols:1 @@ -96,4 +120,13 @@ : size_hint:1,1 - # width: '200dp' \ No newline at end of file + # width: '200dp' + +: + cols:2 + orientation:'horizontal' + spacing:'25dp' + height:self.minimum_height + size_hint:1,None + # md_bg_color:1,0,0,1 + pos_hint: {'center_x':0.5,'center_y':0.5} diff --git a/client/screens/profile/profile.py b/client/screens/profile/profile.py index e5016c6..2f30650 100644 --- a/client/screens/profile/profile.py +++ b/client/screens/profile/profile.py @@ -1,9 +1,10 @@ from screens.base import BaseScreen from main import log from kivymd.uix.boxlayout import MDBoxLayout +from kivymd.uix.chip import MDChip from kivymd.uix.textfield import MDTextField -from kivymd.uix.button import MDRectangleFlatButton -from kivymd.uix.label import MDLabel +from kivymd.uix.button import MDRectangleFlatButton,MDRectangleFlatIconButton,MDIconButton +from kivymd.uix.label import MDLabel, MDIcon from kivy.uix.image import AsyncImage, Image from kivy.metrics import dp from kivy.properties import StringProperty @@ -12,12 +13,31 @@ from kivy.core.image import Image as CoreImage import io from kivy.uix.carousel import Carousel from screens.feed.feed import PostCard +from kivy.clock import Clock +from functools import partial +from copy import copy,deepcopy +from kivy.animation import Animation +from main import MyLabel +from misc import * + + img_src = 'assets/avatar.jpg' #cache/img/1e6/587e880344d1e88cec8fda65b1148.jpeg' # img_src = '/home/ryan/Pictures/Harrier.jpeg' cover_img_src='assets/cover.jpg' #cache/img/60d/9de00e52e4758ade5969c50dc053f.jpg' -class ProfileAvatar(Image): pass +class ProfileAvatar(Image): + def on_touch_down(self, touch): + if self.screen.carousel.index and self.collide_point(*touch.pos) and self.screen.carousel.slides: + start = self.screen.carousel.slides[0] + start.opacity=0 + self.screen.carousel.index=0 + anim = Animation(opacity=1, duration=0.1) + anim.start(start) + # start = self.screen.carousel.slides[0] + # log('????',start) + # self.screen.carousel.load_slide(start) + # self.screen.carousel.load_next() class LayoutAvatar(MDBoxLayout): pass @@ -36,14 +56,14 @@ def crop_square(pil_img, crop_width, crop_height): (img_width + crop_width) // 2, (img_height + crop_height) // 2)) -def circularize_img(img_fn, width): +def circularize_img(img_fn, width, do_crop=True): from PIL import Image, ImageOps, ImageDraw im = Image.open(img_fn) # get center - im = crop_square(im, width, width) + if do_crop: im = crop_square(im, width, width) im = im.resize((width,width)) bigsize = (im.size[0] * 3, im.size[1] * 3) mask = Image.new('L', bigsize, 0) @@ -67,15 +87,58 @@ def circularize_img(img_fn, width): # return output class ProfilePageLayout(MDBoxLayout): pass +class FollowerLayout(MDBoxLayout): pass + +class AuthorName(MyLabel): pass +class AuthorUsername(MyLabel): pass +class AuthorDesc(MyLabel): pass +class AuthorPronouns(MyChip): pass +class AuthorPlace(MyChip): pass +class AuthorWebsite(MyChip): pass +class AuthorFollowers(MyChip): pass +class AuthorFollowing(MyChip): pass + + + +def update_screen_on_carousel_move(self,dt,width=75): + + # screen.author_name.text=str(screen.carousel.index) + # avatar_layout = copy(screen.avatar_layout) + # avatar_layout.width=dp(100) + # avatar_layout.height=dp(100) + + if self.carousel.index: + if not hasattr(self,'avatar_layout_small'): + self.avatar_img.seek(0) + img,byte,avatar,avatar_layout = self.make_profile_img(width,do_crop=False,circ_img=self.avatar_img) + avatar.screen = self + avatar_layout.pos_hint = {'right':0.995, 'top':0.995} + avatar_layout.opacity=0 + # avatar_layout.animate() + self.add_widget(avatar_layout) + self.avatar_layout_small=avatar_layout + self.avatar_layout_small_visible=False + + if not self.avatar_layout_small_visible: + self.avatar_layout_small_visible=True + anim = Animation(opacity=1, duration=0.25) + anim.start(self.avatar_layout_small) + + else: + if hasattr(self,'avatar_layout_small'): + if self.avatar_layout_small_visible: + self.avatar_layout_small_visible=False + anim = Animation(opacity=0, duration=0.25) + anim.start(self.avatar_layout_small) + + # self.remove_widget(self.avatar_layout_small) + # del self.avatar_layout_small + + # avatar_layout = self.avatar_layout + # self.remove_widget(avatar_layout) + # self.add_widget(avatar_layout) -class AuthorName(MDLabel): pass -class AuthorUsername(MDLabel): pass -class AuthorDesc(MDLabel): pass -class AuthorPlace(MDLabel): pass -class AuthorWebsite(MDLabel): pass -class AuthorFollowers(MDLabel): pass -class AuthorFollowing(MDLabel): pass class ProfileScreen(BaseScreen): @@ -83,39 +146,109 @@ class ProfileScreen(BaseScreen): # global app # if app.is_logged_in(): # app.root.change_screen('feed') + username = None + clock_scheduled=None + + def make_profile_img(self,width,do_crop=True,circ_img=None): + + if not circ_img: + circ_img = circularize_img(img_src,width,do_crop=do_crop) + + avatar_layout = LayoutAvatar() + byte=io.BytesIO(circ_img.read()) + + + img = CoreImage(byte,ext='png') + avatar = ProfileAvatar() + avatar.texture = img.texture + avatar_layout.height=dp(width) + avatar_layout.width=dp(width) + avatar_layout.add_widget(avatar) + return (circ_img,byte,avatar,avatar_layout) + def on_pre_enter(self, width=200): + # query author info + if not self.username: self.username=self.app.username + # @TODO + + if not self.clock_scheduled: + Clock.schedule_interval(partial(update_screen_on_carousel_move, self), 0.1) + self.clock_scheduled=True + + # clear - if not hasattr(self,'carousel'): - self.carousel = Carousel() - self.carousel.direction='right' - self.posts=[] - else: + if hasattr(self,'carousel'): for post in self.posts: self.carousel.remove_widget(post) - + self.remove_widget(self.carousel) + del self.carousel + self.posts=[] + + + self.carousel = Carousel() + self.carousel.direction='right' + self.carousel.loop=True + self.posts=[] + # get circular image - circ_img = circularize_img(img_src,200) - self.avatar_layout = LayoutAvatar() - img = CoreImage(io.BytesIO(circ_img.read()),ext='png') - self.avatar = ProfileAvatar() - self.avatar.texture = img.texture - self.avatar_layout.height=dp(width) - self.avatar_layout.width=dp(width) - self.avatar_layout.add_widget(self.avatar) - + self.avatar_img, self.avatar_img_bytes, self.avatar, self.avatar_layout = \ + self.make_profile_img(width) + self.avatar.screen = self ## author info self.author_info_layout = AuthorInfoLayout() self.app.name_irl = 'Marx Zuckerberg' if hasattr(self.app,'name_irl'): self.author_name_irl = AuthorName(text=self.app.name_irl) + self.author_name_irl.font_name = 'assets/font.otf' + self.author_name_irl.font_size = '28sp' self.author_info_layout.add_widget(self.author_name_irl) - self.author_name = AuthorUsername(text=self.app.username) + self.author_name = AuthorUsername(text='@'+self.username) self.author_name.font_name = 'assets/font.otf' - self.author_name.font_size = '28sp' + self.author_name.font_size = '20sp' self.author_info_layout.add_widget(self.author_name) + + + ## AUTHOR DESCRIPTION + self.author_desc = AuthorDesc(text='Blogging bad takes since 1999. Writing on abstraction as literary & capitalist form') + self.author_desc.font_name='assets/font.otf' + self.author_desc.font_size='18sp' + # self.author_desc.halign='left' + + ## Pronouns + self.author_pronouns = AuthorPronouns(label='he/him',icon='gender-transgender') + + ## AUTHOR PLACE + self.author_place = AuthorPlace(label='UK',icon='map-marker-outline') + + ## Website + self.author_website = AuthorWebsite(label='ryanheuser.org', icon='link-variant') + + + ## Followers + self.follower_layout = FollowerLayout() + self.author_followers = AuthorFollowers(label='13 followers',icon='account-arrow-left') + self.author_following = AuthorFollowing(label='777 following',icon='account-arrow-right') + + + ## add to layout + self.author_info_layout.add_widget(self.author_desc) + self.author_info_layout.add_widget(self.author_pronouns) + self.author_info_layout.add_widget(self.author_place) + self.author_info_layout.add_widget(self.author_website) + + self.follower_layout.add_widget(self.author_following) + self.follower_layout.add_widget(self.author_followers) + self.author_info_layout.add_widget(self.follower_layout) + + # class AuthorPlace(MDLabel): pass + # class AuthorWebsite(MDLabel): pass + # class AuthorFollowers(MDLabel): pass + # class AuthorFollowing(MDLabel): pass + + ## add root widgets @@ -132,10 +265,16 @@ class ProfileScreen(BaseScreen): def add_author_posts(self): # add posts lim=25 - for i,post in enumerate(self.app.get_posts()): + for i,post in enumerate(self.app.get_my_posts()): if i>lim: break post_obj = PostCard(post) log(post) self.posts.append(post_obj) - self.carousel.add_widget(post_obj) \ No newline at end of file + self.carousel.add_widget(post_obj) + + # def on_touch_move(self, ent): + # if self.carousel.index: + # self.author_name.text='moved!' + # else: + # self.author_name.text=self.username \ No newline at end of file diff --git a/client/watcher.py b/client/watcher.py index b38763a..f2de54e 100644 --- a/client/watcher.py +++ b/client/watcher.py @@ -37,6 +37,7 @@ class Handler(FileSystemEventHandler): @staticmethod def on_any_event(event): if '/cache/' in str(event.src_path): return None + if '__pycache__' in str(event.src_path): return None if event.is_directory: return None diff --git a/server/server.py b/server/server.py index a715575..9355de7 100644 --- a/server/server.py +++ b/server/server.py @@ -187,7 +187,7 @@ def get_follows(name=None): def get_posts(name=None): if name: person = Person.nodes.get_or_none(name=name) - data = [p.data for p in person.wrote.all] if person is not None else [] + data = [p.data for p in person.wrote.all()] if person is not None else [] else: data = [p.data for p in Post.nodes.order_by('-timestamp')] # print(data)