ecasa: add basic setup dialog
[enigma2-plugins.git] / ecasa / src / EcasaGui.py
1 from __future__ import print_function
2
3 #pragma mark - GUI
4
5 #pragma mark Screens
6 from Screens.Screen import Screen
7 from Screens.HelpMenu import HelpableScreen
8 from Screens.MessageBox import MessageBox
9 from NTIVirtualKeyBoard import NTIVirtualKeyBoard
10 from EcasaSetup import EcasaSetup
11
12 #pragma mark Components
13 from Components.ActionMap import HelpableActionMap
14 from Components.AVSwitch import AVSwitch
15 from Components.Label import Label
16 from Components.Pixmap import Pixmap, MovingPixmap
17 from Components.Sources.StaticText import StaticText
18 from Components.Sources.List import List
19
20 #pragma mark Configuration
21 from Components.config import config
22
23 #pragma mark Picasa
24 from .PicasaApi import PicasaApi
25 from TagStrip import strip_readable
26
27 from enigma import ePicLoad, ePythonMessagePump, getDesktop
28 from collections import deque
29
30 try:
31         xrange = xrange
32 except NameError:
33         xrange = range
34
35 our_print = lambda *args, **kwargs: print("[EcasaGui]", *args, **kwargs)
36
37 class EcasaPictureWall(Screen, HelpableScreen):
38         """Base class for so-called "picture walls"."""
39         PICS_PER_PAGE = 15
40         PICS_PER_ROW = 5
41         skin = """<screen position="center,center" size="600,380">
42                 <ePixmap position="0,0" size="140,40" pixmap="skin_default/buttons/red.png" transparent="1" alphatest="on"/>
43                 <ePixmap position="140,0" size="140,40" pixmap="skin_default/buttons/green.png" transparent="1" alphatest="on"/>
44                 <ePixmap position="280,0" size="140,40" pixmap="skin_default/buttons/yellow.png" transparent="1" alphatest="on"/>
45                 <ePixmap position="420,0" size="140,40" pixmap="skin_default/buttons/blue.png" transparent="1" alphatest="on"/>
46                 <ePixmap position="565,10" size="35,25" pixmap="skin_default/buttons/key_menu.png" alphatest="on"/>
47                 <widget source="key_red" render="Label" position="0,0" zPosition="1" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1"/>
48                 <widget source="key_green" render="Label" position="140,0" zPosition="1" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1"/>
49                 <widget source="key_yellow" render="Label" position="280,0" zPosition="1" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1"/>
50                 <widget source="key_blue" render="Label" position="420,0" zPosition="1" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1"/>
51                 <widget name="waitingtext" position="100,179" size="400,22" valign="center" halign="center" font="Regular;22"/>
52                 <widget name="image0"  position="30,50"   size="90,90"/>
53                 <widget name="image1"  position="140,50"  size="90,90"/>
54                 <widget name="image2"  position="250,50"  size="90,90"/>
55                 <widget name="image3"  position="360,50"  size="90,90"/>
56                 <widget name="image4"  position="470,50"  size="90,90"/>
57                 <widget name="image5"  position="30,160"  size="90,90"/>
58                 <widget name="image6"  position="140,160" size="90,90"/>
59                 <widget name="image7"  position="250,160" size="90,90"/>
60                 <widget name="image8"  position="360,160" size="90,90"/>
61                 <widget name="image9"  position="470,160" size="90,90"/>
62                 <widget name="image10" position="30,270"  size="90,90"/>
63                 <widget name="image11" position="140,270" size="90,90"/>
64                 <widget name="image12" position="250,270" size="90,90"/>
65                 <widget name="image13" position="360,270" size="90,90"/>
66                 <widget name="image14" position="470,270" size="90,90"/>
67                 <!-- TODO: find/create :P -->
68                 <widget name="highlight" position="25,45" size="100,100"/>
69                 </screen>"""
70         def __init__(self, session, api=None):
71                 Screen.__init__(self, session)
72                 HelpableScreen.__init__(self)
73
74                 if api is None:
75                         self.api = PicasaApi(
76                                         config.plugins.ecasa.google_username.value,
77                                         config.plugins.ecasa.google_password.value,
78                                         config.plugins.ecasa.cache.value)
79                 else:
80                         self.api = api
81
82                 self["key_red"] = StaticText(_("Close"))
83                 self["key_green"] = StaticText(_("My Albums"))
84                 self["key_yellow"] = StaticText()
85                 self["key_blue"] = StaticText(_("Search"))
86                 for i in xrange(self.PICS_PER_PAGE):
87                         self['image%d' % i] = Pixmap()
88                         self['title%d' % i] = StaticText()
89                 self["highlight"] = MovingPixmap()
90                 self["waitingtext"] = Label(_("Please wait... Loading list..."))
91
92                 self["overviewActions"] = HelpableActionMap(self, "EcasaOverviewActions", {
93                         "up": self.up,
94                         "down": self.down,
95                         "left": self.left,
96                         "right": self.right,
97                         "nextPage": (self.nextPage, _("show next page")),
98                         "prevPage": (self.prevPage, _("show previous page")),
99                         "select": self.select,
100                         "exit":self.close,
101                         "albums":(self.albums, _("show your albums (if logged in)")),
102                         "search":(self.search, _("start a new search")),
103                         "contextmenu":(self.contextMenu, _("open context menu")),
104                         }, -1)
105
106                 self.offset = 0
107                 self.__highlighted = 0
108                 self.pictures = ()
109
110                 # thumbnail loader
111                 self.picload = ePicLoad()
112                 self.picload.PictureData.get().append(self.gotPicture)
113                 sc = AVSwitch().getFramebufferScale()
114                 self.picload.setPara((90, 90, sc[0], sc[1], False, 1, '#ff000000')) # TODO: hardcoded size is evil!
115                 self.currentphoto = None
116                 self.queue = deque()
117
118         @property
119         def highlighted(self):
120                 return self.__highlighted
121
122         @highlighted.setter
123         def highlighted(self, highlighted):
124                 our_print("setHighlighted", highlighted)
125                 # only allow to select valid pictures
126                 if highlighted + self.offset >= len(self.pictures): return
127
128                 self.__highlighted = highlighted
129                 origpos = self['image%d' % highlighted].getPosition()
130                 # TODO: hardcoded highlight offset is evil :P
131                 self["highlight"].moveTo(origpos[0]-5, origpos[1]-5, 1)
132                 self["highlight"].startMoving()
133
134         def gotPicture(self, picInfo=None):
135                 our_print("picture decoded")
136                 ptr = self.picload.getData()
137                 if ptr is not None:
138                         idx = self.pictures.index(self.currentphoto)
139                         realIdx = idx - self.offset
140                         self['image%d' % realIdx].instance.setPixmap(ptr.__deref__())
141                 self.currentphoto = None
142                 self.maybeDecode()
143
144         def maybeDecode(self):
145                 if self.currentphoto is not None: return
146                 try:
147                         filename, self.currentphoto = self.queue.pop()
148                 except IndexError:
149                         our_print("no queued photos")
150                         # no more pictures
151                         pass
152                 else:
153                         self.picload.startDecode(filename)
154
155         def pictureDownloaded(self, tup):
156                 filename, photo = tup
157                 self.queue.append((filename, photo))
158                 self.maybeDecode()
159
160         def pictureDownloadFailed(self, tup):
161                 error, photo = tup
162                 our_print("pictureDownloadFailed", error, photo)
163                 # TODO: indicate in gui
164
165         def setup(self):
166                 our_print("setup")
167                 self["waitingtext"].hide()
168                 self.queue.clear()
169                 pictures = self.pictures
170                 for i in xrange(self.PICS_PER_PAGE):
171                         try:
172                                 our_print("trying to initiate download of idx", i+self.offset)
173                                 picture = pictures[i+self.offset]
174                                 self.api.downloadThumbnail(picture).addCallbacks(self.pictureDownloaded, self.pictureDownloadFailed)
175                         except IndexError:
176                                 # no more pictures
177                                 # TODO: set invalid pic for remaining items
178                                 our_print("no more pictures in setup")
179                                 break
180                         except Exception as e:
181                                 our_print("unexpected exception in setup:", e)
182
183         def up(self):
184                 # TODO: implement for incomplete pages
185                 highlighted = (self.highlighted - self.PICS_PER_ROW) % self.PICS_PER_PAGE
186                 our_print("up. before:", self.highlighted, ", after:", highlighted)
187                 self.highlighted = highlighted
188         def down(self):
189                 # TODO: implement for incomplete pages
190                 highlighted = (self.highlighted + self.PICS_PER_ROW) % self.PICS_PER_PAGE
191                 our_print("down. before:", self.highlighted, ", after:", highlighted)
192                 self.highlighted = highlighted
193         def left(self):
194                 # TODO: implement for incomplete pages
195                 highlighted = (self.highlighted - 1) % self.PICS_PER_PAGE
196                 our_print("left. before:", self.highlighted, ", after:", highlighted)
197                 self.highlighted = highlighted
198         def right(self):
199                 highlighted = (self.highlighted + 1) % self.PICS_PER_PAGE
200                 if highlighted + self.offset >= len(self.pictures):
201                         highlighted = 0
202                 our_print("right. before:", self.highlighted, ", after:", highlighted)
203                 self.highlighted = highlighted
204         def nextPage(self):
205                 our_print("nextPage")
206                 offset = self.offset + self.PICS_PER_PAGE
207                 Len = len(self.pictures)
208                 if offset > Len:
209                         self.offset = 0
210                 else:
211                         self.offset = offset
212                         if offset + self.highlighted > Len:
213                                 self.highlighted = Len - offset - 1
214                 self.setup()
215         def prevPage(self):
216                 our_print("prevPage")
217                 offset = self.offset - self.PICS_PER_PAGE
218                 if offset < 0:
219                         Len = len(self.pictures)
220                         offset = Len - (Len % self.PICS_PER_PAGE)
221                         self.offset = offset
222                         if offset + self.highlighted > Len:
223                                 self.highlighted = Len - offset - 1
224                 else:
225                         self.offset = offset
226                 self.setup()
227
228         def prevFunc(self):
229                 old = self.highlighted
230                 self.left()
231                 highlighted = self.highlighted
232                 if highlighted > old:
233                         self.prevPage()
234
235                 photo = None
236                 try:
237                         # NOTE: using self.highlighted as prevPage might have moved this if the page is not full
238                         photo = self.pictures[self.highlighted+self.offset]
239                 except IndexError:
240                         pass
241                 return photo
242
243         def nextFunc(self):
244                 old = self.highlighted
245                 self.right()
246                 highlighted = self.highlighted
247                 if highlighted < old:
248                         self.nextPage()
249
250                 photo = None
251                 try:
252                         # NOTE: using self.highlighted as nextPage might have moved this if the page is not full
253                         photo = self.pictures[self.highlighted+self.offset]
254                 except IndexError:
255                         pass
256                 return photo
257
258         def select(self):
259                 try:
260                         photo = self.pictures[self.highlighted+self.offset]
261                 except IndexError:
262                         our_print("no such picture")
263                         # TODO: indicate in gui
264                 else:
265                         self.session.open(EcasaPicture, photo, api=self.api, prevFunc=self.prevFunc, nextFunc=self.nextFunc)
266         def albums(self):
267                 self.session.open(EcasaAlbumview, self.api)
268         def search(self):
269                 self.session.openWithCallback(
270                         self.searchCallback,
271                         NTIVirtualKeyBoard,
272                         title = _("Enter text to search for")
273                 )
274         def searchCallback(self, text=None):
275                 if text:
276                         thread = EcasaThread(lambda:self.api.getSearch(text, limit=str(self.PICS_PER_PAGE)))
277                         self.session.open(EcasaFeedview, thread, api=self.api)
278         def contextMenu(self):
279                 self.session.openWithCallback(self.setupClosed, EcasaSetup)
280         def setupClosed(self):
281                 self.api.setCredentials(
282                         config.plugins.ecasa.google_username.value,
283                         config.plugins.ecasa.google_password.value
284                 )
285                 self.api.cache = config.plugins.ecasa.cache.value
286
287         def gotPictures(self, pictures):
288                 if not self.instance: return
289                 self.pictures = pictures
290                 self.setup()
291
292         def errorPictures(self, error):
293                 if not self.instance: return
294                 our_print("errorPictures", error)
295                 self.session.open(
296                         MessageBox,
297                         _("Error downloading") + ': ' + error.message,
298                         type=MessageBox.TYPE_ERROR,
299                         timeout=3
300                 )
301
302 class EcasaOverview(EcasaPictureWall):
303         """Overview and supposed entry point of ecasa. Shows featured pictures on the "EcasaPictureWall"."""
304         def __init__(self, session):
305                 EcasaPictureWall.__init__(self, session)
306                 self.skinName = ["EcasaOverview", "EcasaPictureWall"]
307                 thread = EcasaThread(self.api.getFeatured)
308                 thread.deferred.addCallbacks(self.gotPictures, self.errorPictures)
309                 thread.start()
310
311 class EcasaFeedview(EcasaPictureWall):
312         """Display a nonspecific feed."""
313         def __init__(self, session, thread, api=None):
314                 EcasaPictureWall.__init__(self, session, api=api)
315                 self.skinName = ["EcasaFeedview", "EcasaPictureWall"]
316                 self['key_green'].text = ''
317                 thread.deferred.addCallbacks(self.gotPictures, self.errorPictures)
318                 thread.start()
319
320         def albums(self):
321                 pass
322
323 class EcasaAlbumview(Screen, HelpableScreen):
324         """Displays albums."""
325         skin = """<screen position="center,center" size="560,420">
326                 <ePixmap pixmap="skin_default/buttons/red.png" position="0,0" size="140,40" transparent="1" alphatest="on" />
327                 <ePixmap pixmap="skin_default/buttons/green.png" position="140,0" size="140,40" transparent="1" alphatest="on" />
328                 <ePixmap pixmap="skin_default/buttons/yellow.png" position="280,0" size="140,40" transparent="1" alphatest="on" />
329                 <ePixmap pixmap="skin_default/buttons/blue.png" position="420,0" size="140,40" transparent="1" alphatest="on" />
330                 <widget source="key_red" render="Label" position="0,0" zPosition="1" size="140,40" font="Regular;20" valign="center" halign="center" backgroundColor="#1f771f" transparent="1" />
331                 <widget source="key_green" render="Label" position="140,0" zPosition="1" size="140,40" font="Regular;20" valign="center" halign="center" backgroundColor="#1f771f" transparent="1" />
332                 <widget source="key_yellow" render="Label" position="280,0" zPosition="1" size="140,40" font="Regular;20" valign="center" halign="center" backgroundColor="#1f771f" transparent="1" />
333                 <widget source="key_blue" render="Label" position="420,0" zPosition="1" size="140,40" font="Regular;20" valign="center" halign="center" backgroundColor="#1f771f" transparent="1" />
334                 <widget source="list" render="Listbox" position="0,50" size="560,360" scrollbarMode="showAlways">
335                         <convert type="TemplatedMultiContent">
336                                 {"template": [
337                                                 MultiContentEntryText(pos=(1,1), size=(540,22), text = 0, font = 0, flags = RT_HALIGN_LEFT|RT_VALIGN_CENTER),
338                                         ],
339                                   "fonts": [gFont("Regular", 20)],
340                                   "itemHeight": 24
341                                  }
342                         </convert>
343                 </widget>
344         </screen>"""
345         def __init__(self, session, api, user='default'):
346                 Screen.__init__(self, session)
347                 HelpableScreen.__init__(self)
348                 self.api = api
349                 self.user = user
350
351                 self['list'] = List()
352                 self['key_red'] = StaticText(_("Close"))
353                 self['key_green'] = StaticText()
354                 self['key_yellow'] = StaticText()
355                 self['key_blue'] = StaticText()
356
357                 self["albumviewActions"] = HelpableActionMap(self, "EcasaAlbumviewActions", {
358                         "select":(self.select, _("show album")),
359                         "exit":(self.close, _("Close")),
360                 }, -1)
361
362                 self.acquireAlbumsForUser(user)
363                 self.onLayoutFinish.append(self.layoutFinished)
364
365         def layoutFinished(self):
366                 self.setTitle(_("eCasa: Albums for user %s") % (self.user,))
367
368         def acquireAlbumsForUser(self, user):
369                 thread = EcasaThread(lambda:self.api.getAlbums(user=user))
370                 thread.deferred.addCallbacks(self.gotAlbums, self.errorAlbums)
371                 thread.start()
372
373         def gotAlbums(self, albums):
374                 if not self.instance: return
375                 self['list'].list = albums
376
377         def errorAlbums(self, error):
378                 if not self.instance: return
379                 our_print("errorAlbums", error)
380                 self['list'].setList([(_("Error downloading"), "0", None)])
381                 self.session.open(
382                         MessageBox,
383                         _("Error downloading") + ': ' + error.value.message,
384                         type=MessageBox.TYPE_ERROR,
385                         timeout=30,
386                 )
387
388         def select(self):
389                 cur = self['list'].getCurrent()
390                 if cur:
391                         album = cur[-1]
392                         thread = EcasaThread(lambda:self.api.getAlbum(album))
393                         self.session.open(EcasaFeedview, thread, api=self.api)
394
395 class EcasaPicture(Screen, HelpableScreen):
396         """Display a single picture and its metadata."""
397         PAGE_PICTURE = 0
398         PAGE_INFO = 1
399         def __init__(self, session, photo, api=None, prevFunc=None, nextFunc=None):
400                 size_w = getDesktop(0).size().width()
401                 size_h = getDesktop(0).size().height()
402                 self.skin = """<screen position="0,0" size="{size_w},{size_h}" title="{title}" flags="wfNoBorder">
403                         <widget name="pixmap" position="0,0" size="{size_w},{size_h}" backgroundColor="black" zPosition="2"/>
404                         <widget source="title" render="Label" position="25,20" zPosition="1" size="{labelwidth},40" valign="center" halign="left" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1"/>
405                         <widget source="summary" render="Label" position="25,60" zPosition="1" size="{labelwidth},100" valign="top" halign="left" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1"/>
406                         <widget source="keywords" render="Label" position="25,160" zPosition="1" size="{labelwidth},40" valign="center" halign="left" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1"/>
407                         <widget source="camera" render="Label" position="25,180" zPosition="1" size="{labelwidth},40" valign="center" halign="left" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1"/>
408                 </screen>""".format(size_w=size_w,size_h=size_h,title=(photo.title.text or '').encode('utf-8'), labelwidth=size_w-50)
409                 Screen.__init__(self, session)
410                 HelpableScreen.__init__(self)
411
412                 self.api = api
413                 self.page = self.PAGE_PICTURE
414                 self.prevFunc = prevFunc
415                 self.nextFunc = nextFunc
416
417                 self['pixmap'] = Pixmap()
418                 self['camera'] = StaticText()
419                 self['title'] = StaticText()
420                 self['summary'] = StaticText()
421                 self['keywords'] = StaticText()
422
423                 self["pictureActions"] = HelpableActionMap(self, "EcasaPictureActions", {
424                         "info": (self.info, _("show metadata")),
425                         "exit": (self.close, _("Close")),
426                         }, -1)
427                 if prevFunc and nextFunc:
428                         self["directionActions"] = HelpableActionMap(self, "DirectionActions", {
429                                 "left": self.previous,
430                                 "right": self.next,
431                                 }, -2)
432
433                 self.picload = ePicLoad()
434                 self.picload.PictureData.get().append(self.gotPicture)
435
436                 # populate with data, initiate download
437                 self.reloadData(photo)
438
439         def gotPicture(self, picInfo=None):
440                 our_print("picture decoded")
441                 ptr = self.picload.getData()
442                 if ptr is not None:
443                         self['pixmap'].instance.setPixmap(ptr.__deref__())
444
445         def cbDownload(self, tup):
446                 if not self.instance: return
447                 filename, photo = tup
448                 self.picload.startDecode(filename)
449
450         def ebDownload(self, tup):
451                 if not self.instance: return
452                 error, photo = tup
453                 print("ebDownload", error)
454                 self.session.open(
455                         MessageBox,
456                         _("Error downloading") + ': ' + error.message,
457                         type=MessageBox.TYPE_ERROR,
458                         timeout=3
459                 )
460
461         def info(self):
462                 our_print("info")
463                 if self.page == self.PAGE_PICTURE:
464                         self.page = self.PAGE_INFO
465                         self['pixmap'].hide()
466                 else:
467                         self.page = self.PAGE_PICTURE
468                         self['pixmap'].show()
469
470         def reloadData(self, photo):
471                 if photo is None: return
472                 self.photo = photo
473                 unk = _("unknown")
474
475                 # camera
476                 if photo.exif.make and photo.exif.model:
477                         camera = '%s %s' % (photo.exif.make.text, photo.exif.model.text)
478                 elif photo.exif.make:
479                         camera = photo.exif.make.text
480                 elif photo.exif.model:
481                         camera = photo.exif.model.text
482                 else:
483                         camera = unk
484                 self['camera'].text = _("Camera: %s") % (camera,)
485
486                 title = photo.title.text if photo.title.text else unk
487                 self['title'].text = _("Title: %s") % (title,)
488                 summary = strip_readable(photo.summary.text) if photo.summary.text else unk
489                 self['summary'].text = _("Summary: %s") % (summary,)
490                 if photo.media and photo.media.keywords and photo.media.keywords.text:
491                         keywords = photo.media.keywords.text
492                         # TODO: find a better way to handle this
493                         if len(keywords) > 50:
494                                 keywords = keywords[:47] + "..."
495                 else:
496                         keywords = unk
497                 self['keywords'].text = _("Keywords: %s") % (keywords,)
498
499                 try:
500                         real_w = int(photo.media.content[0].width.text)
501                         real_h = int(photo.media.content[0].heigth.text)
502                 except Exception as e:
503                         our_print("EcasaPicture.__init__: illegal w/h values, using max size!")
504                         size = getDesktop(0).size()
505                         real_w = size.width()
506                         real_h = size.height()
507
508                 sc = AVSwitch().getFramebufferScale()
509                 self.picload.setPara((real_w, real_h, sc[0], sc[1], False, 1, '#ff000000'))
510
511                 # NOTE: no need to start an extra thread for this, twisted is "parallel" enough in this case
512                 self.api.downloadPhoto(photo).addCallbacks(self.cbDownload, self.ebDownload)
513
514         def previous(self):
515                 if self.prevFunc: self.reloadData(self.prevFunc())
516         def next(self):
517                 if self.nextFunc: self.reloadData(self.nextFunc())
518
519 #pragma mark - Thread
520
521 import threading
522 from twisted.internet import defer
523
524 class EcasaThread(threading.Thread):
525         def __init__(self, fnc):
526                 threading.Thread.__init__(self)
527                 self.deferred = defer.Deferred()
528                 self.__pump = ePythonMessagePump()
529                 self.__pump.recv_msg.get().append(self.gotThreadMsg)
530                 self.__asyncFunc = fnc
531                 self.__result = None
532                 self.__err = None
533
534         def gotThreadMsg(self, msg):
535                 if self.__err:
536                         self.deferred.errback(self.__err)
537                 else:
538                         try:
539                                 self.deferred.callback(self.__result)
540                         except Exception as e:
541                                 self.deferred.errback(e)
542
543         def run(self):
544                 try:
545                         self.__result = self.__asyncFunc()
546                 except Exception as e:
547                         self.__err = e
548                 finally:
549                         self.__pump.send(0)