ecasa: add download functionality and basic slideshow.
[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.ChoiceBox import ChoiceBox
7 from Screens.Screen import Screen
8 from Screens.HelpMenu import HelpableScreen
9 from Screens.InfoBarGenerics import InfoBarNotifications
10 from Screens.LocationBox import LocationBox
11 from Screens.MessageBox import MessageBox
12 from NTIVirtualKeyBoard import NTIVirtualKeyBoard
13 from EcasaSetup import EcasaSetup
14
15 #pragma mark Components
16 from Components.ActionMap import HelpableActionMap
17 from Components.AVSwitch import AVSwitch
18 from Components.Label import Label
19 from Components.Pixmap import Pixmap, MovingPixmap
20 from Components.Sources.StaticText import StaticText
21 from Components.Sources.List import List
22
23 #pragma mark Configuration
24 from Components.config import config
25
26 #pragma mark Picasa
27 from .PicasaApi import PicasaApi
28 from TagStrip import strip_readable
29
30 from enigma import ePicLoad, ePythonMessagePump, eTimer, getDesktop
31 from Tools.Directories import resolveFilename, SCOPE_PLUGINS
32 from Tools.Notifications import AddPopup
33 from collections import deque
34
35 try:
36         xrange = xrange
37 except NameError:
38         xrange = range
39
40 our_print = lambda *args, **kwargs: print("[EcasaGui]", *args, **kwargs)
41
42 AUTHENTICATION_ERROR_ID = "EcasaAuthenticationError"
43
44 class EcasaPictureWall(Screen, HelpableScreen, InfoBarNotifications):
45         """Base class for so-called "picture walls"."""
46         PICS_PER_PAGE = 15
47         PICS_PER_ROW = 5
48         skin = """<screen position="center,center" size="600,380">
49                 <ePixmap position="0,0" size="140,40" pixmap="skin_default/buttons/red.png" transparent="1" alphatest="on"/>
50                 <ePixmap position="140,0" size="140,40" pixmap="skin_default/buttons/green.png" transparent="1" alphatest="on"/>
51                 <ePixmap position="280,0" size="140,40" pixmap="skin_default/buttons/yellow.png" transparent="1" alphatest="on"/>
52                 <ePixmap position="420,0" size="140,40" pixmap="skin_default/buttons/blue.png" transparent="1" alphatest="on"/>
53                 <ePixmap position="565,10" size="35,25" pixmap="skin_default/buttons/key_menu.png" alphatest="on"/>
54                 <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"/>
55                 <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"/>
56                 <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"/>
57                 <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"/>
58                 <widget name="waitingtext" position="100,179" size="400,22" valign="center" halign="center" font="Regular;22"/>
59                 <widget name="image0"  position="30,50"   size="90,90"/>
60                 <widget name="image1"  position="140,50"  size="90,90"/>
61                 <widget name="image2"  position="250,50"  size="90,90"/>
62                 <widget name="image3"  position="360,50"  size="90,90"/>
63                 <widget name="image4"  position="470,50"  size="90,90"/>
64                 <widget name="image5"  position="30,160"  size="90,90"/>
65                 <widget name="image6"  position="140,160" size="90,90"/>
66                 <widget name="image7"  position="250,160" size="90,90"/>
67                 <widget name="image8"  position="360,160" size="90,90"/>
68                 <widget name="image9"  position="470,160" size="90,90"/>
69                 <widget name="image10" position="30,270"  size="90,90"/>
70                 <widget name="image11" position="140,270" size="90,90"/>
71                 <widget name="image12" position="250,270" size="90,90"/>
72                 <widget name="image13" position="360,270" size="90,90"/>
73                 <widget name="image14" position="470,270" size="90,90"/>
74                 <!-- TODO: find some better picture -->
75                 <widget name="highlight" position="30,142" size="90,5"/>
76                 </screen>"""
77         def __init__(self, session, api=None):
78                 Screen.__init__(self, session)
79                 HelpableScreen.__init__(self)
80                 InfoBarNotifications.__init__(self)
81
82                 if api is None:
83                         self.api = PicasaApi(cache=config.plugins.ecasa.cache.value)
84                         try:
85                                 self.api.setCredentials(
86                                         config.plugins.ecasa.google_username.value,
87                                         config.plugins.ecasa.google_password.value
88                                 )
89                         except Exception as e:
90                                 AddPopup(
91                                         _("Unable to authenticate with Google: %s.") % (e.message),
92                                         MessageBox.TYPE_ERROR,
93                                         5,
94                                         id=AUTHENTICATION_ERROR_ID,
95                                 )
96                 else:
97                         self.api = api
98
99                 self["key_red"] = StaticText(_("Close"))
100                 self["key_green"] = StaticText(_("Albums"))
101                 self["key_yellow"] = StaticText()
102                 self["key_blue"] = StaticText(_("Search"))
103                 for i in xrange(self.PICS_PER_PAGE):
104                         self['image%d' % i] = Pixmap()
105                         self['title%d' % i] = StaticText()
106                 self["highlight"] = MovingPixmap()
107                 self["waitingtext"] = Label(_("Please wait... Loading list..."))
108
109                 self["overviewActions"] = HelpableActionMap(self, "EcasaOverviewActions", {
110                         "up": self.up,
111                         "down": self.down,
112                         "left": self.left,
113                         "right": self.right,
114                         "nextPage": (self.nextPage, _("show next page")),
115                         "prevPage": (self.prevPage, _("show previous page")),
116                         "select": self.select,
117                         "exit":self.close,
118                         "albums":(self.albums, _("show your albums (if logged in)")),
119                         "search":(self.search, _("start a new search")),
120                         "contextMenu":(self.contextMenu, _("open context menu")),
121                         }, -1)
122
123                 self.offset = 0
124                 self.__highlighted = 0
125                 self.pictures = ()
126
127                 # thumbnail loader
128                 self.picload = ePicLoad()
129                 self.picload.PictureData.get().append(self.gotPicture)
130                 sc = AVSwitch().getFramebufferScale()
131                 self.picload.setPara((90, 90, sc[0], sc[1], False, 1, '#ff000000')) # TODO: hardcoded size is evil!
132                 self.currentphoto = None
133                 self.queue = deque()
134
135                 self.onLayoutFinish.append(self.layoutFinished)
136
137         def layoutFinished(self):
138                 self["highlight"].instance.setPixmapFromFile(resolveFilename(SCOPE_PLUGINS, "Extensions/Ecasa/highlighted.png"))
139                 self["highlight"].hide()
140
141         @property
142         def highlighted(self):
143                 return self.__highlighted
144
145         @highlighted.setter
146         def highlighted(self, highlighted):
147                 our_print("setHighlighted", highlighted)
148                 # only allow to select valid pictures
149                 if highlighted + self.offset >= len(self.pictures): return
150
151                 self.__highlighted = highlighted
152                 pixmap = self['image%d' % highlighted]
153                 origpos = pixmap.getPosition()
154                 origsize = pixmap.instance.size()
155                 # TODO: hardcoded highlight offset is evil :P
156                 self["highlight"].moveTo(origpos[0], origpos[1]+origsize.height()+2, 1)
157                 self["highlight"].startMoving()
158
159         def gotPicture(self, picInfo=None):
160                 ptr = self.picload.getData()
161                 idx = self.pictures.index(self.currentphoto)
162                 realIdx = idx - self.offset
163                 if ptr is not None:
164                         self['image%d' % realIdx].instance.setPixmap(ptr.__deref__())
165                 else:
166                         our_print("gotPicture got invalid results for idx", idx, "("+str(realIdx)+")")
167                         # NOTE: we could use a different picture here that indicates a failure
168                         self['image%d' % realIdx].instance.setPixmap(None)
169                         # NOTE: the thread WILL most likely be hung and NOT recover from it, so we should remove the old picload and create a new one :/
170                 self.currentphoto = None
171                 self.maybeDecode()
172
173         def maybeDecode(self):
174                 if self.currentphoto is not None: return
175                 try:
176                         filename, self.currentphoto = self.queue.pop()
177                 except IndexError:
178                         our_print("no queued photos")
179                         # no more pictures
180                         pass
181                 else:
182                         self.picload.startDecode(filename)
183
184         def pictureDownloaded(self, tup):
185                 filename, photo = tup
186                 self.queue.append((filename, photo))
187                 self.maybeDecode()
188
189         def pictureDownloadFailed(self, tup):
190                 error, photo = tup
191                 our_print("pictureDownloadFailed", error, photo)
192                 # TODO: indicate in gui
193
194         def setup(self):
195                 our_print("setup")
196                 self["waitingtext"].hide()
197                 self["highlight"].show()
198                 self.queue.clear()
199                 pictures = self.pictures
200                 for i in xrange(self.PICS_PER_PAGE):
201                         try:
202                                 our_print("trying to initiate download of idx", i+self.offset)
203                                 picture = pictures[i+self.offset]
204                                 self.api.downloadThumbnail(picture).addCallbacks(self.pictureDownloaded, self.pictureDownloadFailed)
205                         except IndexError:
206                                 # no more pictures
207                                 self['image%d' % i].instance.setPixmap(None)
208                         except Exception as e:
209                                 our_print("unexpected exception in setup:", e)
210
211         def up(self):
212                 # TODO: implement for incomplete pages
213                 highlighted = (self.highlighted - self.PICS_PER_ROW) % self.PICS_PER_PAGE
214                 our_print("up. before:", self.highlighted, ", after:", highlighted)
215                 self.highlighted = highlighted
216
217                 # we requested an invalid idx
218                 if self.highlighted != highlighted:
219                         # so skip another row
220                         highlighted = (highlighted - self.PICS_PER_ROW) % self.PICS_PER_PAGE
221                         our_print("up2. before:", self.highlighted, ", after:", highlighted)
222                         self.highlighted = highlighted
223
224         def down(self):
225                 # TODO: implement for incomplete pages
226                 highlighted = (self.highlighted + self.PICS_PER_ROW) % self.PICS_PER_PAGE
227                 our_print("down. before:", self.highlighted, ", after:", highlighted)
228                 self.highlighted = highlighted
229
230                 # we requested an invalid idx
231                 if self.highlighted != highlighted:
232                         # so try to skip another row
233                         highlighted = (highlighted + self.PICS_PER_ROW) % self.PICS_PER_PAGE
234                         our_print("down2. before:", self.highlighted, ", after:", highlighted)
235                         self.highlighted = highlighted
236
237         def left(self):
238                 # TODO: implement for incomplete pages
239                 highlighted = (self.highlighted - 1) % self.PICS_PER_PAGE
240                 our_print("left. before:", self.highlighted, ", after:", highlighted)
241                 self.highlighted = highlighted
242
243                 # we requested an invalid idx
244                 if self.highlighted != highlighted:
245                         # go to last possible item
246                         highlighted = (len(self.pictures) - 1) % self.PICS_PER_PAGE
247                         our_print("left2. before:", self.highlighted, ", after:", highlighted)
248                         self.highlighted = highlighted
249
250         def right(self):
251                 highlighted = (self.highlighted + 1) % self.PICS_PER_PAGE
252                 if highlighted + self.offset >= len(self.pictures):
253                         highlighted = 0
254                 our_print("right. before:", self.highlighted, ", after:", highlighted)
255                 self.highlighted = highlighted
256         def nextPage(self):
257                 our_print("nextPage")
258                 if not self.pictures: return
259                 offset = self.offset + self.PICS_PER_PAGE
260                 Len = len(self.pictures)
261                 if offset >= Len:
262                         self.offset = 0
263                 else:
264                         self.offset = offset
265                         if offset + self.highlighted > Len:
266                                 self.highlighted = Len - offset - 1
267                 self.setup()
268         def prevPage(self):
269                 our_print("prevPage")
270                 if not self.pictures: return
271                 offset = self.offset - self.PICS_PER_PAGE
272                 if offset < 0:
273                         Len = len(self.pictures) - 1
274                         offset = Len - (Len % self.PICS_PER_PAGE)
275                         self.offset = offset
276                         if offset + self.highlighted >= Len:
277                                 self.highlighted = Len - offset
278                 else:
279                         self.offset = offset
280                 self.setup()
281
282         def prevFunc(self):
283                 old = self.highlighted
284                 self.left()
285                 highlighted = self.highlighted
286                 if highlighted > old:
287                         self.prevPage()
288
289                 photo = None
290                 try:
291                         # NOTE: using self.highlighted as prevPage might have moved this if the page is not full
292                         photo = self.pictures[self.highlighted+self.offset]
293                 except IndexError:
294                         pass
295                 return photo
296
297         def nextFunc(self):
298                 old = self.highlighted
299                 self.right()
300                 highlighted = self.highlighted
301                 if highlighted < old:
302                         self.nextPage()
303
304                 photo = None
305                 try:
306                         # NOTE: using self.highlighted as nextPage might have moved this if the page is not full
307                         photo = self.pictures[self.highlighted+self.offset]
308                 except IndexError:
309                         pass
310                 return photo
311
312         def select(self):
313                 try:
314                         photo = self.pictures[self.highlighted+self.offset]
315                 except IndexError:
316                         our_print("no such picture")
317                         # TODO: indicate in gui
318                 else:
319                         self.session.open(EcasaPicture, photo, api=self.api, prevFunc=self.prevFunc, nextFunc=self.nextFunc)
320         def albums(self):
321                 self.session.open(EcasaAlbumview, self.api, user=config.plugins.ecasa.user.value)
322         def search(self):
323                 self.session.openWithCallback(
324                         self.searchCallback,
325                         NTIVirtualKeyBoard,
326                         title=_("Enter text to search for")
327                 )
328         def searchCallback(self, text=None):
329                 if text:
330                         # Maintain history
331                         history = config.plugins.ecasa.searchhistory.value
332                         if text not in history:
333                                 history.insert(0, text)
334                                 del history[10:]
335                         else:
336                                 history.remove(text)
337                                 history.insert(0, text)
338                         config.plugins.ecasa.searchhistory.save()
339
340                         # Workaround to allow search for umlauts if we know the encoding (pretty bad, I know...)
341                         thread = EcasaThread(lambda:self.api.getSearch(text, limit=str(config.plugins.ecasa.searchlimit.value)))
342                         self.session.open(EcasaFeedview, thread, api=self.api, title=_("Search for %s") % (text))
343
344         def contextMenu(self):
345                 options = [
346                         (_("Setup"), lambda: self.session.openWithCallback(self.setupClosed, EcasaSetup)),
347                         (_("Search History"), self.openHistory),
348                 ]
349                 self.session.openWithCallback(
350                         self.menuCallback,
351                         ChoiceBox,
352                         list=options
353                 )
354
355         def menuCallback(self, ret=None):
356                 if ret:
357                         ret[1]()
358
359         def openHistory(self):
360                 options = [(x, x) for x in config.plugins.ecasa.searchhistory.value]
361
362                 if options:
363                         self.session.openWithCallback(
364                                 self.historyWrapper,
365                                 ChoiceBox,
366                                 title=_("Select text to search for"),
367                                 list=options
368                         )
369                 else:
370                         self.session.open(
371                                 MessageBox,
372                                 _("No history"),
373                                 type=MessageBox.TYPE_INFO
374                         )
375
376         def historyWrapper(self, ret):
377                 if ret:
378                         self.searchCallback(ret[1])
379
380         def setupClosed(self):
381                 try:
382                         self.api.setCredentials(
383                                 config.plugins.ecasa.google_username.value,
384                                 config.plugins.ecasa.google_password.value
385                         )
386                 except Exception as e:
387                         AddPopup(
388                                 _("Unable to authenticate with Google: %s.") % (e.message),
389                                 MessageBox.TYPE_ERROR,
390                                 5,
391                                 id=AUTHENTICATION_ERROR_ID,
392                         )
393                 self.api.cache = config.plugins.ecasa.cache.value
394
395         def gotPictures(self, pictures):
396                 if not self.instance: return
397                 self.pictures = pictures
398                 self.setup()
399
400         def errorPictures(self, error):
401                 if not self.instance: return
402                 our_print("errorPictures", error)
403                 self.session.open(
404                         MessageBox,
405                         _("Error downloading") + ': ' + error.message,
406                         type=MessageBox.TYPE_ERROR,
407                         timeout=3
408                 )
409
410 class EcasaOverview(EcasaPictureWall):
411         """Overview and supposed entry point of ecasa. Shows featured pictures on the "EcasaPictureWall"."""
412         def __init__(self, session):
413                 EcasaPictureWall.__init__(self, session)
414                 self.skinName = ["EcasaOverview", "EcasaPictureWall"]
415                 thread = EcasaThread(self.api.getFeatured)
416                 thread.deferred.addCallbacks(self.gotPictures, self.errorPictures)
417                 thread.start()
418
419                 self.onClose.append(self.__onClose)
420
421         def __onClose(self):
422                 thread = EcasaThread(lambda: self.api.cleanupCache(config.plugins.ecasa.cachesize.value))
423                 thread.start()
424
425         def layoutFinished(self):
426                 EcasaPictureWall.layoutFinished(self)
427                 self.setTitle(_("eCasa: %s") % (_("Featured Photos")))
428
429 class EcasaFeedview(EcasaPictureWall):
430         """Display a nonspecific feed."""
431         def __init__(self, session, thread, api=None, title=None):
432                 EcasaPictureWall.__init__(self, session, api=api)
433                 self.skinName = ["EcasaFeedview", "EcasaPictureWall"]
434                 self.feedTitle = title
435                 self['key_green'].text = ''
436                 thread.deferred.addCallbacks(self.gotPictures, self.errorPictures)
437                 thread.start()
438
439         def layoutFinished(self):
440                 EcasaPictureWall.layoutFinished(self)
441                 self.setTitle(_("eCasa: %s") % (self.feedTitle or _("Album")))
442
443         def albums(self):
444                 pass
445
446 class EcasaAlbumview(Screen, HelpableScreen, InfoBarNotifications):
447         """Displays albums."""
448         skin = """<screen position="center,center" size="560,420">
449                 <ePixmap pixmap="skin_default/buttons/red.png" position="0,0" size="140,40" transparent="1" alphatest="on" />
450                 <ePixmap pixmap="skin_default/buttons/green.png" position="140,0" size="140,40" transparent="1" alphatest="on" />
451                 <ePixmap pixmap="skin_default/buttons/yellow.png" position="280,0" size="140,40" transparent="1" alphatest="on" />
452                 <ePixmap pixmap="skin_default/buttons/blue.png" position="420,0" size="140,40" transparent="1" alphatest="on" />
453                 <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" />
454                 <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" />
455                 <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" />
456                 <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" />
457                 <widget source="list" render="Listbox" position="0,50" size="560,360" scrollbarMode="showAlways">
458                         <convert type="TemplatedMultiContent">
459                                 {"template": [
460                                                 MultiContentEntryText(pos=(1,1), size=(540,22), text = 0, font = 0, flags = RT_HALIGN_LEFT|RT_VALIGN_CENTER),
461                                         ],
462                                   "fonts": [gFont("Regular", 20)],
463                                   "itemHeight": 24
464                                  }
465                         </convert>
466                 </widget>
467         </screen>"""
468         def __init__(self, session, api, user='default'):
469                 Screen.__init__(self, session)
470                 HelpableScreen.__init__(self)
471                 InfoBarNotifications.__init__(self)
472                 self.api = api
473                 self.user = user
474
475                 self['list'] = List()
476                 self['key_red'] = StaticText(_("Close"))
477                 self['key_green'] = StaticText()
478                 self['key_yellow'] = StaticText(_("Change user"))
479                 self['key_blue'] = StaticText(_("User history"))
480
481                 self["albumviewActions"] = HelpableActionMap(self, "EcasaAlbumviewActions", {
482                         "select":(self.select, _("show album")),
483                         "exit":(self.close, _("Close")),
484                         "users":(self.users, _("Change user")),
485                         "history":(self.history, _("User history")),
486                 }, -1)
487
488                 self.acquireAlbumsForUser(user)
489                 self.onLayoutFinish.append(self.layoutFinished)
490
491         def layoutFinished(self):
492                 self.setTitle(_("eCasa: Albums for user %s") % (self.user,))
493
494         def acquireAlbumsForUser(self, user):
495                 thread = EcasaThread(lambda:self.api.getAlbums(user=user))
496                 thread.deferred.addCallbacks(self.gotAlbums, self.errorAlbums)
497                 thread.start()
498
499         def gotAlbums(self, albums):
500                 if not self.instance: return
501                 self['list'].list = albums
502
503         def errorAlbums(self, error):
504                 if not self.instance: return
505                 our_print("errorAlbums", error)
506                 self['list'].setList([(_("Error downloading"), "0", None)])
507                 self.session.open(
508                         MessageBox,
509                         _("Error downloading") + ': ' + error.value.message,
510                         type=MessageBox.TYPE_ERROR,
511                         timeout=30,
512                 )
513
514         def select(self):
515                 cur = self['list'].getCurrent()
516                 if cur:
517                         album = cur[-1]
518                         thread = EcasaThread(lambda:self.api.getAlbum(album))
519                         self.session.open(EcasaFeedview, thread, api=self.api, title=album.title and album.title.text)
520
521         def users(self):
522                 self.session.openWithCallback(
523                         self.searchCallback,
524                         NTIVirtualKeyBoard,
525                         title = _("Enter username")
526                 )
527         def searchCallback(self, text=None):
528                 if text:
529                         # Maintain history
530                         history = config.plugins.ecasa.userhistory.value
531                         if text not in history:
532                                 history.insert(0, text)
533                                 del history[10:]
534                         else:
535                                 history.remove(text)
536                                 history.insert(0, text)
537                         config.plugins.ecasa.userhistory.save()
538
539                         # Workaround to allow search for umlauts if we know the encoding (pretty bad, I know...)
540                         self.session.openWithCallback(self.close, EcasaAlbumview, self.api, text)
541
542         def history(self):
543                 options = [(x, x) for x in config.plugins.ecasa.userhistory.value]
544
545                 if options:
546                         self.session.openWithCallback(
547                                 self.historyWrapper,
548                                 ChoiceBox,
549                                 title=_("Select user"),
550                                 list=options
551                         )
552                 else:
553                         self.session.open(
554                                 MessageBox,
555                                 _("No history"),
556                                 type=MessageBox.TYPE_INFO
557                         )
558
559         def historyWrapper(self, ret):
560                 if ret:
561                         self.searchCallback(ret[1])
562
563 class EcasaPicture(Screen, HelpableScreen, InfoBarNotifications):
564         """Display a single picture and its metadata."""
565         PAGE_PICTURE = 0
566         PAGE_INFO = 1
567         def __init__(self, session, photo, api=None, prevFunc=None, nextFunc=None):
568                 size_w = getDesktop(0).size().width()
569                 size_h = getDesktop(0).size().height()
570                 self.skin = """<screen position="0,0" size="{size_w},{size_h}" flags="wfNoBorder">
571                         <widget name="pixmap" position="0,0" size="{size_w},{size_h}" backgroundColor="black" zPosition="2"/>
572                         <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"/>
573                         <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"/>
574                         <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"/>
575                         <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"/>
576                 </screen>""".format(size_w=size_w,size_h=size_h,labelwidth=size_w-50)
577                 Screen.__init__(self, session)
578                 HelpableScreen.__init__(self)
579                 InfoBarNotifications.__init__(self)
580
581                 self.api = api
582                 self.page = self.PAGE_PICTURE
583                 self.prevFunc = prevFunc
584                 self.nextFunc = nextFunc
585                 self.nextPhoto = None
586
587                 self['pixmap'] = Pixmap()
588                 self['camera'] = StaticText()
589                 self['title'] = StaticText()
590                 self['summary'] = StaticText()
591                 self['keywords'] = StaticText()
592
593                 self["pictureActions"] = HelpableActionMap(self, "EcasaPictureActions", {
594                         "info": (self.info, _("show metadata")),
595                         "exit": (self.close, _("Close")),
596                         "contextMenu":(self.contextMenu, _("open context menu")),
597                         }, -1)
598                 if prevFunc and nextFunc:
599                         self["directionActions"] = HelpableActionMap(self, "DirectionActions", {
600                                 "left": self.previous,
601                                 "right": self.next,
602                                 }, -2)
603
604                 self.picload = ePicLoad()
605                 self.picload.PictureData.get().append(self.gotPicture)
606                 self.timer = eTimer()
607                 self.timer.callback.append(self.timerFired)
608
609                 # populate with data, initiate download
610                 self.reloadData(photo)
611
612                 self.onClose.append(self.__onClose)
613
614         def __onClose(self):
615                 if self.nextPhoto is not None:
616                         self.toggleSlideshow()
617
618         def gotPicture(self, picInfo=None):
619                 our_print("picture decoded")
620                 ptr = self.picload.getData()
621                 if ptr is not None:
622                         self['pixmap'].instance.setPixmap(ptr.__deref__())
623                         if self.page == self.PAGE_PICTURE:
624                                 self['pixmap'].show()
625                         if self.nextPhoto is not None:
626                                 self.timer.start(config.plugins.ecasa.slideshow_interval.value*1000, True)
627
628         def cbDownload(self, tup):
629                 if not self.instance: return
630                 filename, photo = tup
631                 self.picload.startDecode(filename)
632
633         def ebDownload(self, tup):
634                 if not self.instance: return
635                 error, photo = tup
636                 print("ebDownload", error)
637                 self.session.open(
638                         MessageBox,
639                         _("Error downloading") + ': ' + error.message,
640                         type=MessageBox.TYPE_ERROR,
641                         timeout=3
642                 )
643
644         def info(self):
645                 our_print("info")
646                 if self.page == self.PAGE_PICTURE:
647                         self.page = self.PAGE_INFO
648                         self['pixmap'].hide()
649                 else:
650                         self.page = self.PAGE_PICTURE
651                         self['pixmap'].show()
652
653         def contextMenu(self):
654                 options = [
655                         (_("Download Picture"), self.doDownload),
656                 ]
657                 if self.prevFunc and self.nextFunc:
658                         options.append(
659                                 (_("Start Slideshow") if self.nextPhoto is None else _("Stop Slideshow"), self.toggleSlideshow)
660                         )
661                 self.session.openWithCallback(
662                         self.menuCallback,
663                         ChoiceBox,
664                         list=options
665                 )
666
667         def menuCallback(self, ret=None):
668                 if ret:
669                         ret[1]()
670
671         def doDownload(self):
672                 self.session.openWithCallback(
673                         self.gotFilename,
674                         LocationBox,
675                         _("Where to save?"),
676                         self.photo.media.content[0].url.split('/')[-1],
677                 )
678
679         def gotFilename(self, res):
680                 if res:
681                         try:
682                                 self.api.copyPhoto(self.photo, res)
683                         except Exception as e:
684                                 self.session.open(
685                                         MessageBox,
686                                         _("Unable to download picture: %s") % (e),
687                                         type=MessageBox.TYPE_INFO
688                                 )
689
690         def toggleSlideshow(self):
691                 # is slideshow currently running?
692                 if self.nextPhoto is not None:
693                         self.timer.stop()
694                         self.previous() # we already moved forward in our parent view, so move back
695                         self.nextPhoto = None
696                 else:
697                         self.timer.start(config.plugins.ecasa.slideshow_interval.value*1000, True)
698                         self.timerFired()
699
700         def timerFired(self):
701                 if self.nextPhoto:
702                         self.reloadData(self.nextPhoto)
703                         self.timer.stop()
704                 self.nextPhoto = self.nextFunc()
705                 # XXX: for now, only start download. later on we might want to pre-parse the picture
706                 self.api.downloadPhoto(self.nextPhoto)
707
708         def reloadData(self, photo):
709                 if photo is None: return
710                 self.photo = photo
711                 unk = _("unknown")
712
713                 # camera
714                 if photo.exif.make and photo.exif.model:
715                         camera = '%s %s' % (photo.exif.make.text, photo.exif.model.text)
716                 elif photo.exif.make:
717                         camera = photo.exif.make.text
718                 elif photo.exif.model:
719                         camera = photo.exif.model.text
720                 else:
721                         camera = unk
722                 self['camera'].text = _("Camera: %s") % (camera,)
723
724                 title = photo.title.text if photo.title.text else unk
725                 self.setTitle(_("eCasa: %s") % (title))
726                 self['title'].text = _("Title: %s") % (title,)
727                 summary = strip_readable(photo.summary.text).replace('\n\nView Photo', '') if photo.summary.text else ''
728                 self['summary'].text = summary
729                 if photo.media and photo.media.keywords and photo.media.keywords.text:
730                         keywords = photo.media.keywords.text
731                         # TODO: find a better way to handle this
732                         if len(keywords) > 50:
733                                 keywords = keywords[:47] + "..."
734                 else:
735                         keywords = unk
736                 self['keywords'].text = _("Keywords: %s") % (keywords,)
737
738                 try:
739                         real_w = int(photo.media.content[0].width.text)
740                         real_h = int(photo.media.content[0].heigth.text)
741                 except Exception as e:
742                         our_print("EcasaPicture.__init__: illegal w/h values, using max size!")
743                         size = getDesktop(0).size()
744                         real_w = size.width()
745                         real_h = size.height()
746
747                 sc = AVSwitch().getFramebufferScale()
748                 self.picload.setPara((real_w, real_h, sc[0], sc[1], False, 1, '#ff000000'))
749
750                 # NOTE: no need to start an extra thread for this, twisted is "parallel" enough in this case
751                 self.api.downloadPhoto(photo).addCallbacks(self.cbDownload, self.ebDownload)
752
753         def previous(self):
754                 if self.prevFunc: self.reloadData(self.prevFunc())
755                 self['pixmap'].hide()
756         def next(self):
757                 if self.nextFunc: self.reloadData(self.nextFunc())
758                 self['pixmap'].hide()
759
760 #pragma mark - Thread
761
762 import threading
763 from twisted.internet import defer
764
765 class EcasaThread(threading.Thread):
766         def __init__(self, fnc):
767                 threading.Thread.__init__(self)
768                 self.deferred = defer.Deferred()
769                 self.__pump = ePythonMessagePump()
770                 self.__pump.recv_msg.get().append(self.gotThreadMsg)
771                 self.__asyncFunc = fnc
772                 self.__result = None
773                 self.__err = None
774
775         def gotThreadMsg(self, msg):
776                 if self.__err:
777                         self.deferred.errback(self.__err)
778                 else:
779                         try:
780                                 self.deferred.callback(self.__result)
781                         except Exception as e:
782                                 self.deferred.errback(e)
783
784         def run(self):
785                 try:
786                         self.__result = self.__asyncFunc()
787                 except Exception as e:
788                         self.__err = e
789                 finally:
790                         self.__pump.send(0)