You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

581 lines
26 KiB

# -*- coding: utf-8 -*-
import shutil
from kivy import require
require('1.9.0')
import os, json
from sys import argv
from .meta import version as app_version
import urllib
import re
from lxml import html
import tempfile
from PIL import Image
import tarfile
from kivy.config import Config
# Config.set('graphics', 'fullscreen', 'auto')
Config.set('kivy', 'log_level', 'debug')
from kivy.app import App
from kivy.core.window import Window
from kivy.properties import StringProperty, ListProperty, DictProperty, NumericProperty
from kivy.uix.screenmanager import ScreenManager, SlideTransition, FadeTransition
from kivy.uix.popup import Popup
from kivy.uix.label import Label
from kivy.factory import Factory
from kivy.uix.progressbar import ProgressBar
from .editor import Slide, SlideInfoDialog, DraggableSlide
from .viewer import SlideBox
from kivy.logger import Logger
from kivy.network.urlrequest import UrlRequest
from .search import ItemButton
from kivy.uix.filechooser import FileChooserIconView, FileChooserListView
class HadalyApp(App):
use_kivy_settings = True
def __init__(self):
super(HadalyApp, self).__init__()
self.tempdir = tempfile.mkdtemp()
self.filename = ''
self.dirname = ''
self.presentation = {'app': ('hadaly', app_version),
'title': _('New Title'),
'slides': []}
self.engines = json.load(open('hadaly/data/search_engines.json'))
def build(self):
self.icon = str('data/icon.png')
self.title = str('Hadaly')
root = Manager(transition=FadeTransition(duration=.35))
search_screen = Factory.SearchScreen(name='search')
viewer_screen = Factory.ViewerScreen(name='viewer')
editor_screen = Factory.EditorScreen(name='editor')
root.add_widget(editor_screen)
root.add_widget(viewer_screen)
root.add_widget(search_screen)
return root
def build_config(self, config):
config.add_section('general')
config.add_section('viewer')
config.add_section('editor')
config.add_section('search')
config.set('general', 'switch_on_start', 1)
config.set('editor', 'autosave_time', '15')
config.set('editor', 'autosave', '0')
config.set('editor', 'font_size', '12')
config.set('editor', 'last_dir', os.path.expanduser('~'))
config.set('viewer', 'thumb', '1')
config.set('viewer', 'thumb_pos', 'bottom left')
config.set('viewer', 'font_size', '15')
config.set('viewer', 'caption_pos', 'bottom')
config.set('search', 'search_rpp', '10')
def build_settings(self, settings):
settings.add_json_panel('Hadaly',
self.config,
'hadaly/data/settings_panel.json')
def on_start(self):
try:
if argv[1].endswith('.opah') and tarfile.is_tarfile(argv[1]):
self.load_presentation(os.path.dirname(argv[1]), [os.path.basename(argv[1])])
Logger.info('Application: file \'{file}\' loaded'.format(file=self.filename))
except IndexError:
pass
def load_presentation(self, path, filename):
if len(self.root.get_screen('editor').slides_view.grid_layout.children) > 0:
self.clear()
self.extract_opah(path, filename)
self.load_json()
self.load_slides(path, filename)
if self.config.getint('general', 'switch_on_start'):
self.root.current = 'viewer'
def extract_opah(self, path, filename):
try:
with tarfile.open(os.path.join(path, filename[0]), 'r:*') as tar:
#Check filename in tar
for file in tar.getmembers():
if file.name.startswith(('..', '/')):
raise ValueError
tar.extractall(path=self.tempdir)
except ValueError as msg:
Logger.debug('Application: {msg}'.format(msg=msg))
self.show_popup(_('Error'), _('File is not valid.'))
except IndexError as msg:
Logger.debug('Application: {msg}'.format(msg=msg))
self.show_popup(_('Error'), _('No file selected'))
except PermissionError as msg:
Logger.debug('Application: {msg}'.format(msg=msg))
self.show_popup(_('Error'),
_('You don\'t have permission to access this file.'))
def load_json(self):
try:
with open(os.path.join(self.tempdir,
'presentation.json'), 'r') as fd:
data = json.load(fd)
except ValueError as msg:
Logger.debug('Application (JSON Loading): {msg}'.format(msg=msg))
return data
def load_slides(self, path, filename):
data = self.load_json()
self.dirname = path
self.filename = filename[0]
self.presentation = data
for slide in reversed(self.presentation['slides']):
# TODO: Remove tmp in filename on save
thumb_src = os.path.join(self.tempdir, slide['thumb_src'])
img_src = os.path.join(self.tempdir, slide['img_src'])
index = self.presentation['slides'].index(slide)
self.presentation['slides'][index]['thumb_src'] = thumb_src
self.presentation['slides'][index]['img_src'] = img_src
img_slide = Factory.Slide(img_src=str(img_src),
thumb_src=str(thumb_src),
artist=slide['artist'],
title=slide['title'],
year=slide['year']
)
drag_slide = Factory.DraggableSlide(img=img_slide, app=self)
self.root.get_screen('editor').slides_view.grid_layout.add_widget(drag_slide)
def clear(self):
self.root.current_screen.slides_view.grid_layout.clear_widgets()
self.root.get_screen('viewer').carousel.clear_widgets()
self.presentation = {'app': ('hadaly', __main__.__version__), 'title': 'New Title', 'slides': []}
self.filename = self.dirname = ''
def update_presentation(self, type, old_index, new_index):
"""
:param type: type of update to do : 'mv' (move) or 'rm' (remove) or 'update'
:param old_index: previous index of item in grid layout as an integer.
:param new_index: new index of item in grid layout as an integer.
"""
if type == 'rm':
Logger.debug('Application: Removing presentation item.')
self.presentation['slides'].pop(old_index)
try:
self.root.get_screen('viewer').update_carousel()
except AttributeError:
Logger.exception('Application: Viewer dialog not yet added !')
elif type == 'mv':
Logger.debug('Application: Moving presentation item.')
try:
Logger.debug('Application: Moved {slide} at position (in grid layout) {index} to {new_index}'.format(
slide=self.presentation['slides'][old_index],
index=old_index, new_index=new_index))
item = self.presentation['slides'].pop(old_index)
self.presentation['slides'].insert(new_index, item)
try:
self.root.get_screen('viewer').update_carousel()
except AttributeError:
Logger.exception('Application: Viewer dialog not yet added !')
except TypeError:
Logger.exception('Application: Only one slide in presentation view. (BUG)')
elif type == 'update':
Logger.debug('Application: Updating presentation.')
self.presentation['slides'][old_index] = self.root.get_screen('editor').slides_view.grid_layout.children[
old_index].img.get_slide_info()
def show_open(self):
popup = Factory.OpenDialog()
popup.open()
def show_file_explorer(self):
popup = Popup(size_hint=(0.8, 0.8))
file_explorer = FileChooserListView(filters=['*.jpg', '*.png', '*.jpeg'])
if os.path.exists(self.config.get('editor', 'last_dir')):
file_explorer.path = self.config.get('editor', 'last_dir')
file_explorer.bind(on_submit=self.show_add_slide)
file_explorer.popup = popup
popup.content = file_explorer
popup.title = _('File explorer')
popup.open()
def show_save(self, action):
"""Show save dialog.
If action == 'save', dialog is not shown and file is
saved based on self.dirname and self.filename.
"""
if len(self.presentation['slides']) == 0:
self.show_popup(_('Error'), _('Nothing to save...'))
else:
if self.filename and action == 'save':
self.save(self.dirname, self.filename)
else:
popup = Factory.SaveDialog()
popup.open()
def save(self, path, filename):
"""Save file.
:param path: path where to save to as string
:param filename: filename of file to save as string
"""
if not filename.endswith('.opah'):
filename = '.'.join((filename, 'opah'))
self.filename = filename
self.dirname = path
tar = tarfile.open(os.path.join(path, filename), 'w')
# Add image file to *.opah file
try:
for file in [slide for slide in self.presentation['slides']]:
if os.path.exists(file['img_src']):
tar.add(os.path.relpath(file['img_src']),
arcname=os.path.basename(file['img_src']))
else:
tar.add(os.path.relpath(os.path.join(self.tempdir, file['img_src'])),
arcname=os.path.basename(file['img_src']))
if os.path.exists(file['thumb_src']):
tar.add(os.path.relpath(os.path.join(self.tempdir, file['thumb_src'])),
arcname=os.path.basename(file['thumb_src']))
else:
tar.add(os.path.relpath(file['thumb_src']),
arcname=os.path.basename(file['thumb_src']))
except IndexError:
Logger.exception('Saving:')
except IOError:
Logger.exception('Saving:')
# Create json file with slides metadata
for slide in self.presentation['slides']:
Logger.debug('Save: thumb {thumb} from img {img}'.format(thumb=slide['thumb_src'],
img=slide['img_src']))
slide['img_src'] = os.path.basename(slide['img_src'])
slide['thumb_src'] = os.path.basename(slide['thumb_src'])
try:
with open(os.path.join(self.tempdir, 'presentation.json'), 'w') as fd:
json.dump(self.presentation, fd)
except IOError as msg:
Logger.debug('Application: {msg}'.format(msg=msg[1]))
self.show_popup('Error', msg[1])
# Add json file to *.opah file
tar.add(os.path.join(self.tempdir, 'presentation.json'), 'presentation.json')
tar.close()
def switch_slide(self, index):
"""Switch index of carousel to current index.
:param index:
"""
self.root.current_screen.carousel.index = index
def compare_slide(self, index=None, action='add'):
"""Add new SlideBox to ViewerScreen based on SlideButton index.
:param index: index of slide as int.
:param action: 'add' or 'rm'
"""
if action == 'add':
slide = SlideBox(slide=self.presentation['slides'][index])
self.root.current_screen.box.add_widget(slide, 0)
# Change scatter position to compensate screen split.
pos = self.root.current_screen.carousel.current_slide.viewer.pos
self.root.current_screen.carousel.current_slide.viewer.pos = (pos[0] / 2, pos[1])
elif action == 'rm':
self.root.current_screen.box.remove_widget(self.root.current_screen.box.children[0])
pos = self.root.current_screen.carousel.current_slide.viewer.pos
self.root.current_screen.carousel.current_slide.viewer.pos = (pos[0] * 2, pos[1])
def set_title(self):
popup = Factory.TitleDialog()
popup.open()
def show_add_slide(self, original_src, *args):
"""Show dialog to add a slide.
:param img_source: image path as string.
"""
original_src.popup.dismiss()
self.config.set('editor', 'last_dir',
os.path.dirname(original_src.selection[0]))
thumb_src = self.create_thumbnail(original_src.selection[0])
slide_popup = SlideInfoDialog(slide=Slide(img_src=original_src.selection[0],
thumb_src=thumb_src,
artist='',
title='',
year=''))
slide_popup.open()
def add_slide(self, slide):
"""Add slide to presentation.
:param slide: slide as object
"""
# Logger.debug('Application: Adding to presentation {slide}'.format(slide=slide.get_slide_info()))
img_slide = DraggableSlide(img=slide, app=self)
self.presentation['slides'].insert(0, slide.get_slide_info())
self.root.get_screen('editor').slides_view.grid_layout.add_widget(img_slide)
def create_thumbnail(self, img_src):
Logger.debug('Creating thumbnail from {src}'.format(src=img_src))
image = Image.open(img_src)
if Image.VERSION == '1.1.7':
image.thumbnail((200, 200))
else:
image.thumbnail((200, 200), Image.ANTIALIAS)
thumb_filename = ''.join(('thumb_', os.path.basename(img_src)))
thumb_src = os.path.join(self.tempdir, thumb_filename)
image.save(thumb_src)
Logger.debug('Application: Thumbnail created at {thumb}'.format(thumb=thumb_src))
return thumb_src
def show_popup(self, type, msg):
"""
:param type: popup title as string.
:param msg: content of the popup as string.
"""
popup = Popup(title=type, size_hint=(0.5, 0.2))
popup.content = Label(text=msg, id='label')
popup.open()
def search_flickr(self, term):
"""make a search request on flickr and return results
:param term: the term to search.
:return results: response in json
"""
api_key = '624d3a7086d14e85f1422430f0b889a1'
base_url = 'https://api.flickr.com/services/rest/?'
method = 'flickr.photos.search'
params = urllib.urlencode([('method', method),
('text', term),
('api_key', api_key),
('format', 'json'),
('nojsoncallback', 1),
('per_page', 20),
('content_type', 4)
])
url = ''.join((base_url, params))
result = self.request(url)
return result
def request(self, url):
url = urllib.parse.quote(url, safe="%/:=&?~#+!$,;'@()*[]")
req = UrlRequest(url, debug=True)
req.wait()
return req.result
def download_img(self, url, slide, wait=False):
pb = ProgressBar(id='_pb')
self.progress_dialog = Popup(title='Downloading...',
size_hint=(0.5, 0.2), content=pb, auto_dismiss=False)
url = urllib.parse.quote(url, safe="%/:=&?~#+!$,;'@()*[]")
path = os.path.join(self.tempdir,
os.path.basename(urllib.parse.urlparse(url).path))
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0"}
req = UrlRequest(url, self.reload_slide, file_path=path, on_progress=self.show_download_progress,
req_headers=headers, debug=True)
self.progress_dialog.open()
def show_download_progress(self, req, down_size, end_size):
self.progress_dialog.content.max = end_size
self.progress_dialog.content.value = down_size
if down_size == end_size:
self.progress_dialog.dismiss()
def reload_slide(self, request, data):
Logger.debug('Application: Download finished, reloading slide source.')
slide_path = urllib.parse.urlparse(request.url)
filename = os.path.basename(slide_path[2])
slides = [slide for slide in self.root.get_screen('editor').slides_view.grid_layout.children]
for slide in slides:
img_src = urllib.parse.urlparse(slide.img.img_src)
if img_src[0] == 'http' and img_src[2].endswith(filename):
Logger.debug('Application: Found slide ! Updating image source...')
slide.img.img_src = os.path.join(self.tempdir, filename)
Logger.debug('Application: {src}'.format(src=slide.img.img_src))
slide.img.thumb_src = self.create_thumbnail(slide.img.img_src)
slide.img.update_texture_size()
self.update_presentation('update', 0, 0)
self.root.current = 'editor'
def get_flickr_url(self, photo, size):
"""construct a source url to a flickr photo.
:param photo: photo information in json format.
:param size: desired size of the photo :
s small square 75x75
q large square 150x150
t thumbnail, 100 on longest side
m small, 240 on longest side
n small, 320 on longest side
- medium, 500 on longest side
z medium 640, 640 on longest side
c medium 800, 800 on longest side†
b large, 1024 on longest side*
o original image, either a jpg, gif or png, depending on source format
:return url:
"""
url = 'https://farm{farm_id}.staticflickr.com/{server_id}/{id}_{secret}_{size}.jpg'
url = url.format(farm_id=photo['farm'], server_id=photo['server'], id=photo['id'],
secret=photo['secret'], size=size)
return url
def search_term(self, term, engine, page):
if engine == "www.metmuseum.org":
offset = (page - 1) * int(self.config.get('search', 'search_rpp'))
params = urllib.parse.urlencode(
{self.engines[engine]['params']['term']: term,
self.engines[engine]['params']['rpp']: self.config.get(
'search', 'search_rpp'),
self.engines[engine]['params']['offset']: offset})
else:
params = urllib.parse.urlencode(
{self.engines[engine]['params']['term']: term,
self.engines[engine]['params']['rpp']: self.config.get(
'search', 'search_rpp'),
self.engines[engine]['params']['page']: page})
url = ''.join((self.engines[engine]['base_url'], params))
UrlRequest(url, on_success=self.parse_results, debug=True)
def parse_results(self, request, data):
search_screen = self.root.get_screen('search')
results = []
if self.engines[urllib.parse.urlparse(request.url).hostname]['results']['format'] == 'html':
tree = html.fromstring(data)
try:
total_results = re.sub("[^0-9]", "", tree.xpath(self.engines[urllib.parse.urlparse(request.url).hostname]['results']['total_results'])[0])
if not total_results or int(total_results) == 0:
raise ValueError
except ValueError:
self.show_popup(_('Error'), _('No results found.'))
return
if isinstance(tree.xpath(self.engines[urllib.parse.urlparse(request.url).hostname]['results']['total_pages']), list):
search_screen.box.total_pages = tree.xpath(self.engines[urllib.parse.urlparse(request.url).hostname]['results']['total_pages'])[0]
else:
search_screen.box.total_pages = tree.xpath(self.engines[urllib.parse.urlparse(request.url).hostname]['results']['total_pages'])
for entry in tree.xpath(self.engines[urllib.parse.urlparse(request.url).hostname]['results']['entries']):
artist = entry.xpath(self.engines[urllib.parse.urlparse(request.url).hostname]['results']['artist'])[0]
title = entry.xpath(self.engines[urllib.parse.urlparse(request.url).hostname]['results']['title'])[0]
date = entry.xpath(self.engines[urllib.parse.urlparse(request.url).hostname]['results']['date'])[0]
thumb = entry.xpath(self.engines[urllib.parse.urlparse(request.url).hostname]['results']['thumb'])[0]
obj_link = entry.xpath(self.engines[urllib.parse.urlparse(request.url).hostname]['results']['obj_link'])[0]
results.append({'title': title,
'artist': artist,
'year': date,
'thumb': urllib.parse.quote(thumb, safe="%/:=&?~#+!$,;'@()*[]"),
'obj_link': obj_link}
)
elif self.engines[urllib.parse.urlparse(request.url).hostname]['results']['format'] == 'json':
from ast import literal_eval
from functools import reduce
try:
total_results = reduce(dict.__getitem__, literal_eval(self.engines[urllib.parse.urlparse(request.url).hostname]['results']['total_results']), data)
if int(total_results) == 0:
raise ValueError
except ValueError:
self.show_popup(_('Error'), _('No results found.'))
return
entries = reduce(dict.__getitem__, literal_eval(self.engines[urllib.parse.urlparse(request.url).hostname]['results']['entries']), data)
search_screen.box.total_pages = int(total_results / len(entries))
for entry in entries:
artist = reduce(dict.__getitem__, literal_eval(self.engines[urllib.parse.urlparse(request.url).hostname]['results']['artist']), entry)
title = reduce(dict.__getitem__, literal_eval(self.engines[urllib.parse.urlparse(request.url).hostname]['results']['title']), entry)
date = reduce(dict.__getitem__, literal_eval(self.engines[urllib.parse.urlparse(request.url).hostname]['results']['date']), entry)
thumb = reduce(dict.__getitem__, literal_eval(self.engines[urllib.parse.urlparse(request.url).hostname]['results']['thumb']), entry)
obj_link = reduce(dict.__getitem__, literal_eval(self.engines[urllib.parse.urlparse(request.url).hostname]['results']['obj_link']), entry)
results.append({'title': title,
'artist': artist,
'year': date,
'thumb': urllib.parse.quote(thumb, safe="%/:=&?~#+!$,;'@()*[]"),
'obj_link': obj_link}
)
for photo in results:
Logger.debug('Search : Loading {url}'.format(url=photo['thumb']))
image = ItemButton(photo=photo, source=photo['thumb'], keep_ratio=True)
search_screen.box.grid.add_widget(image)
search_screen.box.status = "Page {page} on {total_page}".format(page=search_screen.box.current_page,
total_page=search_screen.box.total_pages)
def on_stop(self):
try:
shutil.rmtree(self.tempdir, ignore_errors=True)
except:
Logger.exception('Application: Removing temp dir failed.')
# # TODO: Check changes and ask user to save.
pass
class Manager(ScreenManager):
def __init__(self, **kwargs):
super(Manager, self).__init__(**kwargs)
Window.bind(on_key_down=self._on_keyboard_down)
def _on_keyboard_down(self, instance, key, scancode, codepoint, modifier, **kwargs):
if key == 275 and self.current == 'viewer':
self.get_screen('viewer').carousel.load_next(mode='next')
elif key == 276 and self.current == 'viewer':
self.get_screen('viewer').carousel.load_previous()
elif key == 108 and modifier == ['ctrl'] and self.current == 'viewer':
self.get_screen('viewer').carousel.current_slide.viewer.lock()
elif key == 100 and modifier == ['ctrl'] and self.current == 'viewer':
self.get_screen('viewer').carousel.current_slide.viewer.painter.canvas.clear()
elif key == 257 and self.current == 'viewer':
current_slide = self.get_screen('viewer').carousel.current_slide
current_slide.viewer.painter.current_tool = 'arrow'
elif key == 258 and self.current == 'viewer':
current_slide = self.get_screen('viewer').carousel.current_slide
current_slide.viewer.painter.current_tool = 'line'
elif key == 259 and self.current == 'viewer':
current_slide = self.get_screen('viewer').carousel.current_slide
current_slide.viewer.painter.current_tool = 'freeline'
elif key == 269 and self.current == 'viewer':
current_slide = self.get_screen('viewer').carousel.current_slide
painter = current_slide.viewer.painter
painter.thickness -= 0.5
elif key == 270 and self.current == 'viewer':
current_slide = self.get_screen('viewer').carousel.current_slide
painter = current_slide.viewer.painter
painter.thickness += 0.5