|
|
|
import os,sys; sys.path.append(os.path.abspath(os.path.join(os.path.abspath(os.path.join(os.path.dirname(__file__),'..')),'..')))
|
|
|
|
from comrad import *
|
|
|
|
|
|
|
|
from screens.base import BaseScreen
|
|
|
|
from kivymd.uix.boxlayout import MDBoxLayout
|
|
|
|
from kivymd.uix.chip import MDChip
|
|
|
|
from kivymd.uix.textfield import MDTextField
|
|
|
|
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
|
|
|
|
from kivy.core.window import Window
|
|
|
|
from kivy.core.image import Image as CoreImage
|
|
|
|
import io,asyncio
|
|
|
|
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,COLOR_ICON
|
|
|
|
from misc import *
|
|
|
|
import shutil
|
|
|
|
|
|
|
|
|
|
|
|
class ProfileAvatar(Image):
|
|
|
|
def on_touch_down(self, touch):
|
|
|
|
if self.collide_point(*touch.pos):
|
|
|
|
if self.screen.carousel.index 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)
|
|
|
|
elif self.screen.app.username == self.screen.app.comrad.name:
|
|
|
|
self.choose()
|
|
|
|
|
|
|
|
def choose(self):
|
|
|
|
try:
|
|
|
|
self.choose_native()
|
|
|
|
except (NotImplementedError,TypeError,OSError) as e:
|
|
|
|
self.choose_nonnative()
|
|
|
|
|
|
|
|
def choose_native(self):
|
|
|
|
from plyer import filechooser
|
|
|
|
filechooser.open_file(on_selection=self.handle_selection)
|
|
|
|
|
|
|
|
def choose_nonnative(self):
|
|
|
|
import wx
|
|
|
|
app = wx.App(None)
|
|
|
|
wildcard = "PNG Files (*.png)|*.png|JPEG Files (*.jpg/*.jpeg)|*.jpg;*.jpeg|GIF Files (*.gif)|*.gif"
|
|
|
|
style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST
|
|
|
|
dialog = wx.FileDialog(None, 'Select Data File',wildcard=wildcard,style=style)
|
|
|
|
if dialog.ShowModal() == wx.ID_OK:
|
|
|
|
path = dialog.GetPath()
|
|
|
|
else:
|
|
|
|
path = None
|
|
|
|
dialog.Destroy()
|
|
|
|
self.handle_selection(path)
|
|
|
|
|
|
|
|
def handle_selection(self, selection):
|
|
|
|
if selection and type(selection)==list:
|
|
|
|
fnfn = selection[0]
|
|
|
|
_,ext=os.path.splitext(fnfn)
|
|
|
|
if ext[1:] in ALLOWED_IMG_EXT:
|
|
|
|
#avatar_fnfn = os.path.join(PATH_AVATAR,self.com)
|
|
|
|
self.parent.parent.parent.parent.parent.change_avatar(fnfn)
|
|
|
|
|
|
|
|
class LayoutAvatar(MDBoxLayout): pass
|
|
|
|
|
|
|
|
class AuthorInfoLayout(MDBoxLayout): pass
|
|
|
|
|
|
|
|
class LayoutCover(MDBoxLayout):
|
|
|
|
source=StringProperty()
|
|
|
|
pass
|
|
|
|
|
|
|
|
class CoverImage(Image): pass
|
|
|
|
|
|
|
|
def binarize_image(image, threshold=200):
|
|
|
|
"""Binarize an image."""
|
|
|
|
image = image.convert('L') # convert image to monochrome
|
|
|
|
import numpy
|
|
|
|
image = numpy.array(image)
|
|
|
|
image = binarize_array(image, threshold)
|
|
|
|
imsave(target_path, image)
|
|
|
|
return image
|
|
|
|
|
|
|
|
def binarize_array(numpy_array, threshold=200):
|
|
|
|
"""Binarize a numpy array."""
|
|
|
|
import numpy
|
|
|
|
for i in range(len(numpy_array)):
|
|
|
|
for j in range(len(numpy_array[0])):
|
|
|
|
if numpy_array[i][j] > threshold:
|
|
|
|
numpy_array[i][j] = 255
|
|
|
|
else:
|
|
|
|
numpy_array[i][j] = 0
|
|
|
|
return numpy_array
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def crop_square(pil_img, crop_width, crop_height):
|
|
|
|
img_width, img_height = pil_img.size
|
|
|
|
return pil_img.crop(((img_width - crop_width) // 2,
|
|
|
|
(img_height - crop_height) // 2,
|
|
|
|
(img_width + crop_width) // 2,
|
|
|
|
(img_height + crop_height) // 2))
|
|
|
|
|
|
|
|
|
|
|
|
def circularize_img(img_fn, width, im=None, do_crop=True,bw=False,resize=True,circularize=True):
|
|
|
|
from PIL import Image, ImageOps, ImageDraw
|
|
|
|
from kivy.app import App
|
|
|
|
import numpy as np
|
|
|
|
log=App.get_running_app().log
|
|
|
|
|
|
|
|
#if not im:
|
|
|
|
im = Image.open(img_fn).convert('RGB')
|
|
|
|
log('??',im)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# get center
|
|
|
|
if do_crop: im = crop_square(im, width, width)
|
|
|
|
if resize: im = im.resize((width,width))
|
|
|
|
|
|
|
|
if bw:
|
|
|
|
thresh = 175
|
|
|
|
fn = lambda x : 255 if x > thresh else 0
|
|
|
|
im = im.convert('L').point(fn, mode='1').convert('RGB')
|
|
|
|
orig_color = (255,255,255)
|
|
|
|
replacement_color = COLOR_ICON #(255,0,0)
|
|
|
|
# img = im.convert('RGB')
|
|
|
|
data = np.array(im)
|
|
|
|
data[(data == orig_color).all(axis = -1)] = replacement_color
|
|
|
|
im = Image.fromarray(data, mode='RGB').convert('RGBA')
|
|
|
|
|
|
|
|
if circularize_img:
|
|
|
|
bigsize = (im.size[0] * 3, im.size[1] * 3)
|
|
|
|
mask = Image.new('L', bigsize, 0)
|
|
|
|
draw = ImageDraw.Draw(mask)
|
|
|
|
draw.ellipse((0, 0) + bigsize, fill=255)
|
|
|
|
mask = mask.resize(im.size, Image.ANTIALIAS)
|
|
|
|
im.putalpha(mask)
|
|
|
|
|
|
|
|
|
|
|
|
# give back bytes
|
|
|
|
|
|
|
|
|
|
|
|
log('!!',im)
|
|
|
|
|
|
|
|
output = ImageOps.fit(im, mask.size, centering=(0.5, 0.5))
|
|
|
|
imgByteArr = io.BytesIO()
|
|
|
|
output.save(imgByteArr, format='PNG')
|
|
|
|
# imgByteArr = imgByteArr.getvalue()
|
|
|
|
imgByteArr.seek(0)
|
|
|
|
return imgByteArr
|
|
|
|
# output.putalpha(mask)
|
|
|
|
# output.save('output.png')
|
|
|
|
|
|
|
|
# background = Image.open('back.jpg')
|
|
|
|
# background.paste(im, (150, 10), im)
|
|
|
|
# background.save('overlap.png')
|
|
|
|
# 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 not self.do_update_screen_on_carousel_move: return
|
|
|
|
|
|
|
|
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,bw=True,circularize=False)
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
from screens.base import ProtectedScreen
|
|
|
|
class ProfileScreen(ProtectedScreen):
|
|
|
|
username = None
|
|
|
|
clock_scheduled=None
|
|
|
|
do_update_screen_on_carousel_move=True
|
|
|
|
|
|
|
|
def make_profile_img(self,width,do_crop=True,circ_img=None,bw=False,circularize=True):
|
|
|
|
img_src = os.path.join(PATH_AVATARS, f'{self.app.username}.png')
|
|
|
|
if not os.path.exists(img_src):
|
|
|
|
img_src = os.path.join(PATH_GUI_ASSETS, 'avatars', f'{self.app.username}.png')
|
|
|
|
if not os.path.exists(img_src):
|
|
|
|
img_src=PATH_DEFAULT_AVATAR
|
|
|
|
|
|
|
|
circ_img = circularize_img(img_src,width,do_crop=do_crop,bw=bw,circularize=circularize)
|
|
|
|
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 change_avatar(self,fnfn,width=AVATAR_WIDTH):
|
|
|
|
# raise Exception(f'Got filename! {fnfn}')
|
|
|
|
if not os.path.exists(fnfn): return
|
|
|
|
ext=os.path.splitext(fnfn)[1]
|
|
|
|
ofnfn=os.path.join(PATH_AVATARS,self.app.username+ext)
|
|
|
|
shutil.copyfile(fnfn,ofnfn)
|
|
|
|
|
|
|
|
# re-get circular image
|
|
|
|
# self.avatar_layout.remove_widget(self.avatar)
|
|
|
|
# self.on_pre_enter()
|
|
|
|
self.page_layout.remove_widget(self.avatar_layout)
|
|
|
|
self.avatar_img, self.avatar_img_bytes, self.avatar, self.avatar_layout = \
|
|
|
|
self.make_profile_img(width)
|
|
|
|
self.avatar.screen = self
|
|
|
|
self.page_layout.add_widget(self.avatar_layout,1)
|
|
|
|
# self.avatar_layout.add_widget(self.avatar)
|
|
|
|
if hasattr(self,'avatar_layout_small'):
|
|
|
|
del self.avatar_layout_small
|
|
|
|
|
|
|
|
def on_pre_enter(self, width=AVATAR_WIDTH):
|
|
|
|
if not super().on_pre_enter(): return
|
|
|
|
self.do_update_screen_on_carousel_move=True
|
|
|
|
if not self.clock_scheduled:
|
|
|
|
Clock.schedule_interval(partial(update_screen_on_carousel_move, self), 0.1)
|
|
|
|
self.clock_scheduled=True
|
|
|
|
|
|
|
|
|
|
|
|
# clear
|
|
|
|
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
|
|
|
|
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'
|
|
|
|
self.app.name_irl = 'Comrad @'+self.app.username
|
|
|
|
|
|
|
|
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.font_name = 'assets/font.otf'
|
|
|
|
self.author_name.font_size = '20sp'
|
|
|
|
# self.author_info_layout.add_widget(self.author_name)
|
|
|
|
|
|
|
|
|
|
|
|
# desc from us
|
|
|
|
self.log(f'do I know where {self.app.username} lives?')
|
|
|
|
if not self.app.comrad.exists_locally(self.app.username):
|
|
|
|
author_desc=f'Comrad @{self.app.username} is not a contact of yours.'
|
|
|
|
self.author_desc = AuthorDesc(text=author_desc)
|
|
|
|
self.author_desc.font_name='assets/font.otf'
|
|
|
|
self.author_desc.font_size='18sp'
|
|
|
|
self.author_info_layout.add_widget(self.author_desc)
|
|
|
|
|
|
|
|
# this is a contact
|
|
|
|
else:
|
|
|
|
## AUTHOR DESCRIPTION
|
|
|
|
author_desc=f'... etc ...'
|
|
|
|
self.author_desc = AuthorDesc(text=author_desc)
|
|
|
|
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='they/them',icon='gender-transgender')
|
|
|
|
|
|
|
|
## AUTHOR PLACE
|
|
|
|
self.author_place = AuthorPlace(label='Deterritorialized',icon='map-marker-outline')
|
|
|
|
|
|
|
|
## Website
|
|
|
|
self.author_website = AuthorWebsite(label='website.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='13 comrades',icon='account-multiple')
|
|
|
|
|
|
|
|
|
|
|
|
## 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
|
|
|
|
self.page_layout = ProfilePageLayout()
|
|
|
|
self.page_layout.add_widget(self.avatar_layout)
|
|
|
|
self.page_layout.add_widget(self.author_info_layout)
|
|
|
|
|
|
|
|
self.add_widget(self.carousel)
|
|
|
|
self.carousel.add_widget(self.page_layout)
|
|
|
|
|
|
|
|
## add posts
|
|
|
|
asyncio.create_task(self.add_author_posts())
|
|
|
|
|
|
|
|
async def add_author_posts(self):
|
|
|
|
lim=25
|
|
|
|
|
|
|
|
self.log('USERNAME:',self.app.username)
|
|
|
|
|
|
|
|
posts=self.app.comrad.sent_posts(
|
|
|
|
username=self.app.username
|
|
|
|
)
|
|
|
|
self.log('OUTBOX:',len(posts),'posts')
|
|
|
|
|
|
|
|
for i,post in enumerate(posts):
|
|
|
|
if i>lim: break
|
|
|
|
data = {
|
|
|
|
'author':post.from_name,
|
|
|
|
'to_name':post.to_name,
|
|
|
|
'content':post.msg.get('txt') if type(post.msg)==dict else str(post.msg),
|
|
|
|
'timestamp':post.timestamp
|
|
|
|
}
|
|
|
|
post_obj = PostCard(data)
|
|
|
|
self.log('sent post!',post)
|
|
|
|
self.posts.append(post_obj)
|
|
|
|
self.carousel.add_widget(post_obj)
|
|
|
|
|
|
|
|
|
|
|
|
def on_pre_leave(self):
|
|
|
|
self.app.username=self.app.comrad.name
|
|
|
|
# self.avatar_layout_small_visible=False
|
|
|
|
if hasattr(self,'avatar_layout_small'):
|
|
|
|
self.remove_widget(self.avatar_layout_small)
|
|
|
|
del self.avatar_layout_small
|
|
|
|
self.do_update_screen_on_carousel_move=False
|
|
|
|
|
|
|
|
# def on_touch_move(self, ent):
|
|
|
|
# if self.carousel.index:
|
|
|
|
# self.author_name.text='moved!'
|
|
|
|
# else:
|
|
|
|
# self.author_name.text=self.username
|