浏览代码

Initial pygal implementation

master
Dirk Alders 4 年前
父节点
当前提交
d73c7363b7
共有 100 个文件被更改,包括 137412 次插入0 次删除
  1. 237
    0
      __init__.py
  2. 111
    0
      admin.py
  3. 8
    0
      apps.py
  4. 135
    0
      context.py
  5. 65
    0
      forms.py
  6. 150
    0
      help.py
  7. 二进制
      locale/de/LC_MESSAGES/django.mo
  8. 257
    0
      locale/de/LC_MESSAGES/django.po
  9. 0
    0
      management/__init__.py
  10. 4
    0
      management/commands/_private.py
  11. 82
    0
      management/commands/convert_old_userdata.py
  12. 28
    0
      management/commands/delete_lost_items.py
  13. 37
    0
      management/commands/export_userdata.py
  14. 98
    0
      management/commands/import_userdata.py
  15. 25
    0
      management/commands/rebuild_cache.py
  16. 9
    0
      management/commands/rebuild_index.py
  17. 84
    0
      migrations/0001_initial.py
  18. 18
    0
      migrations/0002_setting_suspend_puplic.py
  19. 0
    0
      migrations/__init__.py
  20. 642
    0
      models.py
  21. 20
    0
      queries.py
  22. 125
    0
      search.py
  23. 37
    0
      signals.py
  24. 41
    0
      static/pygal/js/imgareaselect-js/css/imgareaselect-animated.css
  25. 41
    0
      static/pygal/js/imgareaselect-js/css/imgareaselect-default.css
  26. 36
    0
      static/pygal/js/imgareaselect-js/css/imgareaselect-deprecated.css
  27. 730
    0
      static/pygal/js/imgareaselect-js/jquery.imgareaselect.js
  28. 1
    0
      static/pygal/js/imgareaselect-js/jquery.imgareaselect.min.js
  29. 1
    0
      static/pygal/js/imgareaselect-js/jquery.imgareaselect.pack.js
  30. 4
    0
      static/pygal/js/jquery.min.js
  31. 108
    0
      static/pygal/js/video-js/font/VideoJS.svg
  32. 二进制
      static/pygal/js/video-js/font/VideoJS.ttf
  33. 二进制
      static/pygal/js/video-js/font/VideoJS.woff
  34. 34
    0
      static/pygal/js/video-js/lang/ar.js
  35. 26
    0
      static/pygal/js/video-js/lang/ba.js
  36. 26
    0
      static/pygal/js/video-js/lang/bg.js
  37. 26
    0
      static/pygal/js/video-js/lang/ca.js
  38. 26
    0
      static/pygal/js/video-js/lang/cs.js
  39. 26
    0
      static/pygal/js/video-js/lang/da.js
  40. 84
    0
      static/pygal/js/video-js/lang/de.js
  41. 40
    0
      static/pygal/js/video-js/lang/el.js
  42. 85
    0
      static/pygal/js/video-js/lang/en.js
  43. 27
    0
      static/pygal/js/video-js/lang/es.js
  44. 84
    0
      static/pygal/js/video-js/lang/fa.js
  45. 26
    0
      static/pygal/js/video-js/lang/fi.js
  46. 84
    0
      static/pygal/js/video-js/lang/fr.js
  47. 27
    0
      static/pygal/js/video-js/lang/gl.js
  48. 84
    0
      static/pygal/js/video-js/lang/he.js
  49. 26
    0
      static/pygal/js/video-js/lang/hr.js
  50. 26
    0
      static/pygal/js/video-js/lang/hu.js
  51. 26
    0
      static/pygal/js/video-js/lang/it.js
  52. 26
    0
      static/pygal/js/video-js/lang/ja.js
  53. 26
    0
      static/pygal/js/video-js/lang/ko.js
  54. 26
    0
      static/pygal/js/video-js/lang/nb.js
  55. 84
    0
      static/pygal/js/video-js/lang/nl.js
  56. 26
    0
      static/pygal/js/video-js/lang/nn.js
  57. 34
    0
      static/pygal/js/video-js/lang/pl.js
  58. 85
    0
      static/pygal/js/video-js/lang/pt-BR.js
  59. 41
    0
      static/pygal/js/video-js/lang/pt-PT.js
  60. 84
    0
      static/pygal/js/video-js/lang/ru.js
  61. 84
    0
      static/pygal/js/video-js/lang/sk.js
  62. 26
    0
      static/pygal/js/video-js/lang/sr.js
  63. 26
    0
      static/pygal/js/video-js/lang/sv.js
  64. 76
    0
      static/pygal/js/video-js/lang/tr.js
  65. 40
    0
      static/pygal/js/video-js/lang/uk.js
  66. 84
    0
      static/pygal/js/video-js/lang/vi.js
  67. 83
    0
      static/pygal/js/video-js/lang/zh-CN.js
  68. 83
    0
      static/pygal/js/video-js/lang/zh-TW.js
  69. 1279
    0
      static/pygal/js/video-js/video-js.css
  70. 1
    0
      static/pygal/js/video-js/video-js.min.css
  71. 41033
    0
      static/pygal/js/video-js/video.cjs.js
  72. 41031
    0
      static/pygal/js/video-js/video.es.js
  73. 48091
    0
      static/pygal/js/video-js/video.js
  74. 12
    0
      static/pygal/js/video-js/video.min.js
  75. 29
    0
      templates/pygal/audio.html
  76. 8
    0
      templates/pygal/empty.html
  77. 9
    0
      templates/pygal/help.html
  78. 42
    0
      templates/pygal/image.html
  79. 40
    0
      templates/pygal/infoview.html
  80. 37
    0
      templates/pygal/other.html
  81. 20
    0
      templates/pygal/overview.html
  82. 26
    0
      templates/pygal/profile.html
  83. 5
    0
      templates/pygal/repeat_head.html
  84. 58
    0
      templates/pygal/tagedit.html
  85. 10
    0
      templates/pygal/thumb.html
  86. 59
    0
      templates/pygal/video.html
  87. 0
    0
      templatetags/__init__.py
  88. 29
    0
      templatetags/tag_help.py
  89. 3
    0
      tests.py
  90. 26
    0
      urls.py
  91. 306
    0
      views/__init__.py
  92. 二进制
      views/forbidden.png
  93. 284
    0
      views/image.py
  94. 123
    0
      views/infoviews.py
  95. 二进制
      views/mimetype_icons/application-epub+zip.png
  96. 二进制
      views/mimetype_icons/application-gzip.png
  97. 二进制
      views/mimetype_icons/application-illustrator.png
  98. 二进制
      views/mimetype_icons/application-java-archive.png
  99. 二进制
      views/mimetype_icons/application-javascript.png
  100. 0
    0
      views/mimetype_icons/application-msword-template.png

+ 237
- 0
__init__.py 查看文件

@@ -0,0 +1,237 @@
1
+import binascii
2
+from django.conf import settings
3
+from django.urls.base import reverse
4
+import os
5
+
6
+SHUFFLE_DISABLE = 0
7
+SHUFFLE_KEEP = 1
8
+SHUFFLE_ENABLE = 2
9
+
10
+REPEAT_DISABLE = 0
11
+REPEAT_ENABLE = 1
12
+
13
+SEARCH_DISABLE = 0
14
+SEARCH_KEEP = 1
15
+
16
+RESP_TYPE_RAWITEM = 'raw'
17
+RESP_TYPE_WEBNAIL = 'webnail'
18
+RESP_TYPE_THUMBNAIL = 'thumbnail'
19
+RESP_TYPE_USERVIEW = 'userview'
20
+RESP_TYPE_INFOVIEW = 'infoview'
21
+RESP_TYPE_DOWNLOAD = 'download'
22
+RESP_TYPE_SEARCH = 'search'
23
+
24
+DATA_TYPE_ITEM = 'item'
25
+DATA_TYPE_SEARCH = 'search'
26
+
27
+
28
+def __get_settings__():
29
+    from .models import Setting
30
+    try:
31
+        s = Setting.objects.get(id=1)
32
+    except Setting.DoesNotExist:
33
+        s = Setting(id=1)
34
+        s.save()
35
+    return s
36
+
37
+
38
+def suspend_public():
39
+    return __get_settings__().suspend_puplic
40
+
41
+
42
+def show_image():
43
+    return __get_settings__().show_image
44
+
45
+
46
+def show_video():
47
+    return __get_settings__().show_video
48
+
49
+
50
+def show_audio():
51
+    return __get_settings__().show_audio
52
+
53
+
54
+def show_other():
55
+    return __get_settings__().show_other
56
+
57
+
58
+def dict2args(d):
59
+    if len(d) == 0:
60
+        return ''
61
+    al = []
62
+    for key in d:
63
+        if d[key] is None:
64
+            al.append('%s' % key)
65
+        else:
66
+            al.append('%s=%s' % (key, d[key]))
67
+    return '?' + '&'.join(al)
68
+
69
+
70
+def pygal_responses_kwargs(responsetype, datatype, rel_path):
71
+    kwargs = {'responsetype': responsetype, 'datatype': datatype}
72
+    if rel_path:
73
+        kwargs['rel_path'] = rel_path
74
+    return kwargs
75
+
76
+
77
+def url_args(request, repeat=REPEAT_DISABLE, shuffle=SHUFFLE_KEEP, search=SEARCH_DISABLE, flat=False):
78
+    args_d = {}
79
+    if repeat == REPEAT_ENABLE:
80
+        args_d['repeat'] = None
81
+    if shuffle == SHUFFLE_ENABLE:
82
+        args_d['shuffle'] = binascii.hexlify(os.urandom(24)).decode('utf-8')
83
+    elif shuffle == SHUFFLE_KEEP:
84
+        shuffle_id = get_shuffle_id(request)
85
+        if shuffle_id is not None:
86
+            args_d['shuffle'] = shuffle_id
87
+    if search == SEARCH_KEEP:
88
+        search_query = get_search_query(request)
89
+        if search_query is not None:
90
+            args_d['q'] = search_query
91
+    elif type(search) == str:
92
+        args_d['q'] = search
93
+    if flat:
94
+        args_d['flat'] = None
95
+    return args_d
96
+
97
+
98
+def get_full_path(rel_path):
99
+    return os.path.join(settings.ITEM_ROOT, rel_path.replace('/', os.path.sep))
100
+
101
+
102
+def get_rel_path(full_path):
103
+    if full_path.startswith(settings.ITEM_ROOT):
104
+        return full_path[len(settings.ITEM_ROOT) + 1:].replace(os.path.sep, '/')
105
+
106
+
107
+def set_thumbnail_size(request, value):
108
+    request.session[settings.SESSION_KEY_THUMBNAIL_SIZE] = value
109
+
110
+
111
+def get_thumbnail_size(request):
112
+    return request.session.get(settings.SESSION_KEY_THUMBNAIL_SIZE, max(settings.THUMBNAIL_SIZES))
113
+
114
+
115
+def get_thumbnail_max_size(request):
116
+    return max(settings.THUMBNAIL_SIZES)
117
+
118
+
119
+def set_webnail_size(request, value):
120
+    request.session[settings.SESSION_KEY_WEBNAIL_SIZE] = value
121
+
122
+
123
+def get_webnail_size(request):
124
+    return request.session.get(settings.SESSION_KEY_WEBNAIL_SIZE, max(settings.WEBNAIL_SIZES))
125
+
126
+
127
+def get_datatype(request):
128
+    try:
129
+        return request.META['PATH_INFO'].split('/')[3]
130
+    except IndexError:
131
+        return None
132
+
133
+
134
+def get_responsetype(request):
135
+    try:
136
+        return request.META['PATH_INFO'].split('/')[2]
137
+    except IndexError:
138
+        return None
139
+
140
+
141
+def url_download(request, rel_path, flat=False):
142
+    return reverse('pygal-responses', kwargs=pygal_responses_kwargs(RESP_TYPE_DOWNLOAD, get_datatype(request), rel_path)) + dict2args(url_args(request, flat=flat, search=SEARCH_KEEP))
143
+
144
+
145
+def url_helpview(request, page):
146
+    return reverse('pygal-helpview', kwargs={'page': page})
147
+
148
+
149
+def url_infoview(request, rel_path, repeat=REPEAT_DISABLE, shuffle=SHUFFLE_KEEP, search=SEARCH_DISABLE):
150
+    return reverse('pygal-responses', kwargs=pygal_responses_kwargs(RESP_TYPE_INFOVIEW, get_datatype(request), rel_path)) + dict2args(url_args(request, repeat=repeat, shuffle=shuffle, search=search))
151
+
152
+
153
+def url_item(request, rel_path):
154
+    return reverse('pygal-responses', kwargs=pygal_responses_kwargs(RESP_TYPE_RAWITEM, DATA_TYPE_ITEM, rel_path))
155
+
156
+
157
+def url_profile(request):
158
+    nxt = request.GET.get('next', request.get_full_path())
159
+    return reverse('pygal-profile') + '?next=%s' % nxt
160
+
161
+
162
+def url_repeatview(request, rel_path):
163
+    return url_userview(request, rel_path, repeat=REPEAT_ENABLE, search=SEARCH_KEEP) + '#openModal'
164
+
165
+
166
+def url_search(request):
167
+    return reverse('pygal-responses', kwargs={'responsetype': RESP_TYPE_USERVIEW, 'datatype': DATA_TYPE_SEARCH})
168
+
169
+
170
+def url_thumbnail(request, rel_path):
171
+    return reverse('pygal-responses', kwargs=pygal_responses_kwargs(RESP_TYPE_THUMBNAIL, DATA_TYPE_ITEM, rel_path))
172
+
173
+
174
+def url_userview(request, rel_path, repeat=REPEAT_DISABLE, shuffle=SHUFFLE_KEEP, search=SEARCH_DISABLE):
175
+    if shuffle in [SHUFFLE_DISABLE, SHUFFLE_ENABLE]:
176
+        return request.META.get('PATH_INFO') + dict2args(url_args(request, repeat=repeat, shuffle=shuffle, search=search))
177
+    return reverse('pygal-responses', kwargs=pygal_responses_kwargs(RESP_TYPE_USERVIEW, get_datatype(request), rel_path)) + dict2args(url_args(request, repeat=repeat, shuffle=shuffle, search=search))
178
+
179
+
180
+def url_webnail(request, rel_path):
181
+    return reverse('pygal-responses', kwargs=pygal_responses_kwargs(RESP_TYPE_WEBNAIL, DATA_TYPE_ITEM, rel_path))
182
+
183
+
184
+def url_favouriteview(request):
185
+    return url_search(request) + dict2args(url_args(request, repeat=REPEAT_DISABLE, shuffle=SHUFFLE_DISABLE, search='favourite_of:%s' % request.user.username))
186
+
187
+
188
+def url_favourite_set(request, rel_path):
189
+    nxt = request.GET.get('next', request.get_full_path())
190
+    return reverse('pygal-setfavourite', args=(1, rel_path, )) + '?next=%s' % nxt
191
+
192
+
193
+def url_favourite_unset(request, rel_path):
194
+    nxt = request.GET.get('next', request.get_full_path())
195
+    return reverse('pygal-setfavourite', args=(0, rel_path, )) + '?next=%s' % nxt
196
+
197
+
198
+def url_addtag(request, rel_path):
199
+    nxt = request.GET.get('next', request.get_full_path())
200
+    return reverse('pygal-addtag', args=(rel_path, )) + '?next=%s' % nxt
201
+
202
+
203
+def url_tagedit(request, tag_id):
204
+    nxt = request.GET.get('next', request.get_full_path())
205
+    return reverse('pygal-edittag', args=(tag_id, )) + '?next=%s' % nxt
206
+
207
+
208
+def is_helpview(request):
209
+    return request.META['PATH_INFO'].startswith(reverse('pygal-helpview', kwargs={'page': '_'})[:-1])
210
+
211
+
212
+def is_infoview(request):
213
+    return get_responsetype(request) == RESP_TYPE_INFOVIEW
214
+
215
+
216
+def is_repeatview(request):
217
+    return 'repeat' in request.GET
218
+
219
+
220
+def is_favouriteview(request):
221
+    return get_datatype(request) == DATA_TYPE_SEARCH and get_search_query(request) == 'favourite_of:%s' % request.user.username
222
+
223
+
224
+def is_searchview(request):
225
+    return not is_favouriteview(request) and get_datatype(request) == DATA_TYPE_SEARCH
226
+
227
+
228
+def is_flat(request):
229
+    return 'flat' in request.GET
230
+
231
+
232
+def get_shuffle_id(request):
233
+    return request.GET.get('shuffle')
234
+
235
+
236
+def get_search_query(request):
237
+    return request.GET.get('q')

+ 111
- 0
admin.py 查看文件

@@ -0,0 +1,111 @@
1
+from django.contrib import admin
2
+from .models import Item, Tag, Setting, TYPE_AUDIO, TYPE_FOLDER, TYPE_IMAGE, TYPE_VIDEO
3
+
4
+
5
+class ItemAdmin(admin.ModelAdmin):
6
+    list_display = ('rel_path', 'type', )
7
+    search_fields = ('rel_path', )
8
+    readonly_fields = (
9
+        'rel_path',
10
+        'type',
11
+        'artist_c',
12
+        'album_c',
13
+        'year_c',
14
+        'title_c',
15
+        'track_c',
16
+        'duration_c',
17
+        'genre_c',
18
+        'bitrate_c',
19
+        'num_audio_c',
20
+        'num_folders_c',
21
+        'num_images_c',
22
+        'num_other_c',
23
+        'num_videos_c',
24
+        'sil_c',
25
+        'camera_vendor_c',
26
+        'camera_model_c',
27
+        'width_c',
28
+        'height_c',
29
+        'exposure_program_c',
30
+        'exposure_time_c',
31
+        'iso_c',
32
+        'f_number_c',
33
+        'focal_length_c',
34
+        'flash_c',
35
+        'lon_c',
36
+        'lat_c',
37
+        'orientation_c',
38
+        'ratio_c'
39
+    )
40
+
41
+    list_filter = (
42
+        ('type', admin.ChoicesFieldListFilter),
43
+        ('public_access', admin.BooleanFieldListFilter),
44
+        ('read_access', admin.RelatedFieldListFilter),
45
+        ('modify_access', admin.RelatedFieldListFilter),
46
+    )
47
+
48
+    def get_fields(self, request, obj=None):
49
+        if obj is not None:
50
+            rv = ['rel_path', 'type', 'datetime_c']
51
+            if obj.type == TYPE_FOLDER:
52
+                rv += ['public_access', 'read_access', 'modify_access']
53
+            else:
54
+                rv += ['favourite_of']
55
+            rv += ['size_c', 'uid_c', 'settings_c', 'data_version_c']
56
+            if obj.type == TYPE_AUDIO:
57
+                rv += [
58
+                    'artist_c',
59
+                    'album_c',
60
+                    'year_c',
61
+                    'title_c',
62
+                    'track_c',
63
+                    'duration_c',
64
+                    'genre_c',
65
+                    'bitrate_c',
66
+                ]
67
+            if obj.type == TYPE_FOLDER:
68
+                rv += [
69
+                    'num_folders_c',
70
+                    'num_audio_c',
71
+                    'num_images_c',
72
+                    'num_other_c',
73
+                    'num_videos_c',
74
+                    'sil_c',
75
+                ]
76
+            if obj.type == TYPE_IMAGE:
77
+                rv += [
78
+                    'camera_vendor_c',
79
+                    'camera_model_c',
80
+                    'width_c',
81
+                    'height_c',
82
+                    'lon_c',
83
+                    'lat_c',
84
+                    'exposure_program_c',
85
+                    'exposure_time_c',
86
+                    'iso_c',
87
+                    'focal_length_c',
88
+                    'f_number_c',
89
+                    'flash_c',
90
+                    'orientation_c',
91
+                ]
92
+            if obj.type == TYPE_VIDEO:
93
+                rv += [
94
+                    'duration_c',
95
+                    'width_c',
96
+                    'height_c',
97
+                    'ratio_c'
98
+                ]
99
+            return rv
100
+        else:
101
+            return admin.ModelAdmin.get_fields(self, request, obj=obj)
102
+
103
+
104
+class TagAdmin(admin.ModelAdmin):
105
+    list_display = ('item', 'text', )
106
+    search_fields = ('item__rel_path', 'text', )
107
+
108
+
109
+admin.site.register(Item, ItemAdmin)
110
+admin.site.register(Tag, TagAdmin)
111
+admin.site.register(Setting)

+ 8
- 0
apps.py 查看文件

@@ -0,0 +1,8 @@
1
+from django.apps import AppConfig
2
+
3
+
4
+class PygalConfig(AppConfig):
5
+    name = 'pygal'
6
+
7
+    def ready(self):
8
+        import pygal.signals

+ 135
- 0
context.py 查看文件

@@ -0,0 +1,135 @@
1
+from django.utils.translation import gettext as _
2
+from .help import actionbar as actionbar_add_help
3
+import os
4
+from themes import gray_icon_url, color_icon_url
5
+from users.context import menubar as menubar_user
6
+from users.context import PROFILE_ENTRY_UID
7
+import pygal
8
+from pygal.models import get_item_by_rel_path
9
+
10
+MY_FAVOURITES_ENTRY_UID = 'my_favourites-main'
11
+HELP_UID = 'help-main'
12
+NAVIGATION_ENTRY_UID = 'navigation-main-%s'
13
+HOME_ENTRY_UID = 'home-main'
14
+SEARCH_ENTRY_UID = 'search-main'
15
+BACK_ENTRY_UID = 'back-main'
16
+
17
+
18
+def context_adaption(context, request, rel_path, wrapper_instance=None, title='', current_help_page=None):
19
+    context[context.MENUBAR].append_entry(HELP_UID, _('Help'), color_icon_url(request, 'help.png'), pygal.url_helpview(request, 'main'), True, False)
20
+    menubar_user(context[context.MENUBAR], request)
21
+    menubar(context[context.MENUBAR], request, rel_path)
22
+    navigationbar(context[context.NAVIGATIONBAR], request, rel_path)
23
+    if wrapper_instance is None:
24
+        # Pages without direct connection to an item (e.g. Helpview, ...)
25
+        context.set_additional_title(title)
26
+        context[context.ACTIONBAR].append_entry(
27
+            BACK_ENTRY_UID,                             # uid
28
+            _('Back'),                                  # name
29
+            color_icon_url(request, 'back.png'),        # icon
30
+            'javascript:history.back()',                # url
31
+            True,                                       # left
32
+            False                                       # active
33
+        )
34
+    else:
35
+        wrapper_instance.context_adaption(context)
36
+    # HELP
37
+    if pygal.is_helpview(request):
38
+        actionbar_add_help(context, request, current_help_page=current_help_page)
39
+
40
+
41
+def menubar(bar, request, rel_path):
42
+    if request.user.is_authenticated:
43
+        bar.append_entry(*my_favourites_entry_parameters(request, rel_path))
44
+    try:
45
+        bar.replace_entry(PROFILE_ENTRY_UID, *profile_entry_parameters(request))
46
+    except ValueError:
47
+        pass        # Profile entry does not exist, so exchange is not needed (e.g. no user is logged in)
48
+
49
+
50
+def navigationbar(bar, request, path):
51
+    if pygal.is_favouriteview(request):
52
+        if path:
53
+            bar.append_entry(*navigation_entry_parameters(request, path, None))
54
+            anchor = get_item_by_rel_path(path).name
55
+        else:
56
+            anchor = None
57
+    elif pygal.is_searchview(request):
58
+        if path:
59
+            bar.append_entry(*navigation_entry_parameters(request, path, None))
60
+            anchor = get_item_by_rel_path(path).name
61
+        else:
62
+            anchor = None
63
+    else:
64
+        anchor = None
65
+        while len(path) > 0 and path != os.path.sep:
66
+            bar.append_entry(*navigation_entry_parameters(request, path, anchor))
67
+            anchor = get_item_by_rel_path(path).name
68
+            path = os.path.dirname(path)
69
+    bar.append_entry(*home_entry_parameters(request, anchor))
70
+
71
+
72
+def profile_entry_parameters(request):
73
+    return (
74
+        PROFILE_ENTRY_UID,                          # uid
75
+        request.user.username,                      # name
76
+        color_icon_url(request, 'user.png'),        # icon
77
+        pygal.url_profile(request),                 # url
78
+        False,                                      # left
79
+        False                                       # active
80
+    )
81
+
82
+
83
+def home_entry_parameters(request, anchor=None):
84
+    if pygal.is_favouriteview(request):
85
+        icon = 'favourite.png'
86
+    elif pygal.is_searchview(request):
87
+        icon = 'search.png'
88
+    else:
89
+        icon = 'home.png'
90
+    return (
91
+        HOME_ENTRY_UID,
92
+        ':',
93
+        gray_icon_url(request, icon),
94
+        pygal.url_userview(request, rel_path='', search=pygal.SEARCH_KEEP) + ('#%s' % anchor if anchor is not None else ''),
95
+        True,
96
+        False
97
+    )
98
+
99
+
100
+def search_entry_parameters(request):
101
+    return (
102
+        SEARCH_ENTRY_UID,
103
+        ':',
104
+        gray_icon_url(request, 'search.png'),
105
+        pygal.url_search(request),
106
+        True,
107
+        False
108
+    )
109
+
110
+
111
+def navigation_entry_parameters(request, path, anchor=None):
112
+    return (
113
+        NAVIGATION_ENTRY_UID % os.path.basename(path),                                                                  # uid
114
+        '/' + os.path.basename(path),                                                                                   # name
115
+        None,                                                                                                           # icon
116
+        pygal.url_userview(request, path, search=pygal.SEARCH_KEEP) + ('#%s' % anchor if anchor is not None else ''),   # url
117
+        False,                                                                                                          # left
118
+        False                                                                                                           # active
119
+    )
120
+
121
+
122
+def my_favourites_entry_parameters(request, rel_path):
123
+    if pygal.is_favouriteview(request) and rel_path != '':
124
+        url_addon = '#%s' % get_item_by_rel_path(rel_path).name
125
+    else:
126
+        url_addon = ''
127
+    return (
128
+        MY_FAVOURITES_ENTRY_UID,                        # uid
129
+        _('My Favourites'),                             # name
130
+        color_icon_url(request, 'favourite.png'),       # icon
131
+        pygal.url_favouriteview(request) + url_addon,   # url
132
+        True,                                           # left
133
+        pygal.is_favouriteview(request)                 # active
134
+    )
135
+

+ 65
- 0
forms.py 查看文件

@@ -0,0 +1,65 @@
1
+from django import forms
2
+from .models import Tag, is_valid_area
3
+
4
+
5
+class TagForm(forms.ModelForm):
6
+    COORD_KEYS = ['topleft_x', 'topleft_y', 'bottomright_x', 'bottomright_y']
7
+
8
+    class Meta:
9
+        model = Tag
10
+        fields = ['text', 'topleft_x', 'topleft_y', 'bottomright_x', 'bottomright_y']
11
+        widgets = {
12
+            'topleft_x': forms.HiddenInput(),
13
+            'topleft_y': forms.HiddenInput(),
14
+            'bottomright_x': forms.HiddenInput(),
15
+            'bottomright_y': forms.HiddenInput(),
16
+        }
17
+
18
+    def __init__(self, *args, **kwargs):
19
+        self.factor_to_original = kwargs.pop('factor_to_original')
20
+        super(TagForm, self).__init__(*args, **kwargs)
21
+
22
+    def __get_area_value__(self, key):
23
+        if len(self.data):
24
+            rv = self.data.get(key)
25
+        else:
26
+            rv = self.get_initial_for_field(self.fields[key], key)
27
+        try:
28
+            return int(rv)
29
+        except TypeError:
30
+            return None
31
+        except ValueError:
32
+            return None
33
+
34
+    @property
35
+    def value_tl_x(self):
36
+        return self.__get_area_value__('topleft_x')
37
+
38
+    @property
39
+    def value_tl_y(self):
40
+        return self.__get_area_value__('topleft_y')
41
+
42
+    @property
43
+    def value_br_x(self):
44
+        return self.__get_area_value__('bottomright_x')
45
+
46
+    @property
47
+    def value_br_y(self):
48
+        return self.__get_area_value__('bottomright_y')
49
+
50
+    @property
51
+    def has_valid_coordinates(self):
52
+        return is_valid_area(self.value_br_x, self.value_br_y, self.value_tl_x, self.value_tl_y)
53
+
54
+    def save(self, commit=True):
55
+        if self.has_valid_coordinates:
56
+            self.instance.topleft_x = self.value_tl_x
57
+            self.instance.topleft_y = self.value_tl_y
58
+            self.instance.bottomright_x = self.value_br_x
59
+            self.instance.bottomright_y = self.value_br_y
60
+        else:
61
+            self.instance.topleft_x = None
62
+            self.instance.topleft_y = None
63
+            self.instance.bottomright_x = None
64
+            self.instance.bottomright_y = None
65
+        return forms.ModelForm.save(self, commit=commit)

+ 150
- 0
help.py 查看文件

@@ -0,0 +1,150 @@
1
+from django.utils.translation import gettext as _
2
+import mycreole
3
+import pygal
4
+from themes import color_icon_url
5
+
6
+# TODO: Add field descriptions including the filed list choices, if field has a list of limited values (e.g. flash: "fired", ...)
7
+# TODO: Describe logic operator order and brackets if possible
8
+# TODO: Expend Examples for pictures without flash and high f_number to get potentially good quality images.
9
+
10
+HELP_UID = 'help'
11
+
12
+MAIN = mycreole.render_simple(_("""
13
+= PyGal
14
+
15
+**PyGal** is a File **Gal**ery visulisation tool implementes in **Py**thon.
16
+
17
+It is designed to visualise images and videos. Nevertheless it is also possible to \
18
+visualise audio files and all other files.
19
+
20
+
21
+== Help
22
+* [[faq|Frequently asked questions]]
23
+* [[access|Access Conrtrol]]
24
+* [[search|Help on Search]]
25
+
26
+"""))
27
+
28
+FAQ = mycreole.render_simple(_("""
29
+= Frequently asked questions
30
+==Repeat-Mode
31
+On Problems with the Repeat-Mode, please check your Browser \
32
+[[https://www.google.com/search?q=autoplay+enable+browser|properties]] \
33
+for autoplay restrictions!
34
+"""))
35
+
36
+ACCESS = mycreole.render_simple(_("""
37
+= Access control
38
+Access Control is defined on folder level. The rights given for a folder \
39
+are the rights for all items in that folder. The folder itself has no access \
40
+restrictions. The content of a folder is defined by the accessable items \
41
+recursive below that folder.
42
+
43
+**Example:** If a user has only read access to the folder "bar" below the folder "foo", \
44
+but not to "foo", he will see the folder "bar" inside the folder "foo" and all items in \
45
+"foo/bar", but not the items in the folder "foo".
46
+"""))
47
+
48
+SEARCH = mycreole.render_simple(_("""
49
+= Search
50
+The search looks up full words in //Itemnames (name)// and //Tags (tag)// without giving \
51
+special search commands in the search string.
52
+
53
+
54
+== Search-Fields
55
+The useful search fields depend on the item type. Therefore some fields are given twice or \
56
+more in the different type depending lists.
57
+
58
+=== General search fields
59
+* rel_path (TEXT):
60
+* name (TEXT):
61
+* type (TEXT):
62
+* favourite_of (KEYWORD):
63
+* datetime (DATETIME):
64
+* size (NUMERIC):
65
+* tag (KEYWORD):
66
+
67
+=== Image
68
+* exposure_program (TEXT):
69
+* exposure_time (NUMERIC):
70
+* flash (TEXT):
71
+* f_number (NUMERIC):
72
+* focal_length (NUMERIC):
73
+* lon (NUMERIC):
74
+* lat (NUMERIC):
75
+* height (NUMERIC):
76
+* iso (NUMERIC):
77
+* camera_vendor (TEXT):
78
+* camera_model (TEXT):
79
+* orientation (NUMERIC):
80
+* width (NUMERIC):
81
+
82
+=== Audio
83
+* album (TEXT):
84
+* artist (TEXT):
85
+* bitrate (NUMERIC):
86
+* duration (NUMERIC):
87
+* genre (TEXT):
88
+* title (TEXT):
89
+* track (NUMERIC):
90
+* year (NUMERIC):
91
+
92
+=== Video
93
+* ratio (NUMERIC):
94
+
95
+
96
+== Search syntax (Whoosh)
97
+=== Logic operators
98
+* AND
99
+** **Example:** "foo AND bar" - Search will find all items with foo and bar.
100
+* OR
101
+** **Example:** "foo OR bar" - Search will find all items with foo, bar or with foo and bar.
102
+* NOT
103
+** **Example:** "foo NOT bar" - Search will find all items with foo and no bar.
104
+=== Search in specific fields
105
+A search pattern like //foo:bar// does look for //bar// in the field named //foo//.
106
+
107
+This search pattern can also be combined with other search text via logical operators.
108
+=== Search for specific content
109
+* **Wildcards:**
110
+* **Range:**
111
+** From To:
112
+** Above:
113
+** Below:
114
+* **Named constants:**
115
+** //now//: Current date
116
+** //-[num]y//: Current date minus [num] years
117
+** ...
118
+
119
+== Examples
120
+* [[/pygal/userview/search?q=type:video AND datetime:2018|type:video AND datetime:2018]] gives results with all videos in year 2018.
121
+* [[/pygal/userview/search?q=datetime:[-2y to now]|datetime:[-2y to now]]] gives results with all item of the last two years.
122
+* [[/pygal/userview/search?q=rel_path:2018*|rel_path:2018*]] gives results with all item having 2018 in their path.
123
+* [[/pygal/userview/search?q=tag:Test|tag:Test]] gives results with all item having Test in their tags.
124
+* [[/pygal/userview/search?q=datetime:2016 AND favourite_of:*|datetime:2016 AND favourite_of:*]] gives results with all item having 2018 in their tags or path and are favourite of someone.
125
+"""))
126
+
127
+help_pages = {
128
+    'main': MAIN,
129
+    'faq': FAQ,
130
+    'access': ACCESS,
131
+    'search': SEARCH,
132
+}
133
+
134
+
135
+def actionbar(context, request, current_help_page=None, **kwargs):
136
+    actionbar_entries = (
137
+        ('1', 'Main'),
138
+        ('2', 'Faq'),
139
+        ('3', 'Access'),
140
+        ('4', 'Search'),
141
+    )
142
+    for num, name in actionbar_entries:
143
+        context[context.ACTIONBAR].append_entry(
144
+            HELP_UID + '-%s' % name.lower(),                        # uid
145
+            _(name),                                                # name
146
+            color_icon_url(request, num + '.png'),                  # icon
147
+            pygal.url_helpview(request, name.lower()),              # url
148
+            True,                                                   # left
149
+            name.lower() == current_help_page,                      # active
150
+        )

二进制
locale/de/LC_MESSAGES/django.mo 查看文件


+ 257
- 0
locale/de/LC_MESSAGES/django.po 查看文件

@@ -0,0 +1,257 @@
1
+# SOME DESCRIPTIVE TITLE.
2
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3
+# This file is distributed under the same license as the PACKAGE package.
4
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5
+#
6
+#, fuzzy
7
+msgid ""
8
+msgstr ""
9
+"Project-Id-Version: PACKAGE VERSION\n"
10
+"Report-Msgid-Bugs-To: \n"
11
+"POT-Creation-Date: 2019-09-22 14:42+0200\n"
12
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14
+"Language-Team: LANGUAGE <LL@li.org>\n"
15
+"Language: \n"
16
+"MIME-Version: 1.0\n"
17
+"Content-Type: text/plain; charset=UTF-8\n"
18
+"Content-Transfer-Encoding: 8bit\n"
19
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
20
+
21
+#: context.py:48 views/__init__.py:60 views/pygal_help.py:6
22
+msgid "Help"
23
+msgstr "Hilfe"
24
+
25
+#: templates/pygal/profile.html:8
26
+msgid "Xnail-Size"
27
+msgstr "Bildgrößen"
28
+
29
+#: templates/pygal/profile.html:9
30
+msgid "Thumbnail Size"
31
+msgstr "Bildgröße in der Übersicht"
32
+
33
+#: templates/pygal/profile.html:16
34
+msgid "Webnail Size"
35
+msgstr "Bildgröße in der Ansicht"
36
+
37
+#: templates/pygal/profile.html:23
38
+msgid "Save"
39
+msgstr "Speichern"
40
+
41
+#: views/__init__.py:23
42
+msgid "Access denied!"
43
+msgstr "Zugriff verweigert!"
44
+
45
+#: views/__init__.py:24 views/__init__.py:84 views/__init__.py:121
46
+#: views/__init__.py:143
47
+msgid ""
48
+"Access Denied: You either don't have access rights or the item does not "
49
+"exist."
50
+msgstr ""
51
+"Zugriff verweigert: Entweder haben Sie keine Leseberechtigung oder es "
52
+"exitieren keine Elemente."
53
+
54
+#: views/__init__.py:97
55
+#, python-format
56
+msgid "The Thumbnail Size was set to \"%d\""
57
+msgstr "Die Bildgröße in der Übersicht wurde zu \"%d\" gesetzt"
58
+
59
+#: views/__init__.py:103
60
+#, python-format
61
+msgid "The Webnail Size was set to \"%d\""
62
+msgstr "Die Bildgröße in der Ansicht wurde zu \"%d\" gesetzt"
63
+
64
+#: views/__init__.py:107
65
+#, python-format
66
+msgid "Profile for %(username)s"
67
+msgstr "Profil für %(username)s"
68
+
69
+#: views/infoviews.py:29
70
+msgid "General"
71
+msgstr "Allgemein"
72
+
73
+#: views/infoviews.py:30
74
+msgid "Information"
75
+msgstr "Informationen"
76
+
77
+#: views/infoviews.py:31
78
+msgid "Database"
79
+msgstr "Datenbasis"
80
+
81
+#: views/infoviews.py:60
82
+msgid "Number of Folder(s)"
83
+msgstr "Anzahl der Ordner"
84
+
85
+#: views/infoviews.py:61
86
+msgid "Number of Image(s)"
87
+msgstr "Anzahl der Bilder"
88
+
89
+#: views/infoviews.py:62
90
+msgid "Number of Video(s)"
91
+msgstr "Anzahl der Videos"
92
+
93
+#: views/infoviews.py:63
94
+msgid "Number of Audio File(s)"
95
+msgstr "Anzahl der Audio-Dateien"
96
+
97
+#: views/infoviews.py:64
98
+msgid "Number of Other File(s)"
99
+msgstr "Anzahl der anderen Dateien"
100
+
101
+#: views/infoviews.py:78 views/infoviews.py:86 views/infoviews.py:102
102
+#: views/userviews.py:70
103
+msgid "Creation Time"
104
+msgstr "Erstellungsdatum"
105
+
106
+#: views/infoviews.py:79 views/infoviews.py:103
107
+msgid "Resolution"
108
+msgstr "Auflösung"
109
+
110
+#: views/infoviews.py:80
111
+msgid "Camera"
112
+msgstr "Kamera"
113
+
114
+#: views/infoviews.py:84 views/userviews.py:302
115
+msgid "GPS"
116
+msgstr "GPS"
117
+
118
+#: views/infoviews.py:85
119
+msgid "Exposure Program"
120
+msgstr "Aufnahme-Programm"
121
+
122
+#: views/infoviews.py:87
123
+msgid "Focal Length"
124
+msgstr "Brennweite"
125
+
126
+#: views/infoviews.py:88
127
+msgid "Apeture"
128
+msgstr "Blende"
129
+
130
+#: views/infoviews.py:89
131
+msgid "ISO"
132
+msgstr "ISO"
133
+
134
+#: views/infoviews.py:90
135
+msgid "Flash"
136
+msgstr "Blitz"
137
+
138
+#: views/infoviews.py:91
139
+msgid "Orientation"
140
+msgstr "Ausrichtung"
141
+
142
+#: views/infoviews.py:104 views/infoviews.py:120 views/userviews.py:348
143
+msgid "Duration"
144
+msgstr "Dauer"
145
+
146
+#: views/infoviews.py:105
147
+msgid "Ratio"
148
+msgstr "Seitenverhältnis"
149
+
150
+#: views/infoviews.py:116
151
+msgid "Title"
152
+msgstr "Titel"
153
+
154
+#: views/infoviews.py:117 views/userviews.py:345
155
+msgid "Artist"
156
+msgstr "Interpret"
157
+
158
+#: views/infoviews.py:118 views/userviews.py:346
159
+msgid "Album"
160
+msgstr "Album"
161
+
162
+#: views/infoviews.py:119 views/userviews.py:347
163
+msgid "Genre"
164
+msgstr "Genre"
165
+
166
+#: views/infoviews.py:121 views/userviews.py:349
167
+msgid "Bitrate"
168
+msgstr "Bitrate"
169
+
170
+#: views/infoviews.py:122 views/userviews.py:350
171
+msgid "Year"
172
+msgstr "Jahr"
173
+
174
+#: views/infoviews.py:123 views/userviews.py:351
175
+msgid "Track"
176
+msgstr "Titelnummer"
177
+
178
+#: views/pygal_help.py:8
179
+msgid "Repeat-Mode"
180
+msgstr "Wiederholmodus"
181
+
182
+#: views/pygal_help.py:9
183
+msgid ""
184
+"On Problems with the Repeat-Mode, please check your Browser <a href="
185
+"\"https://www.google.com/search?q=autoplay+enable+browser\">properties</a> "
186
+"for autoplay restrictions!"
187
+msgstr ""
188
+"Bei Problemen mit dem Wiederholmodus, sollten Sie die <a href=\""
189
+"https://www.google.com/search?q=autoplay+enable+browser\"> Browsereinstellungen"
190
+"</a> prüfen."
191
+
192
+#: views/pygal_help.py:10
193
+msgid "Access Control"
194
+msgstr "Zugangsberechtigungen"
195
+
196
+#: views/pygal_help.py:11
197
+msgid ""
198
+"Access Control is defined on the folder level. The rights given for a folder "
199
+"are the rights for all items in that folder. The folder itself has no access "
200
+"restrictions. The content of a folder is defined by the accessable items "
201
+"recursive below that folder."
202
+msgstr ""
203
+"Die Zugriffsrechte werden auf Ordnerebene definiert. Die Rechte des Ordners "
204
+"sind die Rechte aller Inhalte dieses Ordners. Der Ordner selbst hat keine "
205
+"Zugriffsberechtigungen. Der sichtbare Inhalt des Ordners wird durch die "
206
+"darin befindlichen Elemente und die Inhalte der Unterordner definiert."
207
+
208
+#: views/pygal_help.py:12
209
+msgid ""
210
+"Example: If a user has only read access to \"data/subfolder\" but not to "
211
+"\"data\", he will see the folder \"subfolder\" in \"data\" and all items in "
212
+"\"data/subfolder\"."
213
+msgstr ""
214
+"Beispiel: Besitzt ein Benutzer nur Zugriffsrechte auf \"data/subfolder\", "
215
+"aber nicht zu \"data\", dann wird er den Ordners \"subfolder\" in \"data\" "
216
+"und alle Elemente in \"subfolder\" sehen, aber nicht die Elemente im Ordner "
217
+"\"data\"."
218
+
219
+#: views/userviews.py:69
220
+msgid "Name"
221
+msgstr "Name"
222
+
223
+#: views/userviews.py:71
224
+msgid "Path"
225
+msgstr "Pfad"
226
+
227
+#: views/userviews.py:72
228
+msgid "Size"
229
+msgstr "Größe"
230
+
231
+#: views/userviews.py:73
232
+msgid "UID"
233
+msgstr "UID"
234
+
235
+#: views/userviews.py:81 views/userviews.py:90
236
+msgid "Info"
237
+msgstr "Info"
238
+
239
+#: views/userviews.py:98
240
+msgid "Download"
241
+msgstr "Herunterladen"
242
+
243
+#: views/userviews.py:108 views/userviews.py:117
244
+msgid "Repeat"
245
+msgstr "Wiederholen"
246
+
247
+#: views/userviews.py:126 views/userviews.py:135
248
+msgid "Shuffle"
249
+msgstr "Durchmischen"
250
+
251
+#: views/userviews.py:255
252
+msgid "Download (Structured)"
253
+msgstr "Herunterladen (Strukturiert)"
254
+
255
+#: views/userviews.py:264
256
+msgid "Download (Flat)"
257
+msgstr "Herunterladen (Flach)"

+ 0
- 0
management/__init__.py 查看文件


+ 4
- 0
management/commands/_private.py 查看文件

@@ -0,0 +1,4 @@
1
+KEY_USERDATA_VERSION = 'version'
2
+KEY_USERDATA_FAVOURITE = 'favourite'
3
+KEY_USERDATA_TAGS = 'tag'
4
+KEY_USERDATA_UPLOAD = 'upload'

+ 82
- 0
management/commands/convert_old_userdata.py 查看文件

@@ -0,0 +1,82 @@
1
+from django.core.management.base import BaseCommand
2
+import fstools
3
+import json
4
+from pygal.models import is_valid_area, Item
5
+
6
+from ._private import KEY_USERDATA_VERSION
7
+from ._private import KEY_USERDATA_FAVOURITE
8
+from ._private import KEY_USERDATA_TAGS
9
+from ._private import KEY_USERDATA_UPLOAD
10
+
11
+KEY_REL_PATH = '_rel_path_'
12
+KEY_FAVOURITE = '_favourite_of_'
13
+KEY_TAG = '_tags_'
14
+KEY_UPLOAD = '_upload_'
15
+
16
+
17
+class Command(BaseCommand):
18
+    help = 'Convert userdata from old pygal to JSON-Format.'
19
+
20
+    def add_arguments(self, parser):
21
+        parser.add_argument('folder', nargs='+', type=str)
22
+
23
+    def handle(self, *args, **options):
24
+        data = {}
25
+        data[KEY_USERDATA_VERSION] = 1
26
+        path = options['folder'][0]
27
+        for f in fstools.filelist(path, '*.json'):
28
+            with open(f, 'r') as fh:
29
+                o = json.load(fh)
30
+            _rel_path_ = o.get(KEY_REL_PATH)
31
+            _favourite_of_ = o.get(KEY_FAVOURITE)
32
+            _tags_ = o.get(KEY_TAG)
33
+            _upload_ = o.get(KEY_UPLOAD)
34
+            if _rel_path_:
35
+                try:
36
+                    i = Item.objects.get(rel_path=_rel_path_)
37
+                except Item.DoesNotExist:
38
+                    self.stderr.write(self.style.ERROR('%s does not exist. No data taken over!' % _rel_path_))
39
+                else:
40
+                    if _favourite_of_:
41
+                        for username in _favourite_of_:
42
+                            if KEY_USERDATA_FAVOURITE not in data:
43
+                                data[KEY_USERDATA_FAVOURITE] = {}
44
+                            if username not in data[KEY_USERDATA_FAVOURITE]:
45
+                                data[KEY_USERDATA_FAVOURITE][username] = []
46
+                            data[KEY_USERDATA_FAVOURITE][username].append(_rel_path_)
47
+                    if len(_tags_):
48
+                        new_tags = []
49
+                        for t in _tags_:
50
+                            new_tag = {}
51
+                            new_tag['text'] = _tags_[t].get('tag')
52
+                            #
53
+                            x = _tags_[t].get('x')
54
+                            y = _tags_[t].get('y')
55
+                            w = _tags_[t].get('w')
56
+                            h = _tags_[t].get('h')
57
+                            if x is not None and y is not None and w is not None and h is not None:
58
+                                width = i.item_data.width
59
+                                height = i.item_data.height
60
+                                x1 = int(x * width)
61
+                                y1 = int(y * height)
62
+                                x2 = x1 + int(w * width)
63
+                                y2 = y1 + int(h * height)
64
+                                if is_valid_area(x1, y1, x2, y2):
65
+                                    new_tag['topleft_x'] = x1
66
+                                    new_tag['topleft_y'] = y1
67
+                                    new_tag['bottomright_x'] = x2
68
+                                    new_tag['bottomright_y'] = y2
69
+                                    new_tags.append(new_tag)
70
+                                else:
71
+                                    self.stderr.write(self.style.ERROR('Coordinates are not valid for %s. Not transferred!' % _rel_path_))
72
+                            else:
73
+                                new_tags.append(new_tag)
74
+                        if len(new_tags) > 0:
75
+                            if KEY_USERDATA_TAGS not in data:
76
+                                data[KEY_USERDATA_TAGS] = {}
77
+                            data[KEY_USERDATA_TAGS][_rel_path_] = new_tags
78
+                    if len(_upload_):
79
+                        if KEY_USERDATA_UPLOAD not in data:
80
+                            data[KEY_USERDATA_UPLOAD] = {}
81
+                        data[KEY_USERDATA_UPLOAD][_rel_path_] = _upload_
82
+        self.stdout.write(json.dumps(data, indent=4, sort_keys=True))

+ 28
- 0
management/commands/delete_lost_items.py 查看文件

@@ -0,0 +1,28 @@
1
+from django.core.management.base import BaseCommand
2
+import os
3
+import pygal
4
+from pygal.models import Item
5
+
6
+
7
+class Command(BaseCommand):
8
+    def handle(self, *args, **options):
9
+        lil = self.lost_item_list()
10
+        if len(lil) == 0:
11
+            self.stdout.write(self.style.SUCCESS('No lost items. Nothing to delete.'))
12
+        else:
13
+            self.stdout.write(self.style.WARNING('Lost Items:'))
14
+            for item in lil:
15
+                self.stdout.write(self.style.WARNING('  - %s' % str(item)))
16
+            response = input('Delete the listed items and all data inside permanently? [y/N]: ')
17
+            if response.lower() == 'y':
18
+                self.stdout.write(self.style.SUCCESS('Deleting Items:'))
19
+                for item in lil:
20
+                    item.delete()
21
+                    self.stdout.write(self.style.SUCCESS('  - Deleted: %s' % str(item)))
22
+
23
+    def lost_item_list(self):
24
+        lil = []
25
+        for i in Item.objects.all():
26
+            if not os.path.exists(pygal.get_full_path(i.rel_path)):
27
+                lil.append(i)
28
+        return lil

+ 37
- 0
management/commands/export_userdata.py 查看文件

@@ -0,0 +1,37 @@
1
+from django.contrib.auth.models import User
2
+from django.core.management.base import BaseCommand
3
+import json
4
+from pygal.models import Tag
5
+
6
+from ._private import KEY_USERDATA_VERSION
7
+from ._private import KEY_USERDATA_FAVOURITE
8
+from ._private import KEY_USERDATA_TAGS
9
+
10
+
11
+class Command(BaseCommand):
12
+    help = 'Export userdata in JSON-Format.'
13
+
14
+    def handle(self, *args, **options):
15
+        data = {}
16
+        data[KEY_USERDATA_VERSION] = 1
17
+        for user in User.objects.all():
18
+            favourites = user.favourite_of.all()
19
+            if len(favourites) > 0:
20
+                if KEY_USERDATA_FAVOURITE not in data:
21
+                    data[KEY_USERDATA_FAVOURITE] = {}
22
+                data[KEY_USERDATA_FAVOURITE][user.username] = [i.rel_path for i in favourites]
23
+        for tag in Tag.objects.all():
24
+            if KEY_USERDATA_TAGS not in data:
25
+                data[KEY_USERDATA_TAGS] = {}
26
+            if tag.item.rel_path not in data[KEY_USERDATA_TAGS]:
27
+                data[KEY_USERDATA_TAGS][tag.item.rel_path] = []
28
+            tagdata = {}
29
+            tagdata['text'] = tag.text
30
+            if tag.has_valid_coordinates:
31
+                tagdata['topleft_x'] = tag.topleft_x
32
+                tagdata['topleft_y'] = tag.topleft_y
33
+                tagdata['bottomright_x'] = tag.bottomright_x
34
+                tagdata['bottomright_y'] = tag.bottomright_y
35
+            data[KEY_USERDATA_TAGS][tag.item.rel_path].append(tagdata)
36
+
37
+        self.stdout.write(json.dumps(data, indent=4, sort_keys=True))

+ 98
- 0
management/commands/import_userdata.py 查看文件

@@ -0,0 +1,98 @@
1
+from django.contrib.auth.models import User
2
+from django.core.management.base import BaseCommand
3
+import json
4
+from pygal.models import Item, Tag
5
+import os
6
+
7
+from ._private import KEY_USERDATA_VERSION
8
+from ._private import KEY_USERDATA_FAVOURITE
9
+from ._private import KEY_USERDATA_TAGS
10
+
11
+
12
+class Command(BaseCommand):
13
+    help = 'Import userdata from a JSON-File.'
14
+
15
+    def add_arguments(self, parser):
16
+        parser.add_argument('filename(s)', nargs='+', type=str)
17
+
18
+    def error_skipped_file(self, path, reason):
19
+        self.stderr.write('  Skipped file %s caused by an error (%s) !' % (path, reason))
20
+
21
+    def handle(self, *args, **options):
22
+        for fn in options['filename(s)']:
23
+            fn = os.path.abspath(fn)
24
+            self.stdout.write('Parsing %s:' % fn)
25
+            try:
26
+                with open(fn, 'r') as fh:
27
+                    data = json.load(fh)
28
+                    try:
29
+                        version = data.pop(KEY_USERDATA_VERSION)
30
+                    except KeyError:
31
+                        version = 1
32
+            except PermissionError:
33
+                self.error_skipped_file(fn, 'file permission')
34
+            except json.decoder.JSONDecodeError:
35
+                self.error_skipped_file(fn, 'json decoding')
36
+            except FileNotFoundError:
37
+                self.error_skipped_file(fn, 'file not found')
38
+            else:
39
+                for section in data:
40
+                    self.stdout.write('  Importing "%s":' % section)
41
+                    cnt_all_datasets = 0
42
+                    cnt_success = 0
43
+                    cnt_already_exist = 0
44
+                    cnt_user_missing = 0
45
+                    cnt_item_missing = 0
46
+                    if section == KEY_USERDATA_FAVOURITE:
47
+                        for username in data[section]:
48
+                            try:
49
+                                user = User.objects.get(username=username)
50
+                            except User.DoesNotExist:
51
+                                user = None
52
+                            for rel_path in data[section][username]:
53
+                                cnt_all_datasets += 1
54
+                                if user is None:
55
+                                    cnt_user_missing += 1
56
+                                try:
57
+                                    i = Item.objects.get(rel_path=rel_path)
58
+                                except Item.DoesNotExist:
59
+                                    cnt_item_missing += 1
60
+                                else:
61
+                                    if user in i.favourite_of.all():
62
+                                        cnt_already_exist += 1
63
+                                    else:
64
+                                        cnt_success += 1
65
+                                        i.favourite_of.add(user)
66
+                    elif section == KEY_USERDATA_TAGS:
67
+                        for rel_path in data[section]:
68
+                            cnt_all_datasets += len(data[section][rel_path])
69
+                            try:
70
+                                i = Item.objects.get(rel_path=rel_path)
71
+                            except Item.DoesNotExist:
72
+                                cnt_item_missing += len(data[section][rel_path])
73
+                            else:
74
+                                for tag_data in data[section][rel_path]:
75
+                                    if len(Tag.objects.filter(item=i, text=tag_data['text'])) > 0:
76
+                                        cnt_already_exist += 1
77
+                                    else:
78
+                                        tag_data['item'] = i
79
+                                        Tag(**tag_data).save()
80
+                                        cnt_success += 1
81
+                    else:
82
+                        self.stdout.write(self.style.ERROR('    Section unknown!'))
83
+                    #
84
+                    # REPORT
85
+                    #
86
+                    if cnt_all_datasets > 0:
87
+                        if (cnt_success + cnt_already_exist) < cnt_all_datasets:
88
+                            style = self.style.WARNING
89
+                            self.stdout.write(style('    NOT all data imported.'))
90
+                        else:
91
+                            style = self.style.SUCCESS
92
+                            self.stdout.write(style('    All data imported.'))
93
+                        self.stdout.write(style('    %5d %s(s) successfully added.' % (cnt_success, section)))
94
+                        self.stdout.write(style('    %5d %s(s) already set correctly.' % (cnt_already_exist, section)))
95
+                        if cnt_user_missing > 0:
96
+                            self.stdout.write(self.style.ERROR('    %5d %s(s) not added, because the target user does not exist.' % (cnt_user_missing, section)))
97
+                        if cnt_item_missing > 0:
98
+                            self.stdout.write(self.style.ERROR('    %5d %s(s) not added, because the target item does not exist.' % (cnt_item_missing, section)))

+ 25
- 0
management/commands/rebuild_cache.py 查看文件

@@ -0,0 +1,25 @@
1
+from django.conf import settings
2
+from django.core.management.base import BaseCommand
3
+import fstools
4
+import pygal
5
+from pygal.models import Item, TYPE_FOLDER, get_item_by_rel_path
6
+
7
+
8
+class Command(BaseCommand):
9
+    def handle(self, *args, **options):
10
+        # rebuild existing non folder items
11
+        #
12
+        for item in Item.objects.all().exclude(type=TYPE_FOLDER):
13
+            item.__set_model_fields_from_file__()
14
+            item.save()
15
+        # rebuild folder items based on file system structure (not existing items will be created)
16
+        folder_list = fstools.dirlist(settings.ITEM_ROOT, rekursive=True)
17
+        folder_list.append(settings.ITEM_ROOT)
18
+        folder_list.sort(reverse=True)
19
+        for full_path in folder_list:
20
+            item = get_item_by_rel_path(pygal.get_rel_path(full_path))
21
+            if item is not None:
22
+                item.__set_model_fields_from_file__()
23
+                item.save()
24
+        self.stdout.write(self.style.SUCCESS('Updated cached data of %d items.' % len(Item.objects.all().exclude(type=TYPE_FOLDER))))
25
+        self.stdout.write(self.style.SUCCESS('Updated cached data of %d folders.' % len(Item.objects.filter(type=TYPE_FOLDER))))

+ 9
- 0
management/commands/rebuild_index.py 查看文件

@@ -0,0 +1,9 @@
1
+from django.core.management.base import BaseCommand
2
+from pygal.search import create_index, rebuild_index
3
+
4
+
5
+class Command(BaseCommand):
6
+    def handle(self, *args, **options):
7
+        ix = create_index()
8
+        n = rebuild_index(ix)
9
+        self.stdout.write(self.style.SUCCESS('Search index for %d items created.') % n)

+ 84
- 0
migrations/0001_initial.py 查看文件

@@ -0,0 +1,84 @@
1
+# Generated by Django 2.2.5 on 2019-12-20 12:09
2
+
3
+from django.conf import settings
4
+from django.db import migrations, models
5
+import django.db.models.deletion
6
+
7
+
8
+class Migration(migrations.Migration):
9
+
10
+    initial = True
11
+
12
+    dependencies = [
13
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14
+    ]
15
+
16
+    operations = [
17
+        migrations.CreateModel(
18
+            name='Item',
19
+            fields=[
20
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21
+                ('rel_path', models.TextField(unique=True)),
22
+                ('type', models.CharField(choices=[('audio', 'Audio'), ('folder', 'Folder'), ('image', 'Image'), ('other', 'Other'), ('video', 'Video')], max_length=25)),
23
+                ('public_access', models.BooleanField(default=False)),
24
+                ('uid_c', models.CharField(blank=True, max_length=50, null=True)),
25
+                ('settings_c', models.IntegerField(blank=True, null=True)),
26
+                ('data_version_c', models.IntegerField(blank=True, null=True)),
27
+                ('size_c', models.IntegerField(blank=True, null=True)),
28
+                ('datetime_c', models.DateTimeField(blank=True, null=True)),
29
+                ('exposure_program_c', models.CharField(blank=True, max_length=100, null=True)),
30
+                ('exposure_time_c', models.FloatField(blank=True, null=True)),
31
+                ('flash_c', models.CharField(blank=True, max_length=100, null=True)),
32
+                ('f_number_c', models.FloatField(blank=True, null=True)),
33
+                ('focal_length_c', models.FloatField(blank=True, null=True)),
34
+                ('lon_c', models.FloatField(blank=True, null=True)),
35
+                ('lat_c', models.FloatField(blank=True, null=True)),
36
+                ('height_c', models.IntegerField(blank=True, null=True)),
37
+                ('iso_c', models.IntegerField(blank=True, null=True)),
38
+                ('camera_vendor_c', models.CharField(blank=True, max_length=100, null=True)),
39
+                ('camera_model_c', models.CharField(blank=True, max_length=100, null=True)),
40
+                ('orientation_c', models.IntegerField(blank=True, null=True)),
41
+                ('width_c', models.IntegerField(blank=True, null=True)),
42
+                ('duration_c', models.FloatField(blank=True, null=True)),
43
+                ('ratio_c', models.FloatField(blank=True, null=True)),
44
+                ('album_c', models.CharField(blank=True, max_length=100, null=True)),
45
+                ('artist_c', models.CharField(blank=True, max_length=100, null=True)),
46
+                ('bitrate_c', models.IntegerField(blank=True, null=True)),
47
+                ('genre_c', models.CharField(blank=True, max_length=100, null=True)),
48
+                ('title_c', models.CharField(blank=True, max_length=100, null=True)),
49
+                ('track_c', models.IntegerField(blank=True, null=True)),
50
+                ('year_c', models.IntegerField(blank=True, null=True)),
51
+                ('num_audio_c', models.IntegerField(blank=True, null=True)),
52
+                ('num_folders_c', models.IntegerField(blank=True, null=True)),
53
+                ('num_images_c', models.IntegerField(blank=True, null=True)),
54
+                ('num_other_c', models.IntegerField(blank=True, null=True)),
55
+                ('num_videos_c', models.IntegerField(blank=True, null=True)),
56
+                ('sil_c', models.TextField(blank=True, null=True)),
57
+                ('favourite_of', models.ManyToManyField(blank=True, related_name='favourite_of', to=settings.AUTH_USER_MODEL)),
58
+                ('modify_access', models.ManyToManyField(blank=True, related_name='modify_access', to=settings.AUTH_USER_MODEL)),
59
+                ('read_access', models.ManyToManyField(blank=True, related_name='read_access', to=settings.AUTH_USER_MODEL)),
60
+            ],
61
+        ),
62
+        migrations.CreateModel(
63
+            name='Setting',
64
+            fields=[
65
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
66
+                ('show_image', models.BooleanField(default=True)),
67
+                ('show_video', models.BooleanField(default=True)),
68
+                ('show_audio', models.BooleanField(default=False)),
69
+                ('show_other', models.BooleanField(default=False)),
70
+            ],
71
+        ),
72
+        migrations.CreateModel(
73
+            name='Tag',
74
+            fields=[
75
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
76
+                ('text', models.CharField(max_length=100)),
77
+                ('topleft_x', models.IntegerField(blank=True, null=True)),
78
+                ('topleft_y', models.IntegerField(blank=True, null=True)),
79
+                ('bottomright_x', models.IntegerField(blank=True, null=True)),
80
+                ('bottomright_y', models.IntegerField(blank=True, null=True)),
81
+                ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pygal.Item')),
82
+            ],
83
+        ),
84
+    ]

+ 18
- 0
migrations/0002_setting_suspend_puplic.py 查看文件

@@ -0,0 +1,18 @@
1
+# Generated by Django 2.2.5 on 2019-12-22 08:42
2
+
3
+from django.db import migrations, models
4
+
5
+
6
+class Migration(migrations.Migration):
7
+
8
+    dependencies = [
9
+        ('pygal', '0001_initial'),
10
+    ]
11
+
12
+    operations = [
13
+        migrations.AddField(
14
+            model_name='setting',
15
+            name='suspend_puplic',
16
+            field=models.BooleanField(default=True),
17
+        ),
18
+    ]

+ 0
- 0
migrations/__init__.py 查看文件


+ 642
- 0
models.py 查看文件

@@ -0,0 +1,642 @@
1
+import datetime
2
+from django.contrib.auth.models import User
3
+from django.db import models
4
+from django.utils import formats, timezone
5
+from django.urls import reverse
6
+import fstools
7
+import json
8
+import logging
9
+import os
10
+from PIL import Image
11
+import pygal
12
+import subprocess
13
+import time
14
+
15
+DEBUG = False
16
+
17
+
18
+TYPE_FOLDER = 'folder'
19
+TYPE_IMAGE = 'image'
20
+TYPE_VIDEO = 'video'
21
+TYPE_AUDIO = 'audio'
22
+TYPE_OTHER = 'other'
23
+
24
+EXTENTIONS_IMAGE = ['.jpg', '.jpeg', '.jpe', '.png', '.tif', '.tiff', '.gif', ]
25
+EXTENTIONS_AUDIO = ['.mp3', ]
26
+EXTENTIONS_VIDEO = ['.avi', '.mpg', '.mpeg', '.mpe', '.mov', '.qt', '.mp4', '.webm', '.ogv', '.flv', '.3gp', ]
27
+
28
+
29
+# Get a logger instance
30
+logger = logging.getLogger("CACHING")
31
+
32
+
33
+def get_item_type(full_path):
34
+    if os.path.isdir(full_path):
35
+        return TYPE_FOLDER
36
+    else:
37
+        if os.path.splitext(full_path)[1].lower() in EXTENTIONS_IMAGE:
38
+            return TYPE_IMAGE
39
+        elif os.path.splitext(full_path)[1].lower() in EXTENTIONS_VIDEO:
40
+            return TYPE_VIDEO
41
+        elif os.path.splitext(full_path)[1].lower() in EXTENTIONS_AUDIO:
42
+            return TYPE_AUDIO
43
+        return TYPE_OTHER
44
+
45
+
46
+def supported_types():
47
+    rv = []
48
+    if pygal.show_audio():
49
+        rv.append(TYPE_AUDIO)
50
+    if pygal.show_image():
51
+        rv.append(TYPE_IMAGE)
52
+    if pygal.show_other():
53
+        rv.append(TYPE_OTHER)
54
+    if pygal.show_video():
55
+        rv.append(TYPE_VIDEO)
56
+    return rv
57
+
58
+
59
+def get_item_by_rel_path(rel_path):
60
+    try:
61
+        rv = Item.objects.get(rel_path=rel_path)
62
+    except Item.DoesNotExist:
63
+        rv = None
64
+    if rv is not None:
65
+        # return the existing item
66
+        return rv
67
+    else:
68
+        # create new item, if rel_path exists in filesystem (folders needs to hold files)
69
+        full_path = pygal.get_full_path(rel_path)
70
+        if os.path.exists(full_path):
71
+            # file exists or folder has files in substructure
72
+            if get_item_type(full_path) != TYPE_FOLDER or len(fstools.filelist(full_path)) > 0:
73
+                i = Item(rel_path=rel_path)
74
+                i.save()
75
+                logger.info('New Item created: %s', repr(rel_path))
76
+                return i
77
+
78
+
79
+def ffprobe_lines(full_path):
80
+    def _ffprobe_command(full_path):
81
+        return ['ffprobe', '-v', 'quiet', '-show_format', '-show_streams', full_path]
82
+
83
+    def _avprobe_command(full_path):
84
+        return ['avprobe', '-v', 'quiet', '-show_format', '-show_streams', full_path]
85
+
86
+    def decode(string):
87
+        for i in ['utf-8', 'cp1252']:
88
+            try:
89
+                return string.decode(i)
90
+            except UnicodeEncodeError:
91
+                pass
92
+            except UnicodeDecodeError:
93
+                pass
94
+        return string
95
+
96
+    try:
97
+        try:
98
+            text_to_be_parsed = subprocess.check_output(_avprobe_command(full_path))
99
+        except OSError:
100
+            text_to_be_parsed = subprocess.check_output(_ffprobe_command(full_path))
101
+    except subprocess.CalledProcessError:
102
+        text_to_be_parsed = ''
103
+    return decode(text_to_be_parsed).splitlines()
104
+
105
+
106
+def is_valid_area(x1, y1, x2, y2):
107
+    for p in [x1, y1, x2, y2]:
108
+        if type(p) is not int:
109
+            return False
110
+    if (x1, y1) == (x2, y2):
111
+        return False
112
+    return True
113
+
114
+
115
+class ItemData(dict):
116
+    DATA_FIELDS = [
117
+        'size',
118
+        'datetime',
119
+        'exposure_program',
120
+        'exposure_time',
121
+        'flash',
122
+        'f_number',
123
+        'focal_length',
124
+        'lon',
125
+        'lat',
126
+        'height',
127
+        'iso',
128
+        'camera_vendor',
129
+        'camera_model',
130
+        'orientation',
131
+        'width',
132
+        'duration',
133
+        'ratio',
134
+        'album',
135
+        'artist',
136
+        'bitrate',
137
+        'genre',
138
+        'title',
139
+        'track',
140
+        'year',
141
+        'sil',
142
+        'num_audio',
143
+        'num_folders',
144
+        'num_images',
145
+        'num_other',
146
+        'num_videos',
147
+    ]
148
+    DEFAULT_VALUES = {
149
+        'size': 0,
150
+        'datetime': datetime.datetime(1900, 1, 1),
151
+        'exposure_program': '-',
152
+        'exposure_time': 0,
153
+        'flash': '-',
154
+        'f_number': 0,
155
+        'focal_length': 0,
156
+        'iso': 0,
157
+        'camera_vendor': '-',
158
+        'camera_model': '-',
159
+        'orientation': 1,
160
+        'duration': 3,
161
+        'ratio': 1,
162
+        'album': '-',
163
+        'artist': '-',
164
+        'bitrate': 0,
165
+        'genre': '-',
166
+        'title': '-',
167
+        'track': 0,
168
+        'year': 0,
169
+        'num_audio': 0,
170
+        'num_folders': 0,
171
+        'num_images': 0,
172
+        'num_other': 0,
173
+        'num_videos': 0,
174
+    }
175
+
176
+    def __init__(self, item):
177
+        for key in self.DATA_FIELDS:
178
+            value = getattr(item, key + '_c')
179
+            if value is not None:
180
+                self[key] = value
181
+                setattr(self, key, value)
182
+            else:
183
+                if key in self.DEFAULT_VALUES:
184
+                    setattr(self, key, self.DEFAULT_VALUES[key])
185
+
186
+    @property
187
+    def formatted_datetime(self):
188
+        try:
189
+            return formats.date_format(self.get('datetime'), format="SHORT_DATE_FORMAT", use_l10n=True) + ' - ' + formats.time_format(self.get('datetime'), use_l10n=True)
190
+        except AttributeError:
191
+            return 'No Datetime available'
192
+
193
+    @property
194
+    def gps(self):
195
+        if self.get('lon') and self.get('lat'):
196
+            return {'lon': self.get('lon'), 'lat': self.get('lat')}
197
+        else:
198
+            return None
199
+
200
+
201
+class Item(models.Model):
202
+    DATA_VERSION_NUMBER = 0
203
+    #
204
+    rel_path = models.TextField(unique=True)
205
+    type = models.CharField(max_length=25, choices=((TYPE_AUDIO, 'Audio'), (TYPE_FOLDER, 'Folder'), (TYPE_IMAGE, 'Image'), (TYPE_OTHER, 'Other'), (TYPE_VIDEO, 'Video')))
206
+    public_access = models.BooleanField(default=False)
207
+    read_access = models.ManyToManyField(User, related_name="read_access", blank=True)
208
+    modify_access = models.ManyToManyField(User, related_name="modify_access", blank=True)
209
+    favourite_of = models.ManyToManyField(User, related_name="favourite_of", blank=True)
210
+    #
211
+    uid_c = models.CharField(max_length=50, null=True, blank=True)
212
+    settings_c = models.IntegerField(null=True, blank=True)
213
+    data_version_c = models.IntegerField(null=True, blank=True)
214
+    # common
215
+    size_c = models.IntegerField(null=True, blank=True)
216
+    datetime_c = models.DateTimeField(null=True, blank=True)
217
+    # image
218
+    exposure_program_c = models.CharField(max_length=100, null=True, blank=True)
219
+    exposure_time_c = models.FloatField(null=True, blank=True)
220
+    flash_c = models.CharField(max_length=100, null=True, blank=True)
221
+    f_number_c = models.FloatField(null=True, blank=True)
222
+    focal_length_c = models.FloatField(null=True, blank=True)
223
+    lon_c = models.FloatField(null=True, blank=True)
224
+    lat_c = models.FloatField(null=True, blank=True)
225
+    height_c = models.IntegerField(null=True, blank=True)
226
+    iso_c = models.IntegerField(null=True, blank=True)
227
+    camera_vendor_c = models.CharField(max_length=100, null=True, blank=True)
228
+    camera_model_c = models.CharField(max_length=100, null=True, blank=True)
229
+    orientation_c = models.IntegerField(null=True, blank=True)
230
+    width_c = models.IntegerField(null=True, blank=True)
231
+    # video
232
+    duration_c = models.FloatField(null=True, blank=True)
233
+    ratio_c = models.FloatField(null=True, blank=True)
234
+    # audio
235
+    album_c = models.CharField(max_length=100, null=True, blank=True)
236
+    artist_c = models.CharField(max_length=100, null=True, blank=True)
237
+    bitrate_c = models.IntegerField(null=True, blank=True)
238
+    genre_c = models.CharField(max_length=100, null=True, blank=True)
239
+    title_c = models.CharField(max_length=100, null=True, blank=True)
240
+    track_c = models.IntegerField(null=True, blank=True)
241
+    year_c = models.IntegerField(null=True, blank=True)
242
+    # folder
243
+    num_audio_c = models.IntegerField(null=True, blank=True)
244
+    num_folders_c = models.IntegerField(null=True, blank=True)
245
+    num_images_c = models.IntegerField(null=True, blank=True)
246
+    num_other_c = models.IntegerField(null=True, blank=True)
247
+    num_videos_c = models.IntegerField(null=True, blank=True)
248
+    sil_c = models.TextField(null=True, blank=True)
249
+
250
+    def __init__(self, *args, **kwargs):
251
+        self.__current_uid__ = None
252
+        self.__current_settings__ = None
253
+        models.Model.__init__(self, *args, **kwargs)
254
+
255
+    @property
256
+    def name(self):
257
+        return os.path.splitext(os.path.basename(self.rel_path))[0]
258
+
259
+    @property
260
+    def item_data(self):
261
+        if self.__cache_update_needed__():
262
+            #
263
+            self.__set_model_fields_from_file__()
264
+            #
265
+            self.save()
266
+        return self.cached_item_data
267
+
268
+    @property
269
+    def cached_item_data(self):
270
+        return ItemData(self)
271
+
272
+    def get_admin_url(self):
273
+        """the url to the Django admin interface for the model instance"""
274
+        info = (self._meta.app_label, self._meta.model_name)
275
+        return reverse('admin:%s_%s_change' % info, args=(self.pk,))
276
+
277
+    def suspended(self, user):
278
+        if pygal.suspend_public() and not user.is_authenticated:
279
+            return True
280
+
281
+    def may_read(self, user):
282
+        if self.suspended(user):
283
+            return False
284
+        elif self.type == TYPE_FOLDER:
285
+            return True
286
+        else:
287
+            parent = get_item_by_rel_path(os.path.dirname(self.rel_path))
288
+            if parent.public_access is True:
289
+                return True
290
+            if user is None:
291
+                return False
292
+            if not user.is_authenticated:
293
+                return False
294
+            if user.is_superuser:
295
+                return True
296
+            return user in parent.read_access.all()
297
+
298
+    def may_modify(self, user):
299
+        if self.suspended(user):
300
+            return False
301
+        elif self.type == TYPE_FOLDER:
302
+            return user in self.modify_access.all()
303
+        else:
304
+            if user is None:
305
+                return False
306
+            if not user.is_authenticated:
307
+                return False
308
+            if user.is_superuser:
309
+                return True
310
+            parent = get_item_by_rel_path(os.path.dirname(self.rel_path))
311
+            return user in parent.modify_access.all()
312
+
313
+    def sort_string(self):
314
+        try:
315
+            tm = int(self.item_data.datetime.strftime('%s'))
316
+        except AttributeError:
317
+            raise AttributeError('Unable to create a sortstring for %s. Used datetime was: %s' % (str(self), repr(self.item_data.datetime)))
318
+        return '%012d_%s' % (tm, os.path.basename(self.rel_path))
319
+
320
+    def sorted_itemlist(self):
321
+        if self.type == TYPE_FOLDER:
322
+            rv = []
323
+            for rel_path in json.loads(self.item_data['sil']):
324
+                try:
325
+                    rv.append(Item.objects.get(rel_path=rel_path))
326
+                except Item.DoesNotExist:
327
+                    raise Item.DoesNotExist("%s: Item with rel_path=%s does not exist. in %s." % (str(self), rel_path))
328
+            return rv
329
+        else:
330
+            return None
331
+
332
+    def thumbnail_item(self):
333
+        if self.type == TYPE_FOLDER:
334
+            try:
335
+                first_rel_path = json.loads(self.item_data['sil'])[0]
336
+                return Item.objects.get(rel_path=first_rel_path).thumbnail_item()
337
+            except IndexError:
338
+                raise Item.DoesNotExist("%s: Tried to get the thumbnail_item for an empty list." % str(self))
339
+            except Item.DoesNotExist:
340
+                raise Item.DoesNotExist("%s: Item with rel_path=%s does not exist. in %s." % (str(self), first_rel_path))
341
+        else:
342
+            return self
343
+
344
+    def all_itemslist(self):
345
+        rv = []
346
+        for i in self.sorted_itemlist():
347
+            if i.type != TYPE_FOLDER:
348
+                rv.append(i)
349
+            else:
350
+                rv.extend(i.all_itemslist())
351
+        return rv
352
+
353
+    def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
354
+        if self.__cache_update_needed__():
355
+            self.__set_model_fields_from_file__()
356
+        return models.Model.save(self, force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
357
+
358
+    def current_uid(self):
359
+        if self.__current_uid__ is None:
360
+            self.__current_uid__ = fstools.uid(pygal.get_full_path(self.rel_path), None)
361
+        return self.__current_uid__
362
+
363
+    def current_settings(self):
364
+        if self.__current_settings__ is None:
365
+            self.__current_settings__ = pygal.show_audio() * 1 + pygal.show_image() * 2 + pygal.show_other() * 3 + pygal.show_video() * 4
366
+        return self.__current_settings__
367
+
368
+    def __cache_update_needed__(self):
369
+        if self.type == TYPE_FOLDER:
370
+            return DEBUG or self.settings_c != self.current_settings() or self.uid_c != self.current_uid() or self.data_version_c != self.DATA_VERSION_NUMBER
371
+        else:
372
+            return DEBUG or self.uid_c != self.current_uid() or self.data_version_c != self.DATA_VERSION_NUMBER
373
+
374
+    def __set_model_fields_from_file__(self):
375
+        logger.info('Updating cached data for Item: %s' % repr(self.rel_path))
376
+        full_path = pygal.get_full_path(self.rel_path)
377
+        self.type = get_item_type(full_path)
378
+        self.uid_c = self.current_uid()
379
+        self.settings_c = self.current_settings()
380
+        self.data_version_c = self.DATA_VERSION_NUMBER
381
+        if self.type == TYPE_AUDIO:
382
+            self.__update_audio_file_data__(full_path)
383
+        elif self.type == TYPE_FOLDER:
384
+            self.__update_folder_file_data__(full_path)
385
+        elif self.type == TYPE_IMAGE:
386
+            self.__update_image_file_data__(full_path)
387
+        elif self.type == TYPE_OTHER:
388
+            self.__update_other_file_data__(full_path)
389
+        elif self.type == TYPE_VIDEO:
390
+            self.__update_video_file_data__(full_path)
391
+        for key, value in self.cached_item_data.items():
392
+            logger.debug('  - Adding %s=%s', key, repr(value))
393
+
394
+    def __update_audio_file_data__(self, full_path):
395
+        self.size_c = os.path.getsize(full_path)
396
+        #
397
+        tag_type_target_dict = {}
398
+        tag_type_target_dict['album'] = (str, 'album')
399
+        tag_type_target_dict['TAG:album'] = (str, 'album')
400
+        tag_type_target_dict['TAG:artist'] = (str, 'artist')
401
+        tag_type_target_dict['artist'] = (str, 'artist')
402
+        tag_type_target_dict['bit_rate'] = (self.__int_conv__, 'bitrate')
403
+        tag_type_target_dict['duration'] = (float, 'duration')
404
+        tag_type_target_dict['TAG:genre'] = (str, 'genre')
405
+        tag_type_target_dict['genre'] = (str, 'genre')
406
+        tag_type_target_dict['TAG:title'] = (str, 'title')
407
+        tag_type_target_dict['title'] = (str, 'title')
408
+        tag_type_target_dict['TAG:track'] = (self.__int_conv__, 'track')
409
+        tag_type_target_dict['track'] = (self.__int_conv__, 'track')
410
+        tag_type_target_dict['TAG:date'] = (self.__int_conv__, 'year')
411
+        tag_type_target_dict['date'] = (self.__int_conv__, 'year')
412
+        for line in ffprobe_lines(full_path):
413
+            try:
414
+                key, val = [snippet.strip() for snippet in line.split('=')]
415
+            except ValueError:
416
+                continue
417
+            else:
418
+                if key in tag_type_target_dict:
419
+                    tp, name = tag_type_target_dict[key]
420
+                    try:
421
+                        setattr(self, name + '_c', tp(val))
422
+                    except ValueError:
423
+                        logger.log(logging.WARNING if val else logger.INFO, 'Can\'t convert %s (%s) for %s', repr(val), name, self.name)
424
+        #
425
+        if self.year_c is not None and self.track_c is not None:
426
+            self.datetime_c = datetime.datetime(max(1971, self.year_c), 1, 1, 12, 0, (60 - self.track_c) % 60, tzinfo=timezone.utc)
427
+
428
+    def __update_folder_file_data__(self, full_path):
429
+        sil = []
430
+        self.size_c = 0
431
+        self.num_audio_c = 0
432
+        self.num_folders_c = 1
433
+        self.num_images_c = 0
434
+        self.num_other_c = 0
435
+        self.num_videos_c = 0
436
+        for fn in os.listdir(full_path):
437
+            sub_rel_path = pygal.get_rel_path(os.path.join(full_path, fn))
438
+            sub_item = get_item_by_rel_path(sub_rel_path)
439
+            # size, num_*
440
+            if sub_item is not None:     # Item does really exist / has sub-items
441
+                if (sub_item.type == TYPE_FOLDER and len(json.loads(sub_item.item_data['sil']))) or sub_item.type in supported_types():
442
+                    self.size_c += sub_item.item_data.size
443
+                    if sub_item.type == TYPE_AUDIO:
444
+                        self.num_audio_c += 1
445
+                    elif sub_item.type == TYPE_FOLDER:
446
+                        self.num_audio_c += sub_item.item_data['num_audio']
447
+                        self.num_folders_c += sub_item.item_data['num_folders']
448
+                        self.num_images_c += sub_item.item_data['num_images']
449
+                        self.num_other_c += sub_item.item_data['num_other']
450
+                        self.num_videos_c += sub_item.item_data['num_videos']
451
+                    elif sub_item.type == TYPE_IMAGE:
452
+                        self.num_images_c += 1
453
+                    elif sub_item.type == TYPE_OTHER:
454
+                        self.num_other_c += 1
455
+                    elif sub_item.type == TYPE_VIDEO:
456
+                        self.num_videos_c += 1
457
+        # sorted item list
458
+                    sil.append(sub_item)
459
+        sil.sort(key=lambda entry: entry.sort_string(), reverse=True)
460
+        self.sil_c = json.dumps([i.rel_path for i in sil], indent=4)
461
+        # datetime
462
+        if len(sil) > 0:
463
+            self.datetime_c = sil[0].datetime_c
464
+
465
+    def __update_image_file_data__(self, full_path):
466
+        self.size_c = os.path.getsize(full_path)
467
+        #
468
+        tag_type_target_dict = {}
469
+        tag_type_target_dict[0x9003] = (self.__datetime_conv__, 'datetime')
470
+        tag_type_target_dict[0x8822] = (self.__exposure_program_conv__, 'exposure_program')
471
+        tag_type_target_dict[0x829A] = (self.__num_denum_conv__, 'exposure_time')
472
+        tag_type_target_dict[0x9209] = (self.__flash_conv__, 'flash')
473
+        tag_type_target_dict[0x829D] = (self.__num_denum_conv__, 'f_number')
474
+        tag_type_target_dict[0x920A] = (self.__num_denum_conv__, 'focal_length')
475
+        tag_type_target_dict[0x8825] = (self.__gps_conv__, ('lon', 'lat'))
476
+        tag_type_target_dict[0xA003] = (self.__int_conv__, 'height')
477
+        tag_type_target_dict[0x8827] = (self.__int_conv__, 'iso')
478
+        tag_type_target_dict[0x010F] = (str, 'camera_vendor')
479
+        tag_type_target_dict[0x0110] = (str, 'camera_model')
480
+        tag_type_target_dict[0x0112] = (self.__int_conv__, 'orientation')
481
+        tag_type_target_dict[0xA002] = (self.__int_conv__, 'width')
482
+        im = Image.open(full_path)
483
+        try:
484
+            exif = dict(im._getexif().items())
485
+        except AttributeError:
486
+            logger.debug('%s does not have any exif information', full_path)
487
+        else:
488
+            for key in tag_type_target_dict:
489
+                if key in exif:
490
+                    tp, name = tag_type_target_dict[key]
491
+                    if type(name) is tuple:
492
+                        data = tp(exif[key]) or (None, None)
493
+                        for name, val in zip(name, data):
494
+                            setattr(self, name + '_c', val)
495
+                    else:
496
+                        setattr(self, name + '_c', tp(exif[key]))
497
+
498
+    def __update_other_file_data__(self, full_path):
499
+        self.size_c = os.path.getsize(full_path)
500
+        #
501
+        self.datetime_c = datetime.datetime.fromtimestamp(os.path.getctime(full_path), tz=timezone.utc)
502
+
503
+    def __update_video_file_data__(self, full_path):
504
+        self.size_c = os.path.getsize(full_path)
505
+        #
506
+        tag_type_target_dict = {}
507
+        tag_type_target_dict['creation_time'] = (self.__vid_datetime_conv__, 'datetime')
508
+        tag_type_target_dict['TAG:creation_time'] = (self.__vid_datetime_conv__, 'datetime')
509
+        tag_type_target_dict['duration'] = (float, 'duration')
510
+        tag_type_target_dict['height'] = (self.__int_conv__, 'height')
511
+        tag_type_target_dict['width'] = (self.__int_conv__, 'width')
512
+        tag_type_target_dict['display_aspect_ratio'] = (self.__ratio_conv__, 'ratio')
513
+        for line in ffprobe_lines(full_path):
514
+            try:
515
+                key, val = [snippet.strip() for snippet in line.split('=')]
516
+            except ValueError:
517
+                continue
518
+            else:
519
+                if key in tag_type_target_dict:
520
+                    tp, name = tag_type_target_dict[key]
521
+                    try:
522
+                        setattr(self, name + '_c', tp(val))
523
+                    except ValueError:
524
+                        logger.log(logging.WARNING if val else logger.INFO, 'Can\'t convert %s (%s) for %s', repr(val), name, self.name)
525
+
526
+    def __datetime_conv__(self, dt):
527
+        format_string = "%Y:%m:%d %H:%M:%S%z"
528
+        return datetime.datetime.strptime(dt + '+0000', format_string)
529
+
530
+    def __exposure_program_conv__(self, n):
531
+        return {
532
+            0: 'Unidentified',
533
+            1: 'Manual',
534
+            2: 'Program Normal',
535
+            3: 'Aperture Priority',
536
+            4: 'Shutter Priority',
537
+            5: 'Program Creative',
538
+            6: 'Program Action',
539
+            7: 'Portrait Mode',
540
+            8: 'Landscape Mode'
541
+        }.get(n, '-')
542
+
543
+    def __flash_conv__(self, n):
544
+        return {
545
+            0: 'No',
546
+            1: 'Fired',
547
+            5: 'Fired (?)',  # no return sensed
548
+            7: 'Fired (!)',  # return sensed
549
+            9: 'Fill Fired',
550
+            13: 'Fill Fired (?)',
551
+            15: 'Fill Fired (!)',
552
+            16: 'Off',
553
+            24: 'Auto Off',
554
+            25: 'Auto Fired',
555
+            29: 'Auto Fired (?)',
556
+            31: 'Auto Fired (!)',
557
+            32: 'Not Available'
558
+        }.get(n, '-')
559
+
560
+    def __int_conv__(self, value):
561
+        try:
562
+            return int(value)
563
+        except ValueError:
564
+            for c in ['.', '/', '-']:
565
+                p = value.find(c)
566
+                if p >= 0:
567
+                    value = value[:p]
568
+        if value == '':
569
+            return 0
570
+        return int(value)
571
+
572
+    def __num_denum_conv__(self, data):
573
+        num, denum = data
574
+        return num / denum
575
+
576
+    def __gps_conv__(self, data):
577
+        def lat_lon_cal(lon_or_lat):
578
+            lon_lat = 0.
579
+            fac = 1.
580
+            for num, denum in lon_or_lat:
581
+                lon_lat += float(num) / float(denum) * fac
582
+                fac *= 1. / 60.
583
+            return lon_lat
584
+        try:
585
+            lon = lat_lon_cal(data[0x0004])
586
+            lat = lat_lon_cal(data[0x0002])
587
+            if lon != 0 or lat != 0:    # do not use lon and lat equal 0, caused by motorola gps weakness
588
+                return lon, lat
589
+        except KeyError:
590
+            logger.warning('GPS data extraction failed for %s: %s', self.name, repr(data))
591
+        return None
592
+
593
+    def __vid_datetime_conv__(self, dt):
594
+        try:
595
+            dt = dt[:dt.index('.')]
596
+        except ValueError:
597
+            pass  # time string seems to have no '.'
598
+        dt = dt.replace('T', ' ').replace('/', '').replace('\\', '')
599
+        if len(dt) == 16:
600
+            dt += ':00'
601
+        dt += '+0000'
602
+        format_string = '%Y-%m-%d %H:%M:%S%z'
603
+        return datetime.datetime.strptime(dt, format_string)
604
+
605
+    def __ratio_conv__(self, ratio):
606
+        ratio = ratio.replace('\\', '')
607
+        num, denum = ratio.split(':')
608
+        return float(num) / float(denum)
609
+
610
+    def __str__(self):
611
+        return 'Item: %s' % self.rel_path
612
+
613
+
614
+class Tag(models.Model):
615
+    item = models.ForeignKey(Item, on_delete=models.CASCADE)
616
+    text = models.CharField(max_length=100)
617
+    topleft_x = models.IntegerField(null=True, blank=True)
618
+    topleft_y = models.IntegerField(null=True, blank=True)
619
+    bottomright_x = models.IntegerField(null=True, blank=True)
620
+    bottomright_y = models.IntegerField(null=True, blank=True)
621
+
622
+    def __init__(self, *args, **kwargs):
623
+        self.__tm_start__ = time.time()
624
+        models.Model.__init__(self, *args, **kwargs)
625
+        logger.log(5, 'Initialising Tag Model object in %.02fs: %s', time.time() - self.__tm_start__, str(self))
626
+
627
+    @property
628
+    def has_valid_coordinates(self):
629
+        return is_valid_area(self.topleft_x, self.topleft_y, self.bottomright_x, self.bottomright_y)
630
+
631
+    def get_admin_url(self):
632
+        """the url to the Django admin interface for the model instance"""
633
+        info = (self._meta.app_label, self._meta.model_name)
634
+        return reverse('admin:%s_%s_change' % info, args=(self.pk,))
635
+
636
+
637
+class Setting(models.Model):
638
+    suspend_puplic = models.BooleanField(default=True)
639
+    show_image = models.BooleanField(default=True)
640
+    show_video = models.BooleanField(default=True)
641
+    show_audio = models.BooleanField(default=False)
642
+    show_other = models.BooleanField(default=False)

+ 20
- 0
queries.py 查看文件

@@ -0,0 +1,20 @@
1
+from .models import Item, supported_types
2
+import pygal
3
+from .search import load_index, search
4
+
5
+
6
+def get_readable_item_query(request, item_query):
7
+    uids = []
8
+    for i in item_query:
9
+        if i.may_read(request.user):
10
+            if i.type in supported_types():
11
+                uids.append(i.id)
12
+    uids = set(uids)
13
+    return Item.objects.filter(id__in=uids)
14
+
15
+
16
+def search_result_query(request):
17
+    search_txt = pygal.get_search_query(request)
18
+    ix = load_index()
19
+    #
20
+    return get_readable_item_query(request, search(ix, search_txt))

+ 125
- 0
search.py 查看文件

@@ -0,0 +1,125 @@
1
+from django.conf import settings
2
+import fstools
3
+import logging
4
+from .models import Item
5
+import os
6
+from whoosh.fields import Schema, ID, TEXT, KEYWORD, NUMERIC, DATETIME
7
+from whoosh.qparser.dateparse import DateParserPlugin
8
+from whoosh import index, qparser
9
+from pygal.models import TYPE_FOLDER
10
+
11
+logger = logging.getLogger("WHOOSH")
12
+
13
+
14
+SCHEMA = Schema(
15
+    rel_path=ID(unique=True, stored=True),
16
+    # Item
17
+    name=TEXT,
18
+    type=TEXT,
19
+    favourite_of=KEYWORD,
20
+    datetime=DATETIME,
21
+    size=NUMERIC,
22
+    # Tag
23
+    tag=KEYWORD,
24
+    # Image Cache
25
+    exposure_program=TEXT,
26
+    exposure_time=NUMERIC,
27
+    flash=TEXT,
28
+    f_number=NUMERIC,
29
+    focal_length=NUMERIC,
30
+    lon=NUMERIC,
31
+    lat=NUMERIC,
32
+    height=NUMERIC,
33
+    iso=NUMERIC,
34
+    camera_vendor=TEXT,
35
+    camera_model=TEXT,
36
+    orientation=NUMERIC,
37
+    width=NUMERIC,
38
+    # Audio Cache
39
+    album=TEXT,
40
+    artist=TEXT,
41
+    bitrate=NUMERIC,
42
+    duration=NUMERIC,
43
+    genre=TEXT,
44
+    title=TEXT,
45
+    track=NUMERIC,
46
+    year=NUMERIC,
47
+    #
48
+    ratio=NUMERIC,
49
+)
50
+
51
+
52
+def create_index():
53
+    logger.debug('Search Index created.')
54
+    return index.create_in(settings.WHOOSH_PATH, schema=SCHEMA)
55
+
56
+
57
+def load_index():
58
+        if not os.path.exists(settings.WHOOSH_PATH):
59
+            fstools.mkdir(settings.WHOOSH_PATH)
60
+        try:
61
+            ix = index.open_dir(settings.WHOOSH_PATH)
62
+        except index.EmptyIndexError:
63
+            ix = create_index()
64
+        else:
65
+            logger.debug('Search Index opened.')
66
+        return ix
67
+
68
+
69
+def item_is_supported(item):
70
+    return item.type != TYPE_FOLDER
71
+
72
+
73
+def add_item(ix, item):
74
+    # Collect data for the item
75
+    #
76
+    data = {
77
+        'rel_path': item.rel_path,
78
+        'name': os.path.splitext(item.rel_path.split('/')[-1])[0],
79
+        'type': item.type,
80
+    }
81
+    favourite_of = item.favourite_of.all()
82
+    if len(favourite_of) > 0:
83
+        data['favourite_of'] = ' '.join([u.username for u in favourite_of])
84
+    tags = item.tag_set.all()
85
+    if len(tags) > 0:
86
+        data['tag'] = ' '.join([t.text for t in tags])
87
+    for key, value in item.cached_item_data.items():
88
+        data[key] = value
89
+    # Write data to the index
90
+    #
91
+    with ix.writer() as w:
92
+        logger.info('Adding document with rel_path=%s to the search index.', data.get('rel_path'))
93
+        w.add_document(**data)
94
+        for key in data:
95
+            logger.debug('  - Adding %s=%s', key, repr(data[key]))
96
+
97
+
98
+def delete_item(ix, item):
99
+    with ix.writer() as w:
100
+        logger.info('Removing document with rel_path=%s from the search index.', item.rel_path)
101
+        w.delete_by_term("rel_path", item.rel_path)
102
+
103
+
104
+def update_item(ix, item):
105
+    if item_is_supported(item):
106
+        delete_item(ix, item)
107
+        add_item(ix, item)
108
+
109
+
110
+def rebuild_index(ix):
111
+    for item in Item.objects.all().exclude(type=TYPE_FOLDER):
112
+        add_item(ix, item)
113
+    return len(Item.objects.all().exclude(type=TYPE_FOLDER))
114
+
115
+
116
+def search(ix, search_txt):
117
+    qp = qparser.MultifieldParser(['name', 'tag'], ix.schema)
118
+    qp.add_plugin(DateParserPlugin(free=True))
119
+    q = qp.parse(search_txt)
120
+    with ix.searcher() as s:
121
+        results = s.search(q, limit=None)
122
+        rpl = []
123
+        for hit in results:
124
+            rpl.append(hit['rel_path'])
125
+        return Item.objects.filter(rel_path__in=rpl)

+ 37
- 0
signals.py 查看文件

@@ -0,0 +1,37 @@
1
+from django.db.models.signals import post_save
2
+from django.db.models.signals import post_delete
3
+from django.dispatch import receiver
4
+import logging
5
+from .models import Item, Tag
6
+from .search import load_index, delete_item, update_item
7
+import shutil
8
+from .views.image import base_item
9
+
10
+# Get a logger instance
11
+clogger = logging.getLogger("CACHING")
12
+
13
+__pre_remove__ = None
14
+__pre_add__ = None
15
+
16
+
17
+@receiver(post_delete, sender=Item)
18
+def item_delete(instance, **kwargs):
19
+    # delete cached xnails
20
+    clogger.info('Deleting Xnails stored in "%s".', base_item.__cache_image_folder__(None, instance.rel_path))
21
+    shutil.rmtree(base_item.__cache_image_folder__(None, instance.rel_path), True)
22
+    # delete index entry
23
+    ix = load_index()
24
+    delete_item(ix, instance)
25
+
26
+
27
+@receiver(post_save, sender=Item)
28
+def item_save(instance, **kwargs):
29
+    ix = load_index()
30
+    update_item(ix, instance)
31
+
32
+
33
+@receiver(post_save, sender=Tag)
34
+@receiver(post_delete, sender=Tag)
35
+def tag_change(instance, **kwargs):
36
+    ix = load_index()
37
+    update_item(ix, instance.item)

+ 41
- 0
static/pygal/js/imgareaselect-js/css/imgareaselect-animated.css 查看文件

@@ -0,0 +1,41 @@
1
+/*
2
+ * imgAreaSelect animated border style
3
+ */
4
+
5
+.imgareaselect-border1 {
6
+	background: url(border-anim-v.gif) repeat-y left top;
7
+}
8
+
9
+.imgareaselect-border2 {
10
+    background: url(border-anim-h.gif) repeat-x left top;
11
+}
12
+
13
+.imgareaselect-border3 {
14
+    background: url(border-anim-v.gif) repeat-y right top;
15
+}
16
+
17
+.imgareaselect-border4 {
18
+    background: url(border-anim-h.gif) repeat-x left bottom;
19
+}
20
+
21
+.imgareaselect-border1, .imgareaselect-border2,
22
+.imgareaselect-border3, .imgareaselect-border4 {
23
+    filter: alpha(opacity=50);
24
+	opacity: 0.5;
25
+}
26
+
27
+.imgareaselect-handle {
28
+    background-color: #fff;
29
+	border: solid 1px #000;
30
+    filter: alpha(opacity=50);
31
+	opacity: 0.5;
32
+}
33
+
34
+.imgareaselect-outer {
35
+	background-color: #000;
36
+    filter: alpha(opacity=50);
37
+	opacity: 0.5;
38
+}
39
+
40
+.imgareaselect-selection {
41
+}

+ 41
- 0
static/pygal/js/imgareaselect-js/css/imgareaselect-default.css 查看文件

@@ -0,0 +1,41 @@
1
+/*
2
+ * imgAreaSelect default style
3
+ */
4
+
5
+.imgareaselect-border1 {
6
+	background: url(border-v.gif) repeat-y left top;
7
+}
8
+
9
+.imgareaselect-border2 {
10
+    background: url(border-h.gif) repeat-x left top;
11
+}
12
+
13
+.imgareaselect-border3 {
14
+    background: url(border-v.gif) repeat-y right top;
15
+}
16
+
17
+.imgareaselect-border4 {
18
+    background: url(border-h.gif) repeat-x left bottom;
19
+}
20
+
21
+.imgareaselect-border1, .imgareaselect-border2,
22
+.imgareaselect-border3, .imgareaselect-border4 {
23
+    filter: alpha(opacity=50);
24
+	opacity: 0.5;
25
+}
26
+
27
+.imgareaselect-handle {
28
+    background-color: #fff;
29
+    border: solid 1px #000;
30
+    filter: alpha(opacity=50);
31
+    opacity: 0.5;
32
+}
33
+
34
+.imgareaselect-outer {
35
+    background-color: #000;
36
+    filter: alpha(opacity=50);
37
+    opacity: 0.5;
38
+}
39
+
40
+.imgareaselect-selection {  
41
+}

+ 36
- 0
static/pygal/js/imgareaselect-js/css/imgareaselect-deprecated.css 查看文件

@@ -0,0 +1,36 @@
1
+/*
2
+ * imgAreaSelect style to be used with deprecated options
3
+ */
4
+
5
+.imgareaselect-border1, .imgareaselect-border2,
6
+.imgareaselect-border3, .imgareaselect-border4 {
7
+    filter: alpha(opacity=50);
8
+	opacity: 0.5;
9
+}
10
+
11
+.imgareaselect-border1 {
12
+	border: solid 1px #000;
13
+}
14
+
15
+.imgareaselect-border2 {
16
+	border: dashed 1px #fff;
17
+}
18
+
19
+.imgareaselect-handle {
20
+    background-color: #fff;
21
+    border: solid 1px #000;
22
+    filter: alpha(opacity=50);
23
+    opacity: 0.5;
24
+}
25
+
26
+.imgareaselect-outer {
27
+    background-color: #000;
28
+    filter: alpha(opacity=40);
29
+    opacity: 0.4;
30
+}
31
+
32
+.imgareaselect-selection {
33
+	background-color: #fff;
34
+    filter: alpha(opacity=0);
35
+	opacity: 0;
36
+}

+ 730
- 0
static/pygal/js/imgareaselect-js/jquery.imgareaselect.js 查看文件

@@ -0,0 +1,730 @@
1
+/*
2
+ * imgAreaSelect jQuery plugin
3
+ * version 0.9.10
4
+ *
5
+ * Copyright (c) 2008-2013 Michal Wojciechowski (odyniec.net)
6
+ *
7
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
8
+ * and GPL (GPL-LICENSE.txt) licenses.
9
+ *
10
+ * http://odyniec.net/projects/imgareaselect/
11
+ *
12
+ */
13
+
14
+(function($) {
15
+
16
+var abs = Math.abs,
17
+    max = Math.max,
18
+    min = Math.min,
19
+    round = Math.round;
20
+
21
+function div() {
22
+    return $('<div/>');
23
+}
24
+
25
+$.imgAreaSelect = function (img, options) {
26
+    var
27
+
28
+        $img = $(img),
29
+
30
+        imgLoaded,
31
+
32
+        $box = div(),
33
+        $area = div(),
34
+        $border = div().add(div()).add(div()).add(div()),
35
+        $outer = div().add(div()).add(div()).add(div()),
36
+        $handles = $([]),
37
+
38
+        $areaOpera,
39
+
40
+        left, top,
41
+
42
+        imgOfs = { left: 0, top: 0 },
43
+
44
+        imgWidth, imgHeight,
45
+
46
+        $parent,
47
+
48
+        parOfs = { left: 0, top: 0 },
49
+
50
+        zIndex = 0,
51
+
52
+        position = 'absolute',
53
+
54
+        startX, startY,
55
+
56
+        scaleX, scaleY,
57
+
58
+        resize,
59
+
60
+        minWidth, minHeight, maxWidth, maxHeight,
61
+
62
+        aspectRatio,
63
+
64
+        shown,
65
+
66
+        x1, y1, x2, y2,
67
+
68
+        selection = { x1: 0, y1: 0, x2: 0, y2: 0, width: 0, height: 0 },
69
+
70
+        docElem = document.documentElement,
71
+
72
+        ua = navigator.userAgent,
73
+
74
+        $p, d, i, o, w, h, adjusted;
75
+
76
+    function viewX(x) {
77
+        return x + imgOfs.left - parOfs.left;
78
+    }
79
+
80
+    function viewY(y) {
81
+        return y + imgOfs.top - parOfs.top;
82
+    }
83
+
84
+    function selX(x) {
85
+        return x - imgOfs.left + parOfs.left;
86
+    }
87
+
88
+    function selY(y) {
89
+        return y - imgOfs.top + parOfs.top;
90
+    }
91
+
92
+    function evX(event) {
93
+        return event.pageX - parOfs.left;
94
+    }
95
+
96
+    function evY(event) {
97
+        return event.pageY - parOfs.top;
98
+    }
99
+
100
+    function getSelection(noScale) {
101
+        var sx = noScale || scaleX, sy = noScale || scaleY;
102
+
103
+        return { x1: round(selection.x1 * sx),
104
+            y1: round(selection.y1 * sy),
105
+            x2: round(selection.x2 * sx),
106
+            y2: round(selection.y2 * sy),
107
+            width: round(selection.x2 * sx) - round(selection.x1 * sx),
108
+            height: round(selection.y2 * sy) - round(selection.y1 * sy) };
109
+    }
110
+
111
+    function setSelection(x1, y1, x2, y2, noScale) {
112
+        var sx = noScale || scaleX, sy = noScale || scaleY;
113
+
114
+        selection = {
115
+            x1: round(x1 / sx || 0),
116
+            y1: round(y1 / sy || 0),
117
+            x2: round(x2 / sx || 0),
118
+            y2: round(y2 / sy || 0)
119
+        };
120
+
121
+        selection.width = selection.x2 - selection.x1;
122
+        selection.height = selection.y2 - selection.y1;
123
+    }
124
+
125
+    function adjust() {
126
+        if (!imgLoaded || !$img.width())
127
+            return;
128
+
129
+        imgOfs = { left: round($img.offset().left), top: round($img.offset().top) };
130
+
131
+        imgWidth = $img.innerWidth();
132
+        imgHeight = $img.innerHeight();
133
+
134
+        imgOfs.top += ($img.outerHeight() - imgHeight) >> 1;
135
+        imgOfs.left += ($img.outerWidth() - imgWidth) >> 1;
136
+
137
+        minWidth = round(options.minWidth / scaleX) || 0;
138
+        minHeight = round(options.minHeight / scaleY) || 0;
139
+        maxWidth = round(min(options.maxWidth / scaleX || 1<<24, imgWidth));
140
+        maxHeight = round(min(options.maxHeight / scaleY || 1<<24, imgHeight));
141
+
142
+        if ($().jquery == '1.3.2' && position == 'fixed' &&
143
+            !docElem['getBoundingClientRect'])
144
+        {
145
+            imgOfs.top += max(document.body.scrollTop, docElem.scrollTop);
146
+            imgOfs.left += max(document.body.scrollLeft, docElem.scrollLeft);
147
+        }
148
+
149
+        parOfs = /absolute|relative/.test($parent.css('position')) ?
150
+            { left: round($parent.offset().left) - $parent.scrollLeft(),
151
+                top: round($parent.offset().top) - $parent.scrollTop() } :
152
+            position == 'fixed' ?
153
+                { left: $(document).scrollLeft(), top: $(document).scrollTop() } :
154
+                { left: 0, top: 0 };
155
+
156
+        left = viewX(0);
157
+        top = viewY(0);
158
+
159
+        if (selection.x2 > imgWidth || selection.y2 > imgHeight)
160
+            doResize();
161
+    }
162
+
163
+    function update(resetKeyPress) {
164
+        if (!shown) return;
165
+
166
+        $box.css({ left: viewX(selection.x1), top: viewY(selection.y1) })
167
+            .add($area).width(w = selection.width).height(h = selection.height);
168
+
169
+        $area.add($border).add($handles).css({ left: 0, top: 0 });
170
+
171
+        $border
172
+            .width(max(w - $border.outerWidth() + $border.innerWidth(), 0))
173
+            .height(max(h - $border.outerHeight() + $border.innerHeight(), 0));
174
+
175
+        $($outer[0]).css({ left: left, top: top,
176
+            width: selection.x1, height: imgHeight });
177
+        $($outer[1]).css({ left: left + selection.x1, top: top,
178
+            width: w, height: selection.y1 });
179
+        $($outer[2]).css({ left: left + selection.x2, top: top,
180
+            width: imgWidth - selection.x2, height: imgHeight });
181
+        $($outer[3]).css({ left: left + selection.x1, top: top + selection.y2,
182
+            width: w, height: imgHeight - selection.y2 });
183
+
184
+        w -= $handles.outerWidth();
185
+        h -= $handles.outerHeight();
186
+
187
+        switch ($handles.length) {
188
+        case 8:
189
+            $($handles[4]).css({ left: w >> 1 });
190
+            $($handles[5]).css({ left: w, top: h >> 1 });
191
+            $($handles[6]).css({ left: w >> 1, top: h });
192
+            $($handles[7]).css({ top: h >> 1 });
193
+        case 4:
194
+            $handles.slice(1,3).css({ left: w });
195
+            $handles.slice(2,4).css({ top: h });
196
+        }
197
+
198
+        if (resetKeyPress !== false) {
199
+            if ($.imgAreaSelect.onKeyPress != docKeyPress)
200
+                $(document).unbind($.imgAreaSelect.keyPress,
201
+                    $.imgAreaSelect.onKeyPress);
202
+
203
+            if (options.keys)
204
+                $(document)[$.imgAreaSelect.keyPress](
205
+                    $.imgAreaSelect.onKeyPress = docKeyPress);
206
+        }
207
+
208
+        if (msie && $border.outerWidth() - $border.innerWidth() == 2) {
209
+            $border.css('margin', 0);
210
+            setTimeout(function () { $border.css('margin', 'auto'); }, 0);
211
+        }
212
+    }
213
+
214
+    function doUpdate(resetKeyPress) {
215
+        adjust();
216
+        update(resetKeyPress);
217
+        x1 = viewX(selection.x1); y1 = viewY(selection.y1);
218
+        x2 = viewX(selection.x2); y2 = viewY(selection.y2);
219
+    }
220
+
221
+    function hide($elem, fn) {
222
+        options.fadeSpeed ? $elem.fadeOut(options.fadeSpeed, fn) : $elem.hide();
223
+
224
+    }
225
+
226
+    function areaMouseMove(event) {
227
+        var x = selX(evX(event)) - selection.x1,
228
+            y = selY(evY(event)) - selection.y1;
229
+
230
+        if (!adjusted) {
231
+            adjust();
232
+            adjusted = true;
233
+
234
+            $box.one('mouseout', function () { adjusted = false; });
235
+        }
236
+
237
+        resize = '';
238
+
239
+        if (options.resizable) {
240
+            if (y <= options.resizeMargin)
241
+                resize = 'n';
242
+            else if (y >= selection.height - options.resizeMargin)
243
+                resize = 's';
244
+            if (x <= options.resizeMargin)
245
+                resize += 'w';
246
+            else if (x >= selection.width - options.resizeMargin)
247
+                resize += 'e';
248
+        }
249
+
250
+        $box.css('cursor', resize ? resize + '-resize' :
251
+            options.movable ? 'move' : '');
252
+        if ($areaOpera)
253
+            $areaOpera.toggle();
254
+    }
255
+
256
+    function docMouseUp(event) {
257
+        $('body').css('cursor', '');
258
+        if (options.autoHide || selection.width * selection.height == 0)
259
+            hide($box.add($outer), function () { $(this).hide(); });
260
+
261
+        $(document).unbind('mousemove', selectingMouseMove);
262
+        $box.mousemove(areaMouseMove);
263
+
264
+        options.onSelectEnd(img, getSelection());
265
+    }
266
+
267
+    function areaMouseDown(event) {
268
+        if (event.which != 1) return false;
269
+
270
+        adjust();
271
+
272
+        if (resize) {
273
+            $('body').css('cursor', resize + '-resize');
274
+
275
+            x1 = viewX(selection[/w/.test(resize) ? 'x2' : 'x1']);
276
+            y1 = viewY(selection[/n/.test(resize) ? 'y2' : 'y1']);
277
+
278
+            $(document).mousemove(selectingMouseMove)
279
+                .one('mouseup', docMouseUp);
280
+            $box.unbind('mousemove', areaMouseMove);
281
+        }
282
+        else if (options.movable) {
283
+            startX = left + selection.x1 - evX(event);
284
+            startY = top + selection.y1 - evY(event);
285
+
286
+            $box.unbind('mousemove', areaMouseMove);
287
+
288
+            $(document).mousemove(movingMouseMove)
289
+                .one('mouseup', function () {
290
+                    options.onSelectEnd(img, getSelection());
291
+
292
+                    $(document).unbind('mousemove', movingMouseMove);
293
+                    $box.mousemove(areaMouseMove);
294
+                });
295
+        }
296
+        else
297
+            $img.mousedown(event);
298
+
299
+        return false;
300
+    }
301
+
302
+    function fixAspectRatio(xFirst) {
303
+        if (aspectRatio)
304
+            if (xFirst) {
305
+                x2 = max(left, min(left + imgWidth,
306
+                    x1 + abs(y2 - y1) * aspectRatio * (x2 > x1 || -1)));
307
+
308
+                y2 = round(max(top, min(top + imgHeight,
309
+                    y1 + abs(x2 - x1) / aspectRatio * (y2 > y1 || -1))));
310
+                x2 = round(x2);
311
+            }
312
+            else {
313
+                y2 = max(top, min(top + imgHeight,
314
+                    y1 + abs(x2 - x1) / aspectRatio * (y2 > y1 || -1)));
315
+                x2 = round(max(left, min(left + imgWidth,
316
+                    x1 + abs(y2 - y1) * aspectRatio * (x2 > x1 || -1))));
317
+                y2 = round(y2);
318
+            }
319
+    }
320
+
321
+    function doResize() {
322
+        x1 = min(x1, left + imgWidth);
323
+        y1 = min(y1, top + imgHeight);
324
+
325
+        if (abs(x2 - x1) < minWidth) {
326
+            x2 = x1 - minWidth * (x2 < x1 || -1);
327
+
328
+            if (x2 < left)
329
+                x1 = left + minWidth;
330
+            else if (x2 > left + imgWidth)
331
+                x1 = left + imgWidth - minWidth;
332
+        }
333
+
334
+        if (abs(y2 - y1) < minHeight) {
335
+            y2 = y1 - minHeight * (y2 < y1 || -1);
336
+
337
+            if (y2 < top)
338
+                y1 = top + minHeight;
339
+            else if (y2 > top + imgHeight)
340
+                y1 = top + imgHeight - minHeight;
341
+        }
342
+
343
+        x2 = max(left, min(x2, left + imgWidth));
344
+        y2 = max(top, min(y2, top + imgHeight));
345
+
346
+        fixAspectRatio(abs(x2 - x1) < abs(y2 - y1) * aspectRatio);
347
+
348
+        if (abs(x2 - x1) > maxWidth) {
349
+            x2 = x1 - maxWidth * (x2 < x1 || -1);
350
+            fixAspectRatio();
351
+        }
352
+
353
+        if (abs(y2 - y1) > maxHeight) {
354
+            y2 = y1 - maxHeight * (y2 < y1 || -1);
355
+            fixAspectRatio(true);
356
+        }
357
+
358
+        selection = { x1: selX(min(x1, x2)), x2: selX(max(x1, x2)),
359
+            y1: selY(min(y1, y2)), y2: selY(max(y1, y2)),
360
+            width: abs(x2 - x1), height: abs(y2 - y1) };
361
+
362
+        update();
363
+
364
+        options.onSelectChange(img, getSelection());
365
+    }
366
+
367
+    function selectingMouseMove(event) {
368
+        x2 = /w|e|^$/.test(resize) || aspectRatio ? evX(event) : viewX(selection.x2);
369
+        y2 = /n|s|^$/.test(resize) || aspectRatio ? evY(event) : viewY(selection.y2);
370
+
371
+        doResize();
372
+
373
+        return false;
374
+
375
+    }
376
+
377
+    function doMove(newX1, newY1) {
378
+        x2 = (x1 = newX1) + selection.width;
379
+        y2 = (y1 = newY1) + selection.height;
380
+
381
+        $.extend(selection, { x1: selX(x1), y1: selY(y1), x2: selX(x2),
382
+            y2: selY(y2) });
383
+
384
+        update();
385
+
386
+        options.onSelectChange(img, getSelection());
387
+    }
388
+
389
+    function movingMouseMove(event) {
390
+        x1 = max(left, min(startX + evX(event), left + imgWidth - selection.width));
391
+        y1 = max(top, min(startY + evY(event), top + imgHeight - selection.height));
392
+
393
+        doMove(x1, y1);
394
+
395
+        event.preventDefault();
396
+
397
+        return false;
398
+    }
399
+
400
+    function startSelection() {
401
+        $(document).unbind('mousemove', startSelection);
402
+        adjust();
403
+
404
+        x2 = x1;
405
+        y2 = y1;
406
+
407
+        doResize();
408
+
409
+        resize = '';
410
+
411
+        if (!$outer.is(':visible'))
412
+            $box.add($outer).hide().fadeIn(options.fadeSpeed||0);
413
+
414
+        shown = true;
415
+
416
+        $(document).unbind('mouseup', cancelSelection)
417
+            .mousemove(selectingMouseMove).one('mouseup', docMouseUp);
418
+        $box.unbind('mousemove', areaMouseMove);
419
+
420
+        options.onSelectStart(img, getSelection());
421
+    }
422
+
423
+    function cancelSelection() {
424
+        $(document).unbind('mousemove', startSelection)
425
+            .unbind('mouseup', cancelSelection);
426
+        hide($box.add($outer));
427
+
428
+        setSelection(selX(x1), selY(y1), selX(x1), selY(y1));
429
+
430
+        if (!(this instanceof $.imgAreaSelect)) {
431
+            options.onSelectChange(img, getSelection());
432
+            options.onSelectEnd(img, getSelection());
433
+        }
434
+    }
435
+
436
+    function imgMouseDown(event) {
437
+        if (event.which != 1 || $outer.is(':animated')) return false;
438
+
439
+        adjust();
440
+        startX = x1 = evX(event);
441
+        startY = y1 = evY(event);
442
+
443
+        $(document).mousemove(startSelection).mouseup(cancelSelection);
444
+
445
+        return false;
446
+    }
447
+
448
+    function windowResize() {
449
+        doUpdate(false);
450
+    }
451
+
452
+    function imgLoad() {
453
+        imgLoaded = true;
454
+
455
+        setOptions(options = $.extend({
456
+            classPrefix: 'imgareaselect',
457
+            movable: true,
458
+            parent: 'body',
459
+            resizable: true,
460
+            resizeMargin: 10,
461
+            onInit: function () {},
462
+            onSelectStart: function () {},
463
+            onSelectChange: function () {},
464
+            onSelectEnd: function () {}
465
+        }, options));
466
+
467
+        $box.add($outer).css({ visibility: '' });
468
+
469
+        if (options.show) {
470
+            shown = true;
471
+            adjust();
472
+            update();
473
+            $box.add($outer).hide().fadeIn(options.fadeSpeed||0);
474
+        }
475
+
476
+        setTimeout(function () { options.onInit(img, getSelection()); }, 0);
477
+    }
478
+
479
+    var docKeyPress = function(event) {
480
+        var k = options.keys, d, t, key = event.keyCode;
481
+
482
+        d = !isNaN(k.alt) && (event.altKey || event.originalEvent.altKey) ? k.alt :
483
+            !isNaN(k.ctrl) && event.ctrlKey ? k.ctrl :
484
+            !isNaN(k.shift) && event.shiftKey ? k.shift :
485
+            !isNaN(k.arrows) ? k.arrows : 10;
486
+
487
+        if (k.arrows == 'resize' || (k.shift == 'resize' && event.shiftKey) ||
488
+            (k.ctrl == 'resize' && event.ctrlKey) ||
489
+            (k.alt == 'resize' && (event.altKey || event.originalEvent.altKey)))
490
+        {
491
+            switch (key) {
492
+            case 37:
493
+                d = -d;
494
+            case 39:
495
+                t = max(x1, x2);
496
+                x1 = min(x1, x2);
497
+                x2 = max(t + d, x1);
498
+                fixAspectRatio();
499
+                break;
500
+            case 38:
501
+                d = -d;
502
+            case 40:
503
+                t = max(y1, y2);
504
+                y1 = min(y1, y2);
505
+                y2 = max(t + d, y1);
506
+                fixAspectRatio(true);
507
+                break;
508
+            default:
509
+                return;
510
+            }
511
+
512
+            doResize();
513
+        }
514
+        else {
515
+            x1 = min(x1, x2);
516
+            y1 = min(y1, y2);
517
+
518
+            switch (key) {
519
+            case 37:
520
+                doMove(max(x1 - d, left), y1);
521
+                break;
522
+            case 38:
523
+                doMove(x1, max(y1 - d, top));
524
+                break;
525
+            case 39:
526
+                doMove(x1 + min(d, imgWidth - selX(x2)), y1);
527
+                break;
528
+            case 40:
529
+                doMove(x1, y1 + min(d, imgHeight - selY(y2)));
530
+                break;
531
+            default:
532
+                return;
533
+            }
534
+        }
535
+
536
+        return false;
537
+    };
538
+
539
+    function styleOptions($elem, props) {
540
+        for (var option in props)
541
+            if (options[option] !== undefined)
542
+                $elem.css(props[option], options[option]);
543
+    }
544
+
545
+    function setOptions(newOptions) {
546
+        if (newOptions.parent)
547
+            ($parent = $(newOptions.parent)).append($box.add($outer));
548
+
549
+        $.extend(options, newOptions);
550
+
551
+        adjust();
552
+
553
+        if (newOptions.handles != null) {
554
+            $handles.remove();
555
+            $handles = $([]);
556
+
557
+            i = newOptions.handles ? newOptions.handles == 'corners' ? 4 : 8 : 0;
558
+
559
+            while (i--)
560
+                $handles = $handles.add(div());
561
+
562
+            $handles.addClass(options.classPrefix + '-handle').css({
563
+                position: 'absolute',
564
+                fontSize: 0,
565
+                zIndex: zIndex + 1 || 1
566
+            });
567
+
568
+            if (!parseInt($handles.css('width')) >= 0)
569
+                $handles.width(5).height(5);
570
+
571
+            if (o = options.borderWidth)
572
+                $handles.css({ borderWidth: o, borderStyle: 'solid' });
573
+
574
+            styleOptions($handles, { borderColor1: 'border-color',
575
+                borderColor2: 'background-color',
576
+                borderOpacity: 'opacity' });
577
+        }
578
+
579
+        scaleX = options.imageWidth / imgWidth || 1;
580
+        scaleY = options.imageHeight / imgHeight || 1;
581
+
582
+        if (newOptions.x1 != null) {
583
+            setSelection(newOptions.x1, newOptions.y1, newOptions.x2,
584
+                newOptions.y2);
585
+            newOptions.show = !newOptions.hide;
586
+        }
587
+
588
+        if (newOptions.keys)
589
+            options.keys = $.extend({ shift: 1, ctrl: 'resize' },
590
+                newOptions.keys);
591
+
592
+        $outer.addClass(options.classPrefix + '-outer');
593
+        $area.addClass(options.classPrefix + '-selection');
594
+        for (i = 0; i++ < 4;)
595
+            $($border[i-1]).addClass(options.classPrefix + '-border' + i);
596
+
597
+        styleOptions($area, { selectionColor: 'background-color',
598
+            selectionOpacity: 'opacity' });
599
+        styleOptions($border, { borderOpacity: 'opacity',
600
+            borderWidth: 'border-width' });
601
+        styleOptions($outer, { outerColor: 'background-color',
602
+            outerOpacity: 'opacity' });
603
+        if (o = options.borderColor1)
604
+            $($border[0]).css({ borderStyle: 'solid', borderColor: o });
605
+        if (o = options.borderColor2)
606
+            $($border[1]).css({ borderStyle: 'dashed', borderColor: o });
607
+
608
+        $box.append($area.add($border).add($areaOpera)).append($handles);
609
+
610
+        if (msie) {
611
+            if (o = ($outer.css('filter')||'').match(/opacity=(\d+)/))
612
+                $outer.css('opacity', o[1]/100);
613
+            if (o = ($border.css('filter')||'').match(/opacity=(\d+)/))
614
+                $border.css('opacity', o[1]/100);
615
+        }
616
+
617
+        if (newOptions.hide)
618
+            hide($box.add($outer));
619
+        else if (newOptions.show && imgLoaded) {
620
+            shown = true;
621
+            $box.add($outer).fadeIn(options.fadeSpeed||0);
622
+            doUpdate();
623
+        }
624
+
625
+        aspectRatio = (d = (options.aspectRatio || '').split(/:/))[0] / d[1];
626
+
627
+        $img.add($outer).unbind('mousedown', imgMouseDown);
628
+
629
+        if (options.disable || options.enable === false) {
630
+            $box.unbind('mousemove', areaMouseMove).unbind('mousedown', areaMouseDown);
631
+            $(window).unbind('resize', windowResize);
632
+        }
633
+        else {
634
+            if (options.enable || options.disable === false) {
635
+                if (options.resizable || options.movable)
636
+                    $box.mousemove(areaMouseMove).mousedown(areaMouseDown);
637
+
638
+                $(window).resize(windowResize);
639
+            }
640
+
641
+            if (!options.persistent)
642
+                $img.add($outer).mousedown(imgMouseDown);
643
+        }
644
+
645
+        options.enable = options.disable = undefined;
646
+    }
647
+
648
+    this.remove = function () {
649
+        setOptions({ disable: true });
650
+        $box.add($outer).remove();
651
+    };
652
+
653
+    this.getOptions = function () { return options; };
654
+
655
+    this.setOptions = setOptions;
656
+
657
+    this.getSelection = getSelection;
658
+
659
+    this.setSelection = setSelection;
660
+
661
+    this.cancelSelection = cancelSelection;
662
+
663
+    this.update = doUpdate;
664
+
665
+    var msie = (/msie ([\w.]+)/i.exec(ua)||[])[1],
666
+        opera = /opera/i.test(ua),
667
+        safari = /webkit/i.test(ua) && !/chrome/i.test(ua);
668
+
669
+    $p = $img;
670
+
671
+    while ($p.length) {
672
+        zIndex = max(zIndex,
673
+            !isNaN($p.css('z-index')) ? $p.css('z-index') : zIndex);
674
+        if ($p.css('position') == 'fixed')
675
+            position = 'fixed';
676
+
677
+        $p = $p.parent(':not(body)');
678
+    }
679
+
680
+    zIndex = options.zIndex || zIndex;
681
+
682
+    if (msie)
683
+        $img.attr('unselectable', 'on');
684
+
685
+    $.imgAreaSelect.keyPress = msie || safari ? 'keydown' : 'keypress';
686
+
687
+    if (opera)
688
+
689
+        $areaOpera = div().css({ width: '100%', height: '100%',
690
+            position: 'absolute', zIndex: zIndex + 2 || 2 });
691
+
692
+    $box.add($outer).css({ visibility: 'hidden', position: position,
693
+        overflow: 'hidden', zIndex: zIndex || '0' });
694
+    $box.css({ zIndex: zIndex + 2 || 2 });
695
+    $area.add($border).css({ position: 'absolute', fontSize: 0 });
696
+
697
+    img.complete || img.readyState == 'complete' || !$img.is('img') ?
698
+        imgLoad() : $img.one('load', imgLoad);
699
+
700
+    if (!imgLoaded && msie && msie >= 7)
701
+        img.src = img.src;
702
+};
703
+
704
+$.fn.imgAreaSelect = function (options) {
705
+    options = options || {};
706
+
707
+    this.each(function () {
708
+        if ($(this).data('imgAreaSelect')) {
709
+            if (options.remove) {
710
+                $(this).data('imgAreaSelect').remove();
711
+                $(this).removeData('imgAreaSelect');
712
+            }
713
+            else
714
+                $(this).data('imgAreaSelect').setOptions(options);
715
+        }
716
+        else if (!options.remove) {
717
+            if (options.enable === undefined && options.disable === undefined)
718
+                options.enable = true;
719
+
720
+            $(this).data('imgAreaSelect', new $.imgAreaSelect(this, options));
721
+        }
722
+    });
723
+
724
+    if (options.instance)
725
+        return $(this).data('imgAreaSelect');
726
+
727
+    return this;
728
+};
729
+
730
+})(jQuery);

+ 1
- 0
static/pygal/js/imgareaselect-js/jquery.imgareaselect.min.js
文件差异内容过多而无法显示
查看文件


+ 1
- 0
static/pygal/js/imgareaselect-js/jquery.imgareaselect.pack.js
文件差异内容过多而无法显示
查看文件


+ 4
- 0
static/pygal/js/jquery.min.js
文件差异内容过多而无法显示
查看文件


+ 108
- 0
static/pygal/js/video-js/font/VideoJS.svg 查看文件

@@ -0,0 +1,108 @@
1
+<?xml version="1.0" standalone="no"?> 
2
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
3
+<svg xmlns="http://www.w3.org/2000/svg">
4
+<defs>
5
+  <font id="VideoJS" horiz-adv-x="1792">
6
+    <font-face font-family="VideoJS"
7
+      units-per-em="1792" ascent="1792"
8
+      descent="0" />
9
+    <missing-glyph horiz-adv-x="0" />
10
+    <glyph glyph-name="play"
11
+      unicode="&#xF101;"
12
+      horiz-adv-x="1792" d=" M597.3333333333334 1418.6666666666665V373.3333333333333L1418.6666666666667 896z" />
13
+    <glyph glyph-name="play-circle"
14
+      unicode="&#xF102;"
15
+      horiz-adv-x="1792" d=" M746.6666666666667 560L1194.6666666666667 896L746.6666666666667 1232V560zM896 1642.6666666666667C483.4666666666667 1642.6666666666667 149.3333333333334 1308.5333333333333 149.3333333333334 896S483.4666666666667 149.3333333333333 896 149.3333333333333S1642.6666666666667 483.4666666666667 1642.6666666666667 896S1308.5333333333333 1642.6666666666667 896 1642.6666666666667zM896 298.6666666666665C566.72 298.6666666666665 298.6666666666667 566.7199999999998 298.6666666666667 896S566.72 1493.3333333333333 896 1493.3333333333333S1493.3333333333335 1225.28 1493.3333333333335 896S1225.2800000000002 298.6666666666665 896 298.6666666666665z" />
16
+    <glyph glyph-name="pause"
17
+      unicode="&#xF103;"
18
+      horiz-adv-x="1792" d=" M448 373.3333333333333H746.6666666666667V1418.6666666666665H448V373.3333333333333zM1045.3333333333335 1418.6666666666665V373.3333333333333H1344V1418.6666666666665H1045.3333333333335z" />
19
+    <glyph glyph-name="volume-mute"
20
+      unicode="&#xF104;"
21
+      horiz-adv-x="1792" d=" M1232 896C1232 1027.7866666666666 1155.8400000000001 1141.6533333333332 1045.3333333333335 1196.5333333333333V1031.52L1228.6399999999999 848.2133333333334C1230.88 863.8933333333334 1232 879.9466666666667 1232 896.0000000000001zM1418.6666666666667 896C1418.6666666666667 825.8133333333333 1403.3600000000001 759.7333333333333 1378.3466666666668 698.8799999999999L1491.466666666667 585.7599999999998C1540 678.72 1568 783.9999999999999 1568 896C1568 1215.5733333333333 1344.3733333333334 1482.88 1045.3333333333335 1550.8266666666666V1396.6399999999999C1261.1200000000001 1332.4266666666667 1418.6666666666667 1132.6933333333332 1418.6666666666667 896zM319.2000000000001 1568L224 1472.8L576.8 1120H224V672H522.6666666666667L896 298.6666666666665V800.8L1213.7066666666667 483.0933333333332C1163.68 444.6399999999999 1107.3066666666666 413.6533333333332 1045.3333333333335 394.9866666666665V240.7999999999998C1148 264.32 1241.7066666666667 311.3599999999997 1320.48 375.9466666666663L1472.8000000000002 224L1568 319.1999999999998L896 991.1999999999998L319.2000000000001 1568zM896 1493.3333333333333L739.9466666666667 1337.28L896 1181.2266666666667V1493.3333333333333z" />
22
+    <glyph glyph-name="volume-low"
23
+      unicode="&#xF105;"
24
+      horiz-adv-x="1792" d=" M522.6666666666667 1120V672H821.3333333333334L1194.6666666666667 298.6666666666665V1493.3333333333333L821.3333333333334 1120H522.6666666666667z" />
25
+    <glyph glyph-name="volume-mid"
26
+      unicode="&#xF106;"
27
+      horiz-adv-x="1792" d=" M1381.3333333333335 896C1381.3333333333335 1027.7866666666666 1305.1733333333334 1141.6533333333332 1194.6666666666667 1196.5333333333333V595.0933333333332C1305.1733333333334 650.3466666666666 1381.3333333333335 764.2133333333331 1381.3333333333335 896zM373.3333333333334 1120V672H672L1045.3333333333335 298.6666666666665V1493.3333333333333L672 1120H373.3333333333334z" />
28
+    <glyph glyph-name="volume-high"
29
+      unicode="&#xF107;"
30
+      horiz-adv-x="1792" d=" M224 1120V672H522.6666666666667L896 298.6666666666665V1493.3333333333333L522.6666666666667 1120H224zM1232 896C1232 1027.7866666666666 1155.8400000000001 1141.6533333333332 1045.3333333333335 1196.5333333333333V595.0933333333332C1155.8400000000001 650.3466666666666 1232 764.2133333333331 1232 896zM1045.3333333333335 1550.8266666666666V1396.6399999999999C1261.1200000000001 1332.4266666666667 1418.6666666666667 1132.6933333333332 1418.6666666666667 896S1261.1200000000001 459.5733333333333 1045.3333333333335 395.3600000000002V241.1733333333332C1344.3733333333334 309.1199999999999 1568 576.0533333333333 1568 896S1344.3733333333334 1482.88 1045.3333333333335 1550.8266666666666z" />
31
+    <glyph glyph-name="fullscreen-enter"
32
+      unicode="&#xF108;"
33
+      horiz-adv-x="1792" d=" M522.6666666666667 746.6666666666665H373.3333333333334V373.3333333333333H746.6666666666667V522.6666666666665H522.6666666666667V746.6666666666665zM373.3333333333334 1045.3333333333333H522.6666666666667V1269.3333333333333H746.6666666666667V1418.6666666666665H373.3333333333334V1045.3333333333333zM1269.3333333333335 522.6666666666665H1045.3333333333335V373.3333333333333H1418.6666666666667V746.6666666666665H1269.3333333333335V522.6666666666665zM1045.3333333333335 1418.6666666666665V1269.3333333333333H1269.3333333333335V1045.3333333333333H1418.6666666666667V1418.6666666666665H1045.3333333333335z" />
34
+    <glyph glyph-name="fullscreen-exit"
35
+      unicode="&#xF109;"
36
+      horiz-adv-x="1792" d=" M373.3333333333334 597.3333333333333H597.3333333333334V373.3333333333333H746.6666666666667V746.6666666666665H373.3333333333334V597.3333333333333zM597.3333333333334 1194.6666666666665H373.3333333333334V1045.3333333333333H746.6666666666667V1418.6666666666665H597.3333333333334V1194.6666666666665zM1045.3333333333335 373.3333333333333H1194.6666666666667V597.3333333333333H1418.6666666666667V746.6666666666665H1045.3333333333335V373.3333333333333zM1194.6666666666667 1194.6666666666665V1418.6666666666665H1045.3333333333335V1045.3333333333333H1418.6666666666667V1194.6666666666665H1194.6666666666667z" />
37
+    <glyph glyph-name="square"
38
+      unicode="&#xF10A;"
39
+      horiz-adv-x="1792" d=" M1344 1493.3333333333333H448C365.4933333333334 1493.3333333333333 298.6666666666667 1426.5066666666667 298.6666666666667 1344V448C298.6666666666667 365.4933333333331 365.4933333333334 298.6666666666665 448 298.6666666666665H1344C1426.506666666667 298.6666666666665 1493.3333333333335 365.4933333333331 1493.3333333333335 448V1344C1493.3333333333335 1426.5066666666667 1426.506666666667 1493.3333333333333 1344 1493.3333333333333zM1344 448H448V1344H1344V448z" />
40
+    <glyph glyph-name="spinner"
41
+      unicode="&#xF10B;"
42
+      horiz-adv-x="1792" d=" M701.8666666666668 1008L1057.6533333333334 1624.3733333333334C1005.7600000000002 1635.9466666666667 951.6266666666666 1642.6666666666667 896 1642.6666666666667C716.8000000000001 1642.6666666666667 552.9066666666668 1579.5733333333333 424.1066666666667 1474.2933333333333L697.76 1000.5333333333334L701.8666666666666 1008zM1608.32 1120C1539.6266666666666 1338.4 1373.1200000000001 1512.7466666666667 1160.6933333333332 1593.3866666666668L887.4133333333334 1120H1608.32zM1627.7333333333336 1045.3333333333333H1068.48L1090.1333333333334 1008L1445.92 392C1567.6266666666668 524.9066666666668 1642.6666666666667 701.4933333333333 1642.6666666666667 896C1642.6666666666667 947.1466666666666 1637.44 997.1733333333332 1627.7333333333336 1045.3333333333333zM637.2800000000001 896L346.08 1400C224.3733333333333 1267.0933333333332 149.3333333333334 1090.5066666666667 149.3333333333334 896C149.3333333333334 844.8533333333332 154.56 794.8266666666666 164.2666666666667 746.6666666666665H723.5200000000001L637.2800000000002 896zM183.68 672C252.3733333333334 453.5999999999999 418.88 279.2533333333334 631.3066666666667 198.6133333333332L904.5866666666668 672H183.68zM1025.1733333333334 672L733.9733333333334 167.6266666666666C786.24 156.0533333333333 840.3733333333334 149.3333333333333 896 149.3333333333333C1075.2 149.3333333333333 1239.0933333333332 212.4266666666665 1367.8933333333334 317.7066666666665L1094.24 791.4666666666666L1025.1733333333334 672z" />
43
+    <glyph glyph-name="subtitles"
44
+      unicode="&#xF10C;"
45
+      horiz-adv-x="1792" d=" M1493.3333333333335 1493.3333333333333H298.6666666666667C216.16 1493.3333333333333 149.3333333333334 1426.5066666666667 149.3333333333334 1344V448C149.3333333333334 365.4933333333331 216.16 298.6666666666665 298.6666666666667 298.6666666666665H1493.3333333333335C1575.8400000000001 298.6666666666665 1642.6666666666667 365.4933333333331 1642.6666666666667 448V1344C1642.6666666666667 1426.5066666666667 1575.8400000000001 1493.3333333333333 1493.3333333333335 1493.3333333333333zM298.6666666666667 896H597.3333333333334V746.6666666666665H298.6666666666667V896zM1045.3333333333335 448H298.6666666666667V597.3333333333333H1045.3333333333335V448zM1493.3333333333335 448H1194.6666666666667V597.3333333333333H1493.3333333333335V448zM1493.3333333333335 746.6666666666665H746.6666666666667V896H1493.3333333333335V746.6666666666665z" />
46
+    <glyph glyph-name="captions"
47
+      unicode="&#xF10D;"
48
+      horiz-adv-x="1792" d=" M1418.6666666666667 1493.3333333333333H373.3333333333334C290.8266666666667 1493.3333333333333 224 1426.5066666666667 224 1344V448C224 365.4933333333331 290.8266666666667 298.6666666666665 373.3333333333334 298.6666666666665H1418.6666666666667C1501.1733333333334 298.6666666666665 1568 365.4933333333331 1568 448V1344C1568 1426.5066666666667 1501.1733333333334 1493.3333333333333 1418.6666666666667 1493.3333333333333zM821.3333333333334 970.6666666666666H709.3333333333334V1008H560V783.9999999999999H709.3333333333334V821.3333333333333H821.3333333333334V746.6666666666665C821.3333333333334 705.5999999999999 788.1066666666667 672 746.6666666666667 672H522.6666666666667C481.2266666666667 672 448 705.5999999999999 448 746.6666666666665V1045.3333333333333C448 1086.4 481.2266666666667 1120 522.6666666666667 1120H746.6666666666667C788.1066666666667 1120 821.3333333333334 1086.4 821.3333333333334 1045.3333333333333V970.6666666666666zM1344 970.6666666666666H1232V1008H1082.6666666666667V783.9999999999999H1232V821.3333333333333H1344V746.6666666666665C1344 705.5999999999999 1310.7733333333333 672 1269.3333333333335 672H1045.3333333333335C1003.8933333333334 672 970.6666666666669 705.5999999999999 970.6666666666669 746.6666666666665V1045.3333333333333C970.6666666666669 1086.4 1003.8933333333334 1120 1045.3333333333335 1120H1269.3333333333335C1310.7733333333333 1120 1344 1086.4 1344 1045.3333333333333V970.6666666666666z" />
49
+    <glyph glyph-name="chapters"
50
+      unicode="&#xF10E;"
51
+      horiz-adv-x="1792" d=" M224 821.3333333333333H373.3333333333334V970.6666666666666H224V821.3333333333333zM224 522.6666666666665H373.3333333333334V672H224V522.6666666666665zM224 1120H373.3333333333334V1269.3333333333333H224V1120zM522.6666666666667 821.3333333333333H1568V970.6666666666666H522.6666666666667V821.3333333333333zM522.6666666666667 522.6666666666665H1568V672H522.6666666666667V522.6666666666665zM522.6666666666667 1269.3333333333333V1120H1568V1269.3333333333333H522.6666666666667z" />
52
+    <glyph glyph-name="share"
53
+      unicode="&#xF10F;"
54
+      horiz-adv-x="1792" d=" M1344 590.9866666666665C1287.2533333333333 590.9866666666665 1236.1066666666668 568.9599999999998 1197.2800000000002 533.4933333333331L665.2800000000001 843.7333333333333C669.3866666666667 860.5333333333333 672 878.08 672 896S669.3866666666667 931.4666666666666 665.2800000000001 948.2666666666667L1191.68 1255.52C1231.6266666666668 1218.1866666666665 1285.0133333333335 1195.04 1344 1195.04C1467.5733333333335 1195.04 1568 1295.4666666666665 1568 1419.04S1467.5733333333335 1643.04 1344 1643.04S1120 1542.6133333333332 1120 1419.04C1120 1401.12 1122.6133333333335 1383.5733333333333 1126.72 1366.773333333333L600.3199999999999 1059.5199999999998C560.3733333333333 1096.853333333333 506.9866666666666 1119.9999999999998 448 1119.9999999999998C324.4266666666666 1119.9999999999998 224 1019.5733333333332 224 895.9999999999998S324.4266666666666 671.9999999999998 448 671.9999999999998C506.9866666666666 671.9999999999998 560.3733333333333 695.1466666666665 600.3199999999999 732.4799999999998L1132.32 422.2399999999998C1128.5866666666666 406.5599999999997 1126.3466666666666 390.133333333333 1126.3466666666666 373.3333333333331C1126.3466666666666 253.1199999999997 1223.7866666666669 155.6799999999996 1344 155.6799999999996S1561.6533333333334 253.1199999999997 1561.6533333333334 373.3333333333331S1464.2133333333334 590.9866666666662 1344 590.9866666666662z" />
55
+    <glyph glyph-name="cog"
56
+      unicode="&#xF110;"
57
+      horiz-adv-x="1792" d=" M1450.7733333333333 823.1999999999999C1453.76 847.0933333333334 1456 871.3599999999999 1456 896S1453.76 944.9066666666666 1450.7733333333333 968.8L1608.6933333333336 1092.3733333333332C1622.8800000000003 1103.5733333333333 1626.986666666667 1123.7333333333331 1617.6533333333336 1140.1599999999999L1468.3200000000004 1398.8799999999999C1458.986666666667 1414.9333333333334 1439.5733333333335 1421.6533333333332 1422.7733333333338 1414.9333333333334L1236.8533333333337 1339.8933333333332C1198.4000000000003 1369.3866666666668 1156.2133333333338 1394.3999999999999 1110.6666666666672 1413.44L1082.6666666666667 1611.3066666666666C1079.3066666666668 1628.8533333333332 1064 1642.6666666666667 1045.3333333333335 1642.6666666666667H746.6666666666667C728 1642.6666666666667 712.6933333333334 1628.8533333333332 709.7066666666668 1611.3066666666666L681.7066666666668 1413.44C636.1600000000002 1394.4 593.9733333333335 1369.76 555.5200000000001 1339.8933333333332L369.6 1414.9333333333334C352.8000000000001 1421.28 333.3866666666667 1414.9333333333334 324.0533333333334 1398.88L174.72 1140.1599999999999C165.3866666666667 1124.1066666666666 169.4933333333334 1103.9466666666667 183.68 1092.3733333333332L341.2266666666667 968.8C338.2400000000001 944.9066666666666 336 920.64 336 896S338.2400000000001 847.0933333333334 341.2266666666667 823.1999999999999L183.68 699.6266666666668C169.4933333333334 688.4266666666667 165.3866666666667 668.2666666666667 174.72 651.8399999999999L324.0533333333334 393.1199999999999C333.3866666666667 377.0666666666666 352.8 370.3466666666666 369.6 377.0666666666666L555.5200000000001 452.1066666666666C593.9733333333334 422.6133333333333 636.16 397.5999999999999 681.7066666666668 378.56L709.7066666666668 180.6933333333334C712.6933333333334 163.1466666666668 728 149.3333333333333 746.6666666666667 149.3333333333333H1045.3333333333335C1064 149.3333333333333 1079.3066666666668 163.1466666666665 1082.2933333333333 180.6933333333334L1110.2933333333333 378.56C1155.84 397.5999999999999 1198.0266666666666 422.24 1236.48 452.1066666666666L1422.3999999999999 377.0666666666666C1439.2 370.7199999999998 1458.6133333333332 377.0666666666666 1467.9466666666665 393.1199999999999L1617.2799999999997 651.8399999999999C1626.6133333333332 667.8933333333332 1622.5066666666664 688.0533333333333 1608.3199999999997 699.6266666666668L1450.773333333333 823.1999999999999zM896 634.6666666666665C751.52 634.6666666666665 634.6666666666667 751.52 634.6666666666667 896S751.52 1157.3333333333333 896 1157.3333333333333S1157.3333333333335 1040.48 1157.3333333333335 896S1040.48 634.6666666666665 896 634.6666666666665z" />
58
+    <glyph glyph-name="circle"
59
+      unicode="&#xF111;"
60
+      horiz-adv-x="1792" d=" M149.3333333333334 896C149.3333333333334 483.6273867930074 483.6273867930075 149.3333333333333 896 149.3333333333333C1308.3726132069926 149.3333333333333 1642.6666666666667 483.6273867930074 1642.6666666666667 896C1642.6666666666667 1308.3726132069926 1308.3726132069926 1642.6666666666667 896 1642.6666666666667C483.6273867930075 1642.6666666666667 149.3333333333334 1308.3726132069926 149.3333333333334 896z" />
61
+    <glyph glyph-name="circle-outline"
62
+      unicode="&#xF112;"
63
+      horiz-adv-x="1792" d=" M896 1642.6666666666667C483.4666666666667 1642.6666666666667 149.3333333333334 1308.5333333333333 149.3333333333334 896S483.4666666666667 149.3333333333333 896 149.3333333333333S1642.6666666666667 483.4666666666667 1642.6666666666667 896S1308.5333333333333 1642.6666666666667 896 1642.6666666666667zM896 298.6666666666665C566.72 298.6666666666665 298.6666666666667 566.7199999999998 298.6666666666667 896S566.72 1493.3333333333333 896 1493.3333333333333S1493.3333333333335 1225.28 1493.3333333333335 896S1225.2800000000002 298.6666666666665 896 298.6666666666665z" />
64
+    <glyph glyph-name="circle-inner-circle"
65
+      unicode="&#xF113;"
66
+      horiz-adv-x="1792" d=" M896 1642.6666666666667C484.2133333333334 1642.6666666666667 149.3333333333334 1307.7866666666666 149.3333333333334 896S484.2133333333334 149.3333333333333 896 149.3333333333333S1642.6666666666667 484.2133333333331 1642.6666666666667 896S1307.7866666666669 1642.6666666666667 896 1642.6666666666667zM896 298.6666666666665C566.72 298.6666666666665 298.6666666666667 566.7199999999998 298.6666666666667 896S566.72 1493.3333333333333 896 1493.3333333333333S1493.3333333333335 1225.28 1493.3333333333335 896S1225.2800000000002 298.6666666666665 896 298.6666666666665zM1120 896C1120 772.4266666666666 1019.5733333333334 672 896 672S672 772.4266666666666 672 896S772.4266666666667 1120 896 1120S1120 1019.5733333333332 1120 896z" />
67
+    <glyph glyph-name="hd"
68
+      unicode="&#xF114;"
69
+      horiz-adv-x="1792" d=" M1418.6666666666667 1568H373.3333333333334C290.4533333333333 1568 224 1500.8 224 1418.6666666666665V373.3333333333333C224 291.1999999999998 290.4533333333334 224 373.3333333333334 224H1418.6666666666667C1500.8000000000002 224 1568 291.1999999999998 1568 373.3333333333333V1418.6666666666665C1568 1500.8 1500.8000000000002 1568 1418.6666666666667 1568zM821.3333333333334 672H709.3333333333334V821.3333333333333H560V672H448V1120H560V933.3333333333331H709.3333333333334V1120H821.3333333333334V672zM970.6666666666669 1120H1269.3333333333335C1310.4 1120 1344 1086.4 1344 1045.3333333333333V746.6666666666665C1344 705.5999999999999 1310.4 672 1269.3333333333335 672H970.6666666666669V1120zM1082.6666666666667 783.9999999999999H1232V1008H1082.6666666666667V783.9999999999999z" />
70
+    <glyph glyph-name="cancel"
71
+      unicode="&#xF115;"
72
+      horiz-adv-x="1792" d=" M896 1642.6666666666667C483.4666666666667 1642.6666666666667 149.3333333333334 1308.5333333333333 149.3333333333334 896S483.4666666666667 149.3333333333333 896 149.3333333333333S1642.6666666666667 483.4666666666667 1642.6666666666667 896S1308.5333333333333 1642.6666666666667 896 1642.6666666666667zM1269.3333333333335 628.3199999999999L1163.68 522.6666666666665L896 790.3466666666667L628.3199999999999 522.6666666666665L522.6666666666667 628.3199999999999L790.3466666666668 896L522.6666666666667 1163.68L628.3199999999999 1269.3333333333333L896 1001.6533333333332L1163.68 1269.3333333333333L1269.3333333333335 1163.68L1001.6533333333334 896L1269.3333333333335 628.3199999999999z" />
73
+    <glyph glyph-name="replay"
74
+      unicode="&#xF116;"
75
+      horiz-adv-x="1792" d=" M896 1418.6666666666665V1717.3333333333333L522.6666666666667 1344L896 970.6666666666666V1269.3333333333333C1143.52 1269.3333333333333 1344 1068.8533333333332 1344 821.3333333333333S1143.52 373.3333333333333 896 373.3333333333333S448 573.813333333333 448 821.3333333333333H298.6666666666667C298.6666666666667 491.3066666666664 565.9733333333334 224 896 224S1493.3333333333335 491.3066666666664 1493.3333333333335 821.3333333333333S1226.0266666666669 1418.6666666666665 896 1418.6666666666665z" />
76
+    <glyph glyph-name="facebook"
77
+      unicode="&#xF117;"
78
+      horiz-adv-x="1792" d=" M1343 1780V1516H1186Q1100 1516 1070 1480T1040 1372V1183H1333L1294 887H1040V128H734V887H479V1183H734V1401Q734 1587 838 1689.5T1115 1792Q1262 1792 1343 1780z" />
79
+    <glyph glyph-name="gplus"
80
+      unicode="&#xF118;"
81
+      horiz-adv-x="1792" d=" M799 996Q799 960 831 925.5T908.5 857.5T999 784T1076 680T1108 538Q1108 448 1060 365Q988 243 849 185.5T551 128Q419 128 304.5 169.5T133 307Q96 367 96 438Q96 519 140.5 588T259 703Q390 785 663 803Q631 845 615.5 877T600 950Q600 986 621 1035Q575 1031 553 1031Q405 1031 303.5 1127.5T202 1372Q202 1454 238 1531T337 1662Q414 1728 519.5 1760T737 1792H1155L1017 1704H886Q960 1641 998 1571T1036 1411Q1036 1339 1011.5 1281.5T952.5 1188.5T883 1123.5T823.5 1062T799 996zM653 1092Q691 1092 731 1108.5T797 1152Q850 1209 850 1311Q850 1369 833 1436T784.5 1565.5T700 1669T583 1710Q541 1710 500.5 1690.5T435 1638Q388 1579 388 1478Q388 1432 398 1380.5T429.5 1277.5T481.5 1185T556.5 1118T653 1092zM655 219Q713 219 766.5 232T865.5 271T938.5 344T966 453Q966 478 959 502T944.5 544T917.5 585.5T888 620.5T849.5 655T813 684T771.5 714T735 740Q719 742 687 742Q634 742 582 735T474.5 710T377.5 664T309 589.5T282 484Q282 414 317 360.5T408.5 277.5T527.5 233.5T655 219zM1465 1095H1678V987H1465V768H1360V987H1148V1095H1360V1312H1465V1095z" />
82
+    <glyph glyph-name="linkedin"
83
+      unicode="&#xF119;"
84
+      horiz-adv-x="1792" d=" M477 1167V176H147V1167H477zM498 1473Q499 1400 447.5 1351T312 1302H310Q228 1302 178 1351T128 1473Q128 1547 179.5 1595.5T314 1644T447 1595.5T498 1473zM1664 744V176H1335V706Q1335 811 1294.5 870.5T1168 930Q1105 930 1062.5 895.5T999 810Q988 780 988 729V176H659Q661 575 661 823T660 1119L659 1167H988V1023H986Q1006 1055 1027 1079T1083.5 1131T1170.5 1174.5T1285 1190Q1456 1190 1560 1076.5T1664 744z" />
85
+    <glyph glyph-name="twitter"
86
+      unicode="&#xF11A;"
87
+      horiz-adv-x="1792" d=" M1684 1384Q1617 1286 1522 1217Q1523 1203 1523 1175Q1523 1045 1485 915.5T1369.5 667T1185 456.5T927 310.5T604 256Q333 256 108 401Q143 397 186 397Q411 397 587 535Q482 537 399 599.5T285 759Q318 754 346 754Q389 754 431 765Q319 788 245.5 876.5T172 1082V1086Q240 1048 318 1045Q252 1089 213 1160T174 1314Q174 1402 218 1477Q339 1328 512.5 1238.5T884 1139Q876 1177 876 1213Q876 1347 970.5 1441.5T1199 1536Q1339 1536 1435 1434Q1544 1455 1640 1512Q1603 1397 1498 1334Q1591 1344 1684 1384z" />
88
+    <glyph glyph-name="tumblr"
89
+      unicode="&#xF11B;"
90
+      horiz-adv-x="1792" d=" M1328 463L1408 226Q1385 191 1297 160T1120 128Q1016 126 929.5 154T787 228T692 334T636.5 454T620 572V1116H452V1331Q524 1357 581 1400.5T672 1490.5T730 1592.5T764 1691.5T779 1780Q780 1785 783.5 1788.5T791 1792H1035V1368H1368V1116H1034V598Q1034 568 1040.5 542T1063 489.5T1112.5 448T1194 434Q1272 436 1328 463z" />
91
+    <glyph glyph-name="pinterest"
92
+      unicode="&#xF11C;"
93
+      horiz-adv-x="1792" d=" M1664 896Q1664 687 1561 510.5T1281.5 231T896 128Q785 128 678 160Q737 253 756 324Q765 358 810 535Q830 496 883 467.5T997 439Q1118 439 1213 507.5T1360 696T1412 966Q1412 1080 1352.5 1180T1180 1343T925 1406Q820 1406 729 1377T574.5 1300T465.5 1189.5T398.5 1060T377 926Q377 822 417 743T534 632Q564 620 572 652Q574 659 580 683T588 713Q594 736 577 756Q526 817 526 907Q526 1058 630.5 1166.5T904 1275Q1055 1275 1139.5 1193T1224 980Q1224 810 1155.5 691T980 572Q919 572 882 615.5T859 720Q867 755 885.5 813.5T915.5 916.5T927 992Q927 1042 900 1075T823 1108Q761 1108 718 1051T675 909Q675 836 700 787L601 369Q584 299 588 192Q382 283 255 473T128 896Q128 1105 231 1281.5T510.5 1561T896 1664T1281.5 1561T1561 1281.5T1664 896z" />
94
+    <glyph glyph-name="audio-description"
95
+      unicode="&#xF11D;"
96
+      horiz-adv-x="1792" d=" M795.5138904615 457.270933L795.5138904615 1221.5248286325C971.84576475 1225.085121904 1107.39330415 1232.12360523 1207.223857 1161.5835220499998C1303.033991 1093.8857027 1377.7922305 962.20560625 1364.3373135 792.9476205000001C1350.102593 613.9029365000001 1219.6655764999998 463.4600215 1050.12389545 448.2843645000001C965.8259268 440.7398275000001 798.21890505 448.2843645000001 798.21890505 448.2843645000001C798.21890505 448.2843645000001 795.2791410655 453.016494 795.5138904615 457.270933M966.1564647 649.0863960000001C1076.16084135 644.6767075 1152.385591 707.3020429999999 1163.8910079999998 807.9351875C1179.2994744999999 942.71878505 1089.73043585 1030.3691748 960.74508635 1020.7227954L960.74508635 658.08043C960.6196169500002 652.9482330000001 962.7606933 650.3134680000001 966.1564647 649.0863960000001 M1343.2299685 457.3517725000002C1389.9059734 444.3690160000001 1404.0840274999998 496.0596970000001 1424.48294065 532.2791494999999C1469.0084255 611.2788500000001 1502.5101322 712.8584189999999 1503.0416912 828.9881705C1503.8147453000001 995.5680973 1438.8404296 1117.7973688000002 1378.4383305 1200.62456881045L1348.652139905 1200.62456881045C1346.6001063899998 1187.06858424 1356.44474056 1175.024791325 1362.18395859 1164.6588891000001C1408.2649952 1081.49431985 1450.96645015 966.7230041 1451.57490975 834.9817034999999C1452.27106325 683.8655425000002 1402.00636065 557.5072264999999 1343.2299685 457.3517725000002 M1488.0379675 457.3517725000002C1534.7139723999999 444.3690160000001 1548.8825828 496.0671625 1569.29093965 532.2791494999999C1613.8164245 611.2788500000001 1647.3113856500001 712.8584189999999 1647.8496902000002 828.9881705C1648.6227442999998 995.5680973 1583.6484286 1117.7973688000002 1523.2463295 1200.62456881045L1493.460138905 1200.62456881045C1491.40810539 1187.06858424 1501.250041305 1175.021805755 1506.9919575899999 1164.6588891000001C1553.0729942 1081.49431985 1595.7757984 966.7230041 1596.3829087499998 834.9817034999999C1597.07906225 683.8655425000002 1546.8143596500001 557.5072264999999 1488.0379675 457.3517725000002 M1631.9130380000001 457.3517725000002C1678.5890429 444.3690160000001 1692.7576533 496.0671625 1713.1660101500001 532.2791494999999C1757.691495 611.2788500000001 1791.1864561500001 712.8584189999999 1791.7247607000002 828.9881705C1792.4978148 995.5680973 1727.5234991000002 1117.7973688000002 1667.1214 1200.62456881045L1637.3352094050001 1200.62456881045C1635.28317589 1187.06858424 1645.1251118050002 1175.02329854 1650.86702809 1164.6588891000001C1696.9480647 1081.49431985 1739.64951965 966.7230041 1740.25797925 834.9817034999999C1740.95413275 683.8655425000002 1690.6894301500001 557.5072264999999 1631.9130380000001 457.3517725000002 M15.66796875 451.481947L254.03034755 451.481947L319.0356932 551.1747990000001L543.6261075 551.6487970000001C543.6261075 551.6487970000001 543.8541115 483.7032095 543.8541115 451.481947L714.4993835 451.481947L714.4993835 1230.9210795L508.643051 1230.9210795C488.8579955 1197.5411595 15.66796875 451.481947 15.66796875 451.481947L15.66796875 451.481947zM550.0048155000001 959.9708615L550.0048155000001 710.916297L408.4199 711.8642895L550.0048155000001 959.9708615L550.0048155000001 959.9708615z" />
97
+    <glyph glyph-name="audio"
98
+      unicode="&#xF11E;"
99
+      horiz-adv-x="1792" d=" M896 1717.3333333333333C524.9066666666668 1717.3333333333333 224 1416.4266666666667 224 1045.3333333333333V522.6666666666665C224 399.0933333333333 324.4266666666667 298.6666666666665 448 298.6666666666665H672V896H373.3333333333334V1045.3333333333333C373.3333333333334 1333.92 607.4133333333334 1568 896 1568S1418.6666666666667 1333.92 1418.6666666666667 1045.3333333333333V896H1120V298.6666666666665H1344C1467.5733333333335 298.6666666666665 1568 399.0933333333333 1568 522.6666666666665V1045.3333333333333C1568 1416.4266666666667 1267.0933333333332 1717.3333333333333 896 1717.3333333333333z" />
100
+    <glyph glyph-name="next-item"
101
+      unicode="&#xF11F;"
102
+      horiz-adv-x="1792" d=" M448 448L1082.6666666666667 896L448 1344V448zM1194.6666666666667 1344V448H1344V1344H1194.6666666666667z" />
103
+    <glyph glyph-name="previous-item"
104
+      unicode="&#xF120;"
105
+      horiz-adv-x="1792" d=" M448 1344H597.3333333333334V448H448zM709.3333333333334 896L1344 448V1344z" />
106
+  </font>
107
+</defs>
108
+</svg>

二进制
static/pygal/js/video-js/font/VideoJS.ttf 查看文件


二进制
static/pygal/js/video-js/font/VideoJS.woff 查看文件


+ 34
- 0
static/pygal/js/video-js/lang/ar.js 查看文件

@@ -0,0 +1,34 @@
1
+videojs.addLanguage("ar",{
2
+ "Play": "تشغيل",
3
+ "Pause": "إيقاف",
4
+ "Current Time": "الوقت الحالي",
5
+ "Duration": "مدة",
6
+ "Remaining Time": "الوقت المتبقي",
7
+ "Stream Type": "نوع التيار",
8
+ "LIVE": "مباشر",
9
+ "Loaded": "تم التحميل",
10
+ "Progress": "التقدم",
11
+ "Fullscreen": "ملء الشاشة",
12
+ "Non-Fullscreen": "تعطيل ملء الشاشة",
13
+ "Mute": "صامت",
14
+ "Unmute": "غير الصامت",
15
+ "Playback Rate": "معدل التشغيل",
16
+ "Subtitles": "الترجمة",
17
+ "subtitles off": "إيقاف الترجمة",
18
+ "Captions": "التعليقات",
19
+ "captions off": "إيقاف التعليقات",
20
+ "Chapters": "فصول",
21
+ "You aborted the media playback": "لقد ألغيت تشغيل الفيديو",
22
+ "A network error caused the media download to fail part-way.": "تسبب خطأ في الشبكة بفشل تحميل الفيديو بالكامل.",
23
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "لا يمكن تحميل الفيديو بسبب فشل في الخادوم أو الشبكة ، أو فشل بسبب عدم إمكانية قراءة تنسيق الفيديو.",
24
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "تم إيقاف تشغيل الفيديو بسبب مشكلة فساد أو لأن الفيديو المستخدم يستخدم ميزات غير مدعومة من متصفحك.",
25
+ "No compatible source was found for this media.": "فشل العثور على أي مصدر متوافق مع هذا الفيديو.",
26
+ "Play Video": "تشغيل الفيديو",
27
+ "Close": "أغلق",
28
+ "Modal Window": "نافذة مشروطة",
29
+ "This is a modal window": "هذه نافذة مشروطة",
30
+ "This modal can be closed by pressing the Escape key or activating the close button.": "يمكن غلق هذه النافذة المشروطة عن طريق الضغط على زر الخروج أو تفعيل زر الإغلاق",
31
+ ", opens captions settings dialog": ", تفتح نافذة  خيارات التعليقات",
32
+ ", opens subtitles settings dialog": ", تفتح نافذة  خيارات الترجمة",
33
+ ", selected": ", مختار"
34
+});

+ 26
- 0
static/pygal/js/video-js/lang/ba.js 查看文件

@@ -0,0 +1,26 @@
1
+videojs.addLanguage("ba",{
2
+ "Play": "Pusti",
3
+ "Pause": "Pauza",
4
+ "Current Time": "Trenutno vrijeme",
5
+ "Duration": "Vrijeme trajanja",
6
+ "Remaining Time": "Preostalo vrijeme",
7
+ "Stream Type": "Način strimovanja",
8
+ "LIVE": "UŽIVO",
9
+ "Loaded": "Učitan",
10
+ "Progress": "Progres",
11
+ "Fullscreen": "Puni ekran",
12
+ "Non-Fullscreen": "Mali ekran",
13
+ "Mute": "Prigušen",
14
+ "Unmute": "Ne-prigušen",
15
+ "Playback Rate": "Stopa reprodukcije",
16
+ "Subtitles": "Podnaslov",
17
+ "subtitles off": "Podnaslov deaktiviran",
18
+ "Captions": "Titlovi",
19
+ "captions off": "Titlovi deaktivirani",
20
+ "Chapters": "Poglavlja",
21
+ "You aborted the media playback": "Isključili ste reprodukciju videa.",
22
+ "A network error caused the media download to fail part-way.": "Video se prestao preuzimati zbog greške na mreži.",
23
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Video se ne može reproducirati zbog servera, greške u mreži ili je format ne podržan.",
24
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Reprodukcija videa je zaustavljenja zbog greške u formatu ili zbog verzije vašeg pretraživača.",
25
+ "No compatible source was found for this media.": "Nije nađen nijedan kompatibilan izvor ovog videa."
26
+});

+ 26
- 0
static/pygal/js/video-js/lang/bg.js 查看文件

@@ -0,0 +1,26 @@
1
+videojs.addLanguage("bg",{
2
+ "Play": "Възпроизвеждане",
3
+ "Pause": "Пауза",
4
+ "Current Time": "Текущо време",
5
+ "Duration": "Продължителност",
6
+ "Remaining Time": "Оставащо време",
7
+ "Stream Type": "Тип на потока",
8
+ "LIVE": "НА ЖИВО",
9
+ "Loaded": "Заредено",
10
+ "Progress": "Прогрес",
11
+ "Fullscreen": "Цял екран",
12
+ "Non-Fullscreen": "Спиране на цял екран",
13
+ "Mute": "Без звук",
14
+ "Unmute": "Със звук",
15
+ "Playback Rate": "Скорост на възпроизвеждане",
16
+ "Subtitles": "Субтитри",
17
+ "subtitles off": "Спряни субтитри",
18
+ "Captions": "Аудио надписи",
19
+ "captions off": "Спряни аудио надписи",
20
+ "Chapters": "Глави",
21
+ "You aborted the media playback": "Спряхте възпроизвеждането на видеото",
22
+ "A network error caused the media download to fail part-way.": "Грешка в мрежата провали изтеглянето на видеото.",
23
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Видеото не може да бъде заредено заради проблем със сървъра или мрежата или защото този формат не е поддържан.",
24
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Възпроизвеждането на видеото беше прекъснато заради проблем с файла или защото видеото използва опции които браузърът Ви не поддържа.",
25
+ "No compatible source was found for this media.": "Не беше намерен съвместим източник за това видео."
26
+});

+ 26
- 0
static/pygal/js/video-js/lang/ca.js 查看文件

@@ -0,0 +1,26 @@
1
+videojs.addLanguage("ca",{
2
+ "Play": "Reproducció",
3
+ "Pause": "Pausa",
4
+ "Current Time": "Temps reproduït",
5
+ "Duration": "Durada total",
6
+ "Remaining Time": "Temps restant",
7
+ "Stream Type": "Tipus de seqüència",
8
+ "LIVE": "EN DIRECTE",
9
+ "Loaded": "Carregat",
10
+ "Progress": "Progrés",
11
+ "Fullscreen": "Pantalla completa",
12
+ "Non-Fullscreen": "Pantalla no completa",
13
+ "Mute": "Silencia",
14
+ "Unmute": "Amb so",
15
+ "Playback Rate": "Velocitat de reproducció",
16
+ "Subtitles": "Subtítols",
17
+ "subtitles off": "Subtítols desactivats",
18
+ "Captions": "Llegendes",
19
+ "captions off": "Llegendes desactivades",
20
+ "Chapters": "Capítols",
21
+ "You aborted the media playback": "Heu interromput la reproducció del vídeo.",
22
+ "A network error caused the media download to fail part-way.": "Un error de la xarxa ha interromput la baixada del vídeo.",
23
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "No s'ha pogut carregar el vídeo perquè el servidor o la xarxa han fallat, o bé perquè el seu format no és compatible.",
24
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "La reproducció de vídeo s'ha interrumput per un problema de corrupció de dades o bé perquè el vídeo demanava funcions que el vostre navegador no ofereix.",
25
+ "No compatible source was found for this media.": "No s'ha trobat cap font compatible amb el vídeo."
26
+});

+ 26
- 0
static/pygal/js/video-js/lang/cs.js 查看文件

@@ -0,0 +1,26 @@
1
+videojs.addLanguage("cs",{
2
+ "Play": "Přehrát",
3
+ "Pause": "Pauza",
4
+ "Current Time": "Aktuální čas",
5
+ "Duration": "Doba trvání",
6
+ "Remaining Time": "Zbývající čas",
7
+ "Stream Type": "Stream Type",
8
+ "LIVE": "ŽIVĚ",
9
+ "Loaded": "Načteno",
10
+ "Progress": "Stav",
11
+ "Fullscreen": "Celá obrazovka",
12
+ "Non-Fullscreen": "Zmenšená obrazovka",
13
+ "Mute": "Ztlumit zvuk",
14
+ "Unmute": "Přehrát zvuk",
15
+ "Playback Rate": "Rychlost přehrávání",
16
+ "Subtitles": "Titulky",
17
+ "subtitles off": "Titulky vypnuty",
18
+ "Captions": "Popisky",
19
+ "captions off": "Popisky vypnuty",
20
+ "Chapters": "Kapitoly",
21
+ "You aborted the media playback": "Přehrávání videa je přerušeno.",
22
+ "A network error caused the media download to fail part-way.": "Video nemohlo být načteno, kvůli chybě v síti.",
23
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Video nemohlo být načteno, buď kvůli chybě serveru nebo sítě nebo proto, že daný formát není podporován.",
24
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Váš prohlížeč nepodporuje formát videa.",
25
+ "No compatible source was found for this media.": "Špatně zadaný zdroj videa."
26
+});

+ 26
- 0
static/pygal/js/video-js/lang/da.js 查看文件

@@ -0,0 +1,26 @@
1
+videojs.addLanguage("da",{
2
+ "Play": "Afspil",
3
+ "Pause": "Pause",
4
+ "Current Time": "Aktuel tid",
5
+ "Duration": "Varighed",
6
+ "Remaining Time": "Resterende tid",
7
+ "Stream Type": "Stream-type",
8
+ "LIVE": "LIVE",
9
+ "Loaded": "Indlæst",
10
+ "Progress": "Status",
11
+ "Fullscreen": "Fuldskærm",
12
+ "Non-Fullscreen": "Luk fuldskærm",
13
+ "Mute": "Uden lyd",
14
+ "Unmute": "Med lyd",
15
+ "Playback Rate": "Afspilningsrate",
16
+ "Subtitles": "Undertekster",
17
+ "subtitles off": "Uden undertekster",
18
+ "Captions": "Undertekster for hørehæmmede",
19
+ "captions off": "Uden undertekster for hørehæmmede",
20
+ "Chapters": "Kapitler",
21
+ "You aborted the media playback": "Du afbrød videoafspilningen.",
22
+ "A network error caused the media download to fail part-way.": "En netværksfejl fik download af videoen til at fejle.",
23
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Videoen kunne ikke indlæses, enten fordi serveren eller netværket fejlede, eller fordi formatet ikke er understøttet.",
24
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Videoafspilningen blev afbrudt på grund af ødelagte data eller fordi videoen benyttede faciliteter som din browser ikke understøtter.",
25
+ "No compatible source was found for this media.": "Fandt ikke en kompatibel kilde for denne media."
26
+});

+ 84
- 0
static/pygal/js/video-js/lang/de.js 查看文件

@@ -0,0 +1,84 @@
1
+videojs.addLanguage("de",{
2
+ "Play": "Wiedergabe",
3
+ "Pause": "Pause",
4
+ "Replay": "Erneut abspielen",
5
+ "Current Time": "Aktueller Zeitpunkt",
6
+ "Duration": "Dauer",
7
+ "Remaining Time": "Verbleibende Zeit",
8
+ "Stream Type": "Streamtyp",
9
+ "LIVE": "LIVE",
10
+ "Loaded": "Geladen",
11
+ "Progress": "Status",
12
+ "Fullscreen": "Vollbild",
13
+ "Non-Fullscreen": "Kein Vollbild",
14
+ "Mute": "Ton aus",
15
+ "Unmute": "Ton ein",
16
+ "Playback Rate": "Wiedergabegeschwindigkeit",
17
+ "Subtitles": "Untertitel",
18
+ "subtitles off": "Untertitel aus",
19
+ "Captions": "Untertitel",
20
+ "captions off": "Untertitel aus",
21
+ "Chapters": "Kapitel",
22
+ "You aborted the media playback": "Sie haben die Videowiedergabe abgebrochen.",
23
+ "A network error caused the media download to fail part-way.": "Der Videodownload ist aufgrund eines Netzwerkfehlers fehlgeschlagen.",
24
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Das Video konnte nicht geladen werden, da entweder ein Server- oder Netzwerkfehler auftrat oder das Format nicht unterstützt wird.",
25
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Die Videowiedergabe wurde entweder wegen eines Problems mit einem beschädigten Video oder wegen verwendeten Funktionen, die vom Browser nicht unterstützt werden, abgebrochen.",
26
+ "No compatible source was found for this media.": "Für dieses Video wurde keine kompatible Quelle gefunden.",
27
+ "Play Video": "Video abspielen",
28
+ "Close": "Schließen",
29
+ "Modal Window": "Modales Fenster",
30
+ "This is a modal window": "Dies ist ein modales Fenster",
31
+ "This modal can be closed by pressing the Escape key or activating the close button.": "Durch Drücken der Esc-Taste bzw. Betätigung der Schaltfläche \"Schließen\" wird dieses modale Fenster geschlossen.",
32
+ ", opens captions settings dialog": ", öffnet Einstellungen für Untertitel",
33
+ ", opens subtitles settings dialog": ", öffnet Einstellungen für Untertitel",
34
+ ", selected": ", ausgewählt",
35
+ "captions settings": "Untertiteleinstellungen",
36
+ "subtitles settings": "Untertiteleinstellungen",
37
+ "descriptions settings": "Einstellungen für Beschreibungen",
38
+ "Close Modal Dialog": "Modales Fenster schließen",
39
+ "Descriptions": "Beschreibungen",
40
+ "descriptions off": "Beschreibungen aus",
41
+ "The media is encrypted and we do not have the keys to decrypt it.": "Die Entschlüsselungsschlüssel für den verschlüsselten Medieninhalt sind nicht verfügbar.",
42
+ ", opens descriptions settings dialog": ", öffnet Einstellungen für Beschreibungen",
43
+ "Audio Track": "Tonspur",
44
+ "Text": "Schrift",
45
+ "White": "Weiß",
46
+ "Black": "Schwarz",
47
+ "Red": "Rot",
48
+ "Green": "Grün",
49
+ "Blue": "Blau",
50
+ "Yellow": "Gelb",
51
+ "Magenta": "Magenta",
52
+ "Cyan": "Türkis",
53
+ "Background": "Hintergrund",
54
+ "Window": "Fenster",
55
+ "Transparent": "Durchsichtig",
56
+ "Semi-Transparent": "Halbdurchsichtig",
57
+ "Opaque": "Undurchsictig",
58
+ "Font Size": "Schriftgröße",
59
+ "Text Edge Style": "Textkantenstil",
60
+ "None": "Kein",
61
+ "Raised": "Erhoben",
62
+ "Depressed": "Gedrückt",
63
+ "Uniform": "Uniform",
64
+ "Dropshadow": "Schlagschatten",
65
+ "Font Family": "Schristfamilie",
66
+ "Proportional Sans-Serif": "Proportionale Sans-Serif",
67
+ "Monospace Sans-Serif": "Monospace Sans-Serif",
68
+ "Proportional Serif": "Proportionale Serif",
69
+ "Monospace Serif": "Monospace Serif",
70
+ "Casual": "Zwanglos",
71
+ "Script": "Schreibeschrift",
72
+ "Small Caps": "Small-Caps",
73
+ "Reset": "Zurücksetzen",
74
+ "restore all settings to the default values": "Alle Einstellungen auf die Standardwerte zurücksetzen",
75
+ "Done": "Fertig",
76
+ "Caption Settings Dialog": "Einstellungsdialog für Untertitel",
77
+ "Beginning of dialog window. Escape will cancel and close the window.": "Anfang des Dialogfensters. Esc bricht ab und schließt das Fenster.",
78
+ "End of dialog window.": "Ende des Dialogfensters.",
79
+ "Audio Player": "Audio-Player",
80
+ "Video Player": "Video-Player",
81
+ "Progress Bar": "Forschrittsbalken",
82
+ "progress bar timing: currentTime={1} duration={2}": "{1} von {2}",
83
+ "Volume Level": "Lautstärkestufe"
84
+});

+ 40
- 0
static/pygal/js/video-js/lang/el.js 查看文件

@@ -0,0 +1,40 @@
1
+videojs.addLanguage("el",{
2
+ "Play": "Aναπαραγωγή",
3
+ "Pause": "Παύση",
4
+ "Current Time": "Τρέχων χρόνος",
5
+ "Duration": "Συνολικός χρόνος",
6
+ "Remaining Time": "Υπολοιπόμενος χρόνος",
7
+ "Stream Type": "Τύπος ροής",
8
+ "LIVE": "ΖΩΝΤΑΝΑ",
9
+ "Loaded": "Φόρτωση επιτυχής",
10
+ "Progress": "Πρόοδος",
11
+ "Fullscreen": "Πλήρης οθόνη",
12
+ "Non-Fullscreen": "Έξοδος από πλήρη οθόνη",
13
+ "Mute": "Σίγαση",
14
+ "Unmute": "Kατάργηση σίγασης",
15
+ "Playback Rate": "Ρυθμός αναπαραγωγής",
16
+ "Subtitles": "Υπότιτλοι",
17
+ "subtitles off": "απόκρυψη υπότιτλων",
18
+ "Captions": "Λεζάντες",
19
+ "captions off": "απόκρυψη λεζάντων",
20
+ "Chapters": "Κεφάλαια",
21
+ "Close Modal Dialog": "Κλείσιμο παραθύρου",
22
+ "Descriptions": "Περιγραφές",
23
+ "descriptions off": "απόκρυψη περιγραφών",
24
+ "Audio Track": "Ροή ήχου",
25
+ "You aborted the media playback": "Ακυρώσατε την αναπαραγωγή",
26
+ "A network error caused the media download to fail part-way.": "Ένα σφάλμα δικτύου προκάλεσε την αποτυχία μεταφόρτωσης του αρχείου προς αναπαραγωγή.",
27
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Το αρχείο προς αναπαραγωγή δεν ήταν δυνατό να φορτωθεί είτε γιατί υπήρξε σφάλμα στον διακομιστή ή το δίκτυο, είτε γιατί ο τύπος του αρχείου δεν υποστηρίζεται.",
28
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Η αναπαραγωγή ακυρώθηκε είτε λόγω κατεστραμμένου αρχείου, είτε γιατί το αρχείο απαιτεί λειτουργίες που δεν υποστηρίζονται από το πρόγραμμα περιήγησης που χρησιμοποιείτε.",
29
+ "No compatible source was found for this media.": "Δεν βρέθηκε συμβατή πηγή αναπαραγωγής για το συγκεκριμένο αρχείο.",
30
+ "The media is encrypted and we do not have the keys to decrypt it.": "Το αρχείο προς αναπαραγωγή είναι κρυπτογραφημένo και δεν υπάρχουν τα απαραίτητα κλειδιά αποκρυπτογράφησης.",
31
+ "Play Video": "Αναπαραγωγή βίντεο",
32
+ "Close": "Κλείσιμο",
33
+ "Modal Window": "Aναδυόμενο παράθυρο",
34
+ "This is a modal window": "Το παρών είναι ένα αναδυόμενο παράθυρο",
35
+ "This modal can be closed by pressing the Escape key or activating the close button.": "Αυτό το παράθυρο μπορεί να εξαφανιστεί πατώντας το πλήκτρο Escape ή πατώντας το κουμπί κλεισίματος.",
36
+ ", opens captions settings dialog": ", εμφανίζει τις ρυθμίσεις για τις λεζάντες",
37
+ ", opens subtitles settings dialog": ", εμφανίζει τις ρυθμίσεις για τους υπότιτλους",
38
+ ", opens descriptions settings dialog": ", εμφανίζει τις ρυθμίσεις για τις περιγραφές",
39
+ ", selected": ", επιλεγμένο"
40
+});

+ 85
- 0
static/pygal/js/video-js/lang/en.js 查看文件

@@ -0,0 +1,85 @@
1
+videojs.addLanguage("en",{
2
+ "Audio Player": "Audio Player",
3
+ "Video Player": "Video Player",
4
+ "Play": "Play",
5
+ "Pause": "Pause",
6
+ "Replay": "Replay",
7
+ "Current Time": "Current Time",
8
+ "Duration": "Duration",
9
+ "Remaining Time": "Remaining Time",
10
+ "Stream Type": "Stream Type",
11
+ "LIVE": "LIVE",
12
+ "Loaded": "Loaded",
13
+ "Progress": "Progress",
14
+ "Progress Bar": "Progress Bar",
15
+ "progress bar timing: currentTime={1} duration={2}": "{1} of {2}",
16
+ "Fullscreen": "Fullscreen",
17
+ "Non-Fullscreen": "Non-Fullscreen",
18
+ "Mute": "Mute",
19
+ "Unmute": "Unmute",
20
+ "Playback Rate": "Playback Rate",
21
+ "Subtitles": "Subtitles",
22
+ "subtitles off": "subtitles off",
23
+ "Captions": "Captions",
24
+ "captions off": "captions off",
25
+ "Chapters": "Chapters",
26
+ "Descriptions": "Descriptions",
27
+ "descriptions off": "descriptions off",
28
+ "Audio Track": "Audio Track",
29
+ "Volume Level": "Volume Level",
30
+ "You aborted the media playback": "You aborted the media playback",
31
+ "A network error caused the media download to fail part-way.": "A network error caused the media download to fail part-way.",
32
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "The media could not be loaded, either because the server or network failed or because the format is not supported.",
33
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.",
34
+ "No compatible source was found for this media.": "No compatible source was found for this media.",
35
+ "The media is encrypted and we do not have the keys to decrypt it.": "The media is encrypted and we do not have the keys to decrypt it.",
36
+ "Play Video": "Play Video",
37
+ "Close": "Close",
38
+ "Close Modal Dialog": "Close Modal Dialog",
39
+ "Modal Window": "Modal Window",
40
+ "This is a modal window": "This is a modal window",
41
+ "This modal can be closed by pressing the Escape key or activating the close button.": "This modal can be closed by pressing the Escape key or activating the close button.",
42
+ ", opens captions settings dialog": ", opens captions settings dialog",
43
+ ", opens subtitles settings dialog": ", opens subtitles settings dialog",
44
+ ", opens descriptions settings dialog": ", opens descriptions settings dialog",
45
+ ", selected": ", selected",
46
+ "captions settings": "captions settings",
47
+ "subtitles settings": "subititles settings",
48
+ "descriptions settings": "descriptions settings",
49
+ "Text": "Text",
50
+ "White": "White",
51
+ "Black": "Black",
52
+ "Red": "Red",
53
+ "Green": "Green",
54
+ "Blue": "Blue",
55
+ "Yellow": "Yellow",
56
+ "Magenta": "Magenta",
57
+ "Cyan": "Cyan",
58
+ "Background": "Background",
59
+ "Window": "Window",
60
+ "Transparent": "Transparent",
61
+ "Semi-Transparent": "Semi-Transparent",
62
+ "Opaque": "Opaque",
63
+ "Font Size": "Font Size",
64
+ "Text Edge Style": "Text Edge Style",
65
+ "None": "None",
66
+ "Raised": "Raised",
67
+ "Depressed": "Depressed",
68
+ "Uniform": "Uniform",
69
+ "Dropshadow": "Dropshadow",
70
+ "Font Family": "Font Family",
71
+ "Proportional Sans-Serif": "Proportional Sans-Serif",
72
+ "Monospace Sans-Serif": "Monospace Sans-Serif",
73
+ "Proportional Serif": "Proportional Serif",
74
+ "Monospace Serif": "Monospace Serif",
75
+ "Casual": "Casual",
76
+ "Script": "Script",
77
+ "Small Caps": "Small Caps",
78
+ "Reset": "Reset",
79
+ "restore all settings to the default values": "restore all settings to the default values",
80
+ "Done": "Done",
81
+ "Caption Settings Dialog": "Caption Settings Dialog",
82
+ "Beginning of dialog window. Escape will cancel and close the window.": "Beginning of dialog window. Escape will cancel and close the window.",
83
+ "End of dialog window.": "End of dialog window.",
84
+ "{1} is loading.": "{1} is loading."
85
+});

+ 27
- 0
static/pygal/js/video-js/lang/es.js 查看文件

@@ -0,0 +1,27 @@
1
+videojs.addLanguage("es",{
2
+ "Play": "Reproducción",
3
+ "Play Video": "Reproducción Vídeo",
4
+ "Pause": "Pausa",
5
+ "Current Time": "Tiempo reproducido",
6
+ "Duration": "Duración total",
7
+ "Remaining Time": "Tiempo restante",
8
+ "Stream Type": "Tipo de secuencia",
9
+ "LIVE": "DIRECTO",
10
+ "Loaded": "Cargado",
11
+ "Progress": "Progreso",
12
+ "Fullscreen": "Pantalla completa",
13
+ "Non-Fullscreen": "Pantalla no completa",
14
+ "Mute": "Silenciar",
15
+ "Unmute": "No silenciado",
16
+ "Playback Rate": "Velocidad de reproducción",
17
+ "Subtitles": "Subtítulos",
18
+ "subtitles off": "Subtítulos desactivados",
19
+ "Captions": "Subtítulos especiales",
20
+ "captions off": "Subtítulos especiales desactivados",
21
+ "Chapters": "Capítulos",
22
+ "You aborted the media playback": "Ha interrumpido la reproducción del vídeo.",
23
+ "A network error caused the media download to fail part-way.": "Un error de red ha interrumpido la descarga del vídeo.",
24
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "No se ha podido cargar el vídeo debido a un fallo de red o del servidor o porque el formato es incompatible.",
25
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "La reproducción de vídeo se ha interrumpido por un problema de corrupción de datos o porque el vídeo precisa funciones que su navegador no ofrece.",
26
+ "No compatible source was found for this media.": "No se ha encontrado ninguna fuente compatible con este vídeo."
27
+});

+ 84
- 0
static/pygal/js/video-js/lang/fa.js 查看文件

@@ -0,0 +1,84 @@
1
+videojs.addLanguage("fa",{
2
+ "Audio Player": "پخش کننده صوتی",
3
+ "Video Player": "پخش کننده ویدیو",
4
+ "Play": "پخش",
5
+ "Pause": "مکث",
6
+ "Replay": "بازپخش",
7
+ "Current Time": "زمان کنونی",
8
+ "Duration": "مدت زمان",
9
+ "Remaining Time": "زمان باقیمانده",
10
+ "Stream Type": "نوع استریم",
11
+ "LIVE": "زنده",
12
+ "Loaded": "بارگیری شده",
13
+ "Progress": "پیشرفت",
14
+ "Progress Bar": "نوار پیشرفت",
15
+ "progress bar timing: currentTime={1} duration={2}": "{1} از {2}",
16
+ "Fullscreen": "تمام‌صفحه",
17
+ "Non-Fullscreen": "غیر تمام‌صفحه",
18
+ "Mute": "بی صدا",
19
+ "Unmute": "صدا دار",
20
+ "Playback Rate": "سرعت پخش",
21
+ "Subtitles": "زیرنویس",
22
+ "subtitles off": "بدون زیرنویس",
23
+ "Captions": "زیرتوضیح",
24
+ "captions off": "بدون زیرتوضیح",
25
+ "Chapters": "قسمت‌ها",
26
+ "Descriptions": "توصیف",
27
+ "descriptions off": "بدون توصیف",
28
+ "Audio Track": "صوت",
29
+ "Volume Level": "میزان صدا",
30
+ "You aborted the media playback": "شما پخش را قطع کردید.",
31
+ "A network error caused the media download to fail part-way.": "خطای شبکه باعث عدم بارگیری بخشی از رسانه شد.",
32
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": ".رسانه قابل بارگیری نیست.   علت آن ممکن است خطا در اتصال یا عدم پشتیبانی از فرمت باشد",
33
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "پخش  رسانه به علت اشکال در آن یا عدم پشتیبانی مرورگر شما قطع شد.",
34
+ "No compatible source was found for this media.": "هیچ منبع سازگاری، برای این رسانه پیدا نشد.",
35
+ "The media is encrypted and we do not have the keys to decrypt it.": "این رسانه رمزنگاری شده است و ما کلید رمزگشایی آن را نداریم.",
36
+ "Play Video": "پخش ویدیو",
37
+ "Close": "بستن",
38
+ "Close Modal Dialog": "بستن پنجره مودال",
39
+ "Modal Window": "پنجره مودال",
40
+ "This is a modal window": "این پنجره مودال",
41
+ "This modal can be closed by pressing the Escape key or activating the close button.": "این پنجره با دکمه اسکیپ با دکمه بستن قابل بسته شدن میباشد.",
42
+ ", opens captions settings dialog": ", تنظیمات زیرتوضیح را باز میکند",
43
+ ", opens subtitles settings dialog": ", تنظیمات زیرنویس را باز میکند",
44
+ ", opens descriptions settings dialog": ", تنظیمات توصیفات را باز میکند",
45
+ ", selected": ", انتخاب شده",
46
+ "captions settings": "تنظیمات زیرتوضیح",
47
+ "subtitles settings": "تنظیمات زیرنویس",
48
+ "descriptions settings": "تنظیمات توصیفات",
49
+ "Text": "متن",
50
+ "White": "سفید",
51
+ "Black": "سیاه",
52
+ "Red": "قرمز",
53
+ "Green": "سبز",
54
+ "Blue": "آبی",
55
+ "Yellow": "زرد",
56
+ "Magenta": "ارغوانی",
57
+ "Cyan": "سبزآبی",
58
+ "Background": "زمینه",
59
+ "Window": "پنجره",
60
+ "Transparent": "شفاف",
61
+ "Semi-Transparent": "نیمه شفاف",
62
+ "Opaque": "مات",
63
+ "Font Size": "اندازه فونت",
64
+ "Text Edge Style": "سبک لبه متن",
65
+ "None": "هیچ",
66
+ "Raised": "برآمده",
67
+ "Depressed": "فرورفته",
68
+ "Uniform": "یکنواخت",
69
+ "Dropshadow": "سایه دار",
70
+ "Font Family": "نوع فونت",
71
+ "Proportional Sans-Serif": "سنس-سریف متناسب",
72
+ "Monospace Sans-Serif": "سنس-سریف هم اندازه",
73
+ "Proportional Serif": "سریف متناسب",
74
+ "Monospace Serif": "سریف هم اندازه",
75
+ "Casual": "فانتزی",
76
+ "Script": "دست خط",
77
+ "Small Caps": "حروف کوچک به بزرگ",
78
+ "Reset": "باز نشاندن",
79
+ "restore all settings to the default values": "بازیابی همه تنظیمات به حالت اولیه",
80
+ "Done": "تکمیل",
81
+ "Caption Settings Dialog": "پنجره تنظیمات عناوین",
82
+ "Beginning of dialog window. Escape will cancel and close the window.": "ابتدای پنجره محاوره‌ای. دکمه اسکیپ پنجره را لغو میکند و میبندد.",
83
+ "End of dialog window.": "انتهای پنجره محاوره‌ای."
84
+});

+ 26
- 0
static/pygal/js/video-js/lang/fi.js 查看文件

@@ -0,0 +1,26 @@
1
+videojs.addLanguage("fi",{
2
+ "Play": "Toisto",
3
+ "Pause": "Tauko",
4
+ "Current Time": "Tämänhetkinen aika",
5
+ "Duration": "Kokonaisaika",
6
+ "Remaining Time": "Jäljellä oleva aika",
7
+ "Stream Type": "Lähetystyyppi",
8
+ "LIVE": "LIVE",
9
+ "Loaded": "Ladattu",
10
+ "Progress": "Edistyminen",
11
+ "Fullscreen": "Koko näyttö",
12
+ "Non-Fullscreen": "Koko näyttö pois",
13
+ "Mute": "Ääni pois",
14
+ "Unmute": "Ääni päällä",
15
+ "Playback Rate": "Toistonopeus",
16
+ "Subtitles": "Tekstitys",
17
+ "subtitles off": "Tekstitys pois",
18
+ "Captions": "Tekstitys",
19
+ "captions off": "Tekstitys pois",
20
+ "Chapters": "Kappaleet",
21
+ "You aborted the media playback": "Olet keskeyttänyt videotoiston.",
22
+ "A network error caused the media download to fail part-way.": "Verkkovirhe keskeytti videon latauksen.",
23
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Videon lataus ei onnistunut joko palvelin- tai verkkovirheestä tai väärästä formaatista johtuen.",
24
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Videon toisto keskeytyi, koska media on vaurioitunut tai käyttää käyttää toimintoja, joita selaimesi ei tue.",
25
+ "No compatible source was found for this media.": "Tälle videolle ei löytynyt yhteensopivaa lähdettä."
26
+});

+ 84
- 0
static/pygal/js/video-js/lang/fr.js 查看文件

@@ -0,0 +1,84 @@
1
+videojs.addLanguage("fr",{
2
+ "Audio Player": "Lecteur audio",
3
+ "Video Player": "Lecteur vidéo",
4
+ "Play": "Lecture",
5
+ "Pause": "Pause",
6
+ "Replay": "Revoir",
7
+ "Current Time": "Temps actuel",
8
+ "Duration": "Durée",
9
+ "Remaining Time": "Temps restant",
10
+ "Stream Type": "Type de flux",
11
+ "LIVE": "EN DIRECT",
12
+ "Loaded": "Chargé",
13
+ "Progress": "Progression",
14
+ "Progress Bar": "Barre de progression",
15
+ "progress bar timing: currentTime={1} duration={2}": "{1} de {2}",
16
+ "Fullscreen": "Plein écran",
17
+ "Non-Fullscreen": "Fenêtré",
18
+ "Mute": "Sourdine",
19
+ "Unmute": "Son activé",
20
+ "Playback Rate": "Vitesse de lecture",
21
+ "Subtitles": "Sous-titres",
22
+ "subtitles off": "Sous-titres désactivés",
23
+ "Captions": "Sous-titres transcrits",
24
+ "captions off": "Sous-titres transcrits désactivés",
25
+ "Chapters": "Chapitres",
26
+ "Descriptions": "Descriptions",
27
+ "descriptions off": "descriptions désactivées",
28
+ "Audio Track": "Piste audio",
29
+ "Volume Level": "Niveau de volume",
30
+ "You aborted the media playback": "Vous avez interrompu la lecture de la vidéo.",
31
+ "A network error caused the media download to fail part-way.": "Une erreur de réseau a interrompu le téléchargement de la vidéo.",
32
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Cette vidéo n'a pas pu être chargée, soit parce que le serveur ou le réseau a échoué ou parce que le format n'est pas reconnu.",
33
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "La lecture de la vidéo a été interrompue à cause d'un problème de corruption ou parce que la vidéo utilise des fonctionnalités non prises en charge par votre navigateur.",
34
+ "No compatible source was found for this media.": "Aucune source compatible n'a été trouvée pour cette vidéo.",
35
+ "The media is encrypted and we do not have the keys to decrypt it.": "Le média est chiffré et nous n'avons pas les clés pour le déchiffrer.",
36
+ "Play Video": "Lire la vidéo",
37
+ "Close": "Fermer",
38
+ "Close Modal Dialog": "Fermer la boîte de dialogue modale",
39
+ "Modal Window": "Fenêtre modale",
40
+ "This is a modal window": "Ceci est une fenêtre modale",
41
+ "This modal can be closed by pressing the Escape key or activating the close button.": "Ce modal peut être fermé en appuyant sur la touche Échap ou activer le bouton de fermeture.",
42
+ ", opens captions settings dialog": ", ouvrir les paramètres des sous-titres transcrits",
43
+ ", opens subtitles settings dialog": ", ouvrir les paramètres des sous-titres",
44
+ ", opens descriptions settings dialog": ", ouvrir les paramètres des descriptions",
45
+ ", selected": ", sélectionné",
46
+ "captions settings": "Paramètres des sous-titres transcrits",
47
+ "subtitles settings": "Paramètres des sous-titres",
48
+ "descriptions settings": "Paramètres des descriptions",
49
+ "Text": "Texte",
50
+ "White": "Blanc",
51
+ "Black": "Noir",
52
+ "Red": "Rouge",
53
+ "Green": "Vert",
54
+ "Blue": "Bleu",
55
+ "Yellow": "Jaune",
56
+ "Magenta": "Magenta",
57
+ "Cyan": "Cyan",
58
+ "Background": "Arrière-plan",
59
+ "Window": "Fenêtre",
60
+ "Transparent": "Transparent",
61
+ "Semi-Transparent": "Semi-transparent",
62
+ "Opaque": "Opaque",
63
+ "Font Size": "Taille des caractères",
64
+ "Text Edge Style": "Style des contours du texte",
65
+ "None": "Aucun",
66
+ "Raised": "Élevé",
67
+ "Depressed": "Enfoncé",
68
+ "Uniform": "Uniforme",
69
+ "Dropshadow": "Ombre portée",
70
+ "Font Family": "Famille de polices",
71
+ "Proportional Sans-Serif": "Polices à chasse variable sans empattement (Proportional Sans-Serif)",
72
+ "Monospace Sans-Serif": "Polices à chasse fixe sans empattement (Monospace Sans-Serif)",
73
+ "Proportional Serif": "Polices à chasse variable avec empattement (Proportional Serif)",
74
+ "Monospace Serif": "Polices à chasse fixe avec empattement (Monospace Serif)",
75
+ "Casual": "Manuscrite",
76
+ "Script": "Scripte",
77
+ "Small Caps": "Petites capitales",
78
+ "Reset": "Réinitialiser",
79
+ "restore all settings to the default values": "Restaurer tous les paramètres aux valeurs par défaut",
80
+ "Done": "Terminé",
81
+ "Caption Settings Dialog": "Boîte de dialogue des paramètres des sous-titres transcrits",
82
+ "Beginning of dialog window. Escape will cancel and close the window.": "Début de la fenêtre de dialogue. La touche d'échappement annulera et fermera la fenêtre.",
83
+ "End of dialog window.": "Fin de la fenêtre de dialogue."
84
+});

+ 27
- 0
static/pygal/js/video-js/lang/gl.js 查看文件

@@ -0,0 +1,27 @@
1
+videojs.addLanguage("gl",{
2
+ "Play": "Reprodución",
3
+ "Play Video": "Reprodución Vídeo",
4
+ "Pause": "Pausa",
5
+ "Current Time": "Tempo reproducido",
6
+ "Duration": "Duración total",
7
+ "Remaining Time": "Tempo restante",
8
+ "Stream Type": "Tipo de secuencia",
9
+ "LIVE": "DIRECTO",
10
+ "Loaded": "Cargado",
11
+ "Progress": "Progreso",
12
+ "Fullscreen": "Pantalla completa",
13
+ "Non-Fullscreen": "Pantalla non completa",
14
+ "Mute": "Silenciar",
15
+ "Unmute": "Non silenciado",
16
+ "Playback Rate": "Velocidade de reprodución",
17
+ "Subtitles": "Subtítulos",
18
+ "subtitles off": "Subtítulos desactivados",
19
+ "Captions": "Subtítulos con lenda",
20
+ "captions off": "Subtítulos con lenda desactivados",
21
+ "Chapters": "Capítulos",
22
+ "You aborted the media playback": "Interrompeches a reprodución do vídeo.",
23
+ "A network error caused the media download to fail part-way.": "Un erro de rede interrompeu a descarga do vídeo.",
24
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Non se puido cargar o vídeo debido a un fallo de rede ou do servidor ou porque o formato é incompatible.",
25
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "A reproducción de vídeo interrompeuse por un problema de corrupción de datos ou porque o vídeo precisa funcións que o teu navegador non ofrece.",
26
+ "No compatible source was found for this media.": "Non se atopou ningunha fonte compatible con este vídeo."
27
+});

+ 84
- 0
static/pygal/js/video-js/lang/he.js 查看文件

@@ -0,0 +1,84 @@
1
+videojs.addLanguage("he",{
2
+ "Audio Player": "נַגָּן שמע",
3
+ "Video Player": "נַגָּן וידאו",
4
+ "Play": "נַגֵּן",
5
+ "Pause": "השהה",
6
+ "Replay": "נַגֵּן שוב",
7
+ "Current Time": "זמן נוכחי",
8
+ "Duration": "זמן כולל",
9
+ "Remaining Time": "זמן נותר",
10
+ "Stream Type": "סוג Stream",
11
+ "LIVE": "שידור חי",
12
+ "Loaded": "נטען",
13
+ "Progress": "התקדמות",
14
+ "Progress Bar": "סרגל התקדמות",
15
+ "progress bar timing: currentTime={1} duration={2}": "{1} מתוך {2}",
16
+ "Fullscreen": "מסך מלא",
17
+ "Non-Fullscreen": "מסך לא מלא",
18
+ "Mute": "השתק",
19
+ "Unmute": "בטל השתקה",
20
+ "Playback Rate": "קצב ניגון",
21
+ "Subtitles": "כתוביות",
22
+ "subtitles off": "כתוביות כבויות",
23
+ "Captions": "כיתובים",
24
+ "captions off": "כיתובים כבויים",
25
+ "Chapters": "פרקים",
26
+ "Descriptions": "תיאורים",
27
+ "descriptions off": "תיאורים כבויים",
28
+ "Audio Track": "רצועת שמע",
29
+ "Volume Level": "רמת ווליום",
30
+ "You aborted the media playback": "ביטלת את השמעת המדיה",
31
+ "A network error caused the media download to fail part-way.": "שגיאת רשת גרמה להורדת המדיה להיכשל באמצע.",
32
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "לא ניתן לטעון את המדיה, או מכיוון שהרשת או השרת כשלו או מכיוון שהפורמט אינו נתמך.",
33
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "השמעת המדיה בוטלה בשל בעית השחטת מידע או מכיוון שהמדיה עשתה שימוש בתכונות שהדפדפן שלך לא תמך בהן.",
34
+ "No compatible source was found for this media.": "לא נמצא מקור תואם עבור מדיה זו.",
35
+ "The media is encrypted and we do not have the keys to decrypt it.": "המדיה מוצפנת ואין בידינו את המפתח כדי לפענח אותה.",
36
+ "Play Video": "נַגֵּן וידאו",
37
+ "Close": "סְגוֹר",
38
+ "Close Modal Dialog": "סְגוֹר דו-שיח מודאלי",
39
+ "Modal Window": "חלון מודאלי",
40
+ "This is a modal window": "זהו חלון מודאלי",
41
+ "This modal can be closed by pressing the Escape key or activating the close button.": "ניתן לסגור חלון מודאלי זה ע\"י לחיצה על כפתור ה-Escape או הפעלת כפתור הסגירה.",
42
+ ", opens captions settings dialog": ", פותח חלון הגדרות כיתובים",
43
+ ", opens subtitles settings dialog": ", פותח חלון הגדרות כתוביות",
44
+ ", opens descriptions settings dialog": ", פותח חלון הגדרות תיאורים",
45
+ ", selected": ", נבחר/ו",
46
+ "captions settings": "הגדרות כיתובים",
47
+ "subtitles settings": "הגדרות כתוביות",
48
+ "descriptions settings": "הגדרות תיאורים",
49
+ "Text": "טקסט",
50
+ "White": "לבן",
51
+ "Black": "שחור",
52
+ "Red": "אדום",
53
+ "Green": "ירוק",
54
+ "Blue": "כחול",
55
+ "Yellow": "צהוב",
56
+ "Magenta": "מַגֶ'נטָה",
57
+ "Cyan": "טורקיז",
58
+ "Background": "רקע",
59
+ "Window": "חלון",
60
+ "Transparent": "שקוף",
61
+ "Semi-Transparent": "שקוף למחצה",
62
+ "Opaque": "אָטוּם",
63
+ "Font Size": "גודל גופן",
64
+ "Text Edge Style": "סגנון קצוות טקסט",
65
+ "None": "ללא",
66
+ "Raised": "מורם",
67
+ "Depressed": "מורד",
68
+ "Uniform": "אחיד",
69
+ "Dropshadow": "הטלת צל",
70
+ "Font Family": "משפחת גופן",
71
+ "Proportional Sans-Serif": "פרופורציוני וללא תגיות (Proportional Sans-Serif)",
72
+ "Monospace Sans-Serif": "ברוחב אחיד וללא תגיות (Monospace Sans-Serif)",
73
+ "Proportional Serif": "פרופורציוני ועם תגיות (Proportional Serif)",
74
+ "Monospace Serif": "ברוחב אחיד ועם תגיות (Monospace Serif)",
75
+ "Casual": "אַגָבִי",
76
+ "Script": "תסריט",
77
+ "Small Caps": "אותיות קטנות",
78
+ "Reset": "אִפּוּס",
79
+ "restore all settings to the default values": "שחזר את כל ההגדרות לערכי ברירת המחדל",
80
+ "Done": "בוצע",
81
+ "Caption Settings Dialog": "דו-שיח הגדרות כיתובים",
82
+ "Beginning of dialog window. Escape will cancel and close the window.": "תחילת חלון דו-שיח. Escape יבטל ויסגור את החלון",
83
+ "End of dialog window.": "סוף חלון דו-שיח."
84
+});

+ 26
- 0
static/pygal/js/video-js/lang/hr.js 查看文件

@@ -0,0 +1,26 @@
1
+videojs.addLanguage("hr",{
2
+ "Play": "Pusti",
3
+ "Pause": "Pauza",
4
+ "Current Time": "Trenutno vrijeme",
5
+ "Duration": "Vrijeme trajanja",
6
+ "Remaining Time": "Preostalo vrijeme",
7
+ "Stream Type": "Način strimovanja",
8
+ "LIVE": "UŽIVO",
9
+ "Loaded": "Učitan",
10
+ "Progress": "Progres",
11
+ "Fullscreen": "Puni ekran",
12
+ "Non-Fullscreen": "Mali ekran",
13
+ "Mute": "Prigušen",
14
+ "Unmute": "Ne-prigušen",
15
+ "Playback Rate": "Stopa reprodukcije",
16
+ "Subtitles": "Podnaslov",
17
+ "subtitles off": "Podnaslov deaktiviran",
18
+ "Captions": "Titlovi",
19
+ "captions off": "Titlovi deaktivirani",
20
+ "Chapters": "Poglavlja",
21
+ "You aborted the media playback": "Isključili ste reprodukciju videa.",
22
+ "A network error caused the media download to fail part-way.": "Video se prestao preuzimati zbog greške na mreži.",
23
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Video se ne može reproducirati zbog servera, greške u mreži ili je format ne podržan.",
24
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Reprodukcija videa je zaustavljenja zbog greške u formatu ili zbog verzije vašeg pretraživača.",
25
+ "No compatible source was found for this media.": "Nije nađen nijedan kompatibilan izvor ovog videa."
26
+});

+ 26
- 0
static/pygal/js/video-js/lang/hu.js 查看文件

@@ -0,0 +1,26 @@
1
+videojs.addLanguage("hu",{
2
+ "Play": "Lejátszás",
3
+ "Pause": "Szünet",
4
+ "Current Time": "Aktuális időpont",
5
+ "Duration": "Hossz",
6
+ "Remaining Time": "Hátralévő idő",
7
+ "Stream Type": "Adatfolyam típusa",
8
+ "LIVE": "ÉLŐ",
9
+ "Loaded": "Betöltve",
10
+ "Progress": "Állapot",
11
+ "Fullscreen": "Teljes képernyő",
12
+ "Non-Fullscreen": "Normál méret",
13
+ "Mute": "Némítás",
14
+ "Unmute": "Némítás kikapcsolva",
15
+ "Playback Rate": "Lejátszási sebesség",
16
+ "Subtitles": "Feliratok",
17
+ "subtitles off": "Feliratok kikapcsolva",
18
+ "Captions": "Magyarázó szöveg",
19
+ "captions off": "Magyarázó szöveg kikapcsolva",
20
+ "Chapters": "Fejezetek",
21
+ "You aborted the media playback": "Leállította a lejátszást",
22
+ "A network error caused the media download to fail part-way.": "Hálózati hiba miatt a videó részlegesen töltődött le.",
23
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "A videó nem tölthető be hálózati vagy kiszolgálói hiba miatt, vagy a formátuma nem támogatott.",
24
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "A lejátszás adatsérülés miatt leállt, vagy a videó egyes tulajdonságait a böngészője nem támogatja.",
25
+ "No compatible source was found for this media.": "Nincs kompatibilis forrás ehhez a videóhoz."
26
+});

+ 26
- 0
static/pygal/js/video-js/lang/it.js 查看文件

@@ -0,0 +1,26 @@
1
+videojs.addLanguage("it",{
2
+ "Play": "Play",
3
+ "Pause": "Pausa",
4
+ "Current Time": "Orario attuale",
5
+ "Duration": "Durata",
6
+ "Remaining Time": "Tempo rimanente",
7
+ "Stream Type": "Tipo del Streaming",
8
+ "LIVE": "LIVE",
9
+ "Loaded": "Caricato",
10
+ "Progress": "Stato",
11
+ "Fullscreen": "Schermo intero",
12
+ "Non-Fullscreen": "Chiudi schermo intero",
13
+ "Mute": "Muto",
14
+ "Unmute": "Audio",
15
+ "Playback Rate": "Tasso di riproduzione",
16
+ "Subtitles": "Sottotitoli",
17
+ "subtitles off": "Senza sottotitoli",
18
+ "Captions": "Sottotitoli non udenti",
19
+ "captions off": "Senza sottotitoli non udenti",
20
+ "Chapters": "Capitolo",
21
+ "You aborted the media playback": "La riproduzione del filmato è stata interrotta.",
22
+ "A network error caused the media download to fail part-way.": "Il download del filmato è stato interrotto a causa di un problema rete.",
23
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Il filmato non può essere caricato a causa di un errore nel server o nella rete o perché il formato non viene supportato.",
24
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "La riproduzione del filmato è stata interrotta a causa di un file danneggiato o per l’utilizzo di impostazioni non supportate dal browser.",
25
+ "No compatible source was found for this media.": "Non ci sono fonti compatibili per questo filmato."
26
+});

+ 26
- 0
static/pygal/js/video-js/lang/ja.js 查看文件

@@ -0,0 +1,26 @@
1
+videojs.addLanguage("ja",{
2
+ "Play": "再生",
3
+ "Pause": "一時停止",
4
+ "Current Time": "現在の時間",
5
+ "Duration": "長さ",
6
+ "Remaining Time": "残りの時間",
7
+ "Stream Type": "ストリームの種類",
8
+ "LIVE": "ライブ",
9
+ "Loaded": "ロード済み",
10
+ "Progress": "進行状況",
11
+ "Fullscreen": "フルスクリーン",
12
+ "Non-Fullscreen": "フルスクリーン以外",
13
+ "Mute": "ミュート",
14
+ "Unmute": "ミュート解除",
15
+ "Playback Rate": "再生レート",
16
+ "Subtitles": "サブタイトル",
17
+ "subtitles off": "サブタイトル オフ",
18
+ "Captions": "キャプション",
19
+ "captions off": "キャプション オフ",
20
+ "Chapters": "チャプター",
21
+ "You aborted the media playback": "動画再生を中止しました",
22
+ "A network error caused the media download to fail part-way.": "ネットワーク エラーにより動画のダウンロードが途中で失敗しました",
23
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "サーバーまたはネットワークのエラー、またはフォーマットがサポートされていないため、動画をロードできませんでした",
24
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "破損の問題、またはお使いのブラウザがサポートしていない機能が動画に使用されていたため、動画の再生が中止されました",
25
+ "No compatible source was found for this media.": "この動画に対して互換性のあるソースが見つかりませんでした"
26
+});

+ 26
- 0
static/pygal/js/video-js/lang/ko.js 查看文件

@@ -0,0 +1,26 @@
1
+videojs.addLanguage("ko",{
2
+ "Play": "재생",
3
+ "Pause": "일시중지",
4
+ "Current Time": "현재 시간",
5
+ "Duration": "지정 기간",
6
+ "Remaining Time": "남은 시간",
7
+ "Stream Type": "스트리밍 유형",
8
+ "LIVE": "라이브",
9
+ "Loaded": "로드됨",
10
+ "Progress": "진행",
11
+ "Fullscreen": "전체 화면",
12
+ "Non-Fullscreen": "전체 화면 해제",
13
+ "Mute": "음소거",
14
+ "Unmute": "음소거 해제",
15
+ "Playback Rate": "재생 비율",
16
+ "Subtitles": "서브타이틀",
17
+ "subtitles off": "서브타이틀 끄기",
18
+ "Captions": "자막",
19
+ "captions off": "자막 끄기",
20
+ "Chapters": "챕터",
21
+ "You aborted the media playback": "비디오 재생을 취소했습니다.",
22
+ "A network error caused the media download to fail part-way.": "네트워크 오류로 인하여 비디오 일부를 다운로드하지 못 했습니다.",
23
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "비디오를 로드할 수 없습니다. 서버 혹은 네트워크 오류 때문이거나 지원되지 않는 형식 때문일 수 있습니다.",
24
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "비디오 재생이 취소됐습니다. 비디오가 손상되었거나 비디오가 사용하는 기능을 브라우저에서 지원하지 않는 것 같습니다.",
25
+ "No compatible source was found for this media.": "비디오에 호환되지 않는 소스가 있습니다."
26
+});

+ 26
- 0
static/pygal/js/video-js/lang/nb.js 查看文件

@@ -0,0 +1,26 @@
1
+videojs.addLanguage("nb",{
2
+ "Play": "Spill",
3
+ "Pause": "Pause",
4
+ "Current Time": "Aktuell tid",
5
+ "Duration": "Varighet",
6
+ "Remaining Time": "Gjenstående tid",
7
+ "Stream Type": "Type strøm",
8
+ "LIVE": "DIREKTE",
9
+ "Loaded": "Lastet inn",
10
+ "Progress": "Status",
11
+ "Fullscreen": "Fullskjerm",
12
+ "Non-Fullscreen": "Lukk fullskjerm",
13
+ "Mute": "Lyd av",
14
+ "Unmute": "Lyd på",
15
+ "Playback Rate": "Avspillingsrate",
16
+ "Subtitles": "Undertekst på",
17
+ "subtitles off": "Undertekst av",
18
+ "Captions": "Undertekst for hørselshemmede på",
19
+ "captions off": "Undertekst for hørselshemmede av",
20
+ "Chapters": "Kapitler",
21
+ "You aborted the media playback": "Du avbrøt avspillingen.",
22
+ "A network error caused the media download to fail part-way.": "En nettverksfeil avbrøt nedlasting av videoen.",
23
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Videoen kunne ikke lastes ned, på grunn av nettverksfeil eller serverfeil, eller fordi formatet ikke er støttet.",
24
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Videoavspillingen ble avbrudt på grunn av ødelagte data eller fordi videoen ville gjøre noe som nettleseren din ikke har støtte for.",
25
+ "No compatible source was found for this media.": "Fant ikke en kompatibel kilde for dette mediainnholdet."
26
+});

+ 84
- 0
static/pygal/js/video-js/lang/nl.js 查看文件

@@ -0,0 +1,84 @@
1
+videojs.addLanguage("nl",{
2
+ "Audio Player": "Audiospeler",
3
+ "Video Player": "Videospeler",
4
+ "Play": "Afspelen",
5
+ "Pause": "Pauzeren",
6
+ "Replay": "Opnieuw afspelen",
7
+ "Current Time": "Huidige tijd",
8
+ "Duration": "Tijdsduur",
9
+ "Remaining Time": "Resterende tijd",
10
+ "Stream Type": "Streamtype",
11
+ "LIVE": "LIVE",
12
+ "Loaded": "Geladen",
13
+ "Progress": "Voortgang",
14
+ "Progress Bar": "Voortgangsbalk",
15
+ "progress bar timing: currentTime={1} duration={2}": "{1} van {2}",
16
+ "Fullscreen": "Volledig scherm",
17
+ "Non-Fullscreen": "Geen volledig scherm",
18
+ "Mute": "Dempen",
19
+ "Unmute": "Niet dempen",
20
+ "Playback Rate": "Afspeelsnelheid",
21
+ "Subtitles": "Ondertiteling",
22
+ "subtitles off": "ondertiteling uit",
23
+ "Captions": "Bijschriften",
24
+ "captions off": "bijschriften uit",
25
+ "Chapters": "Hoofdstukken",
26
+ "Descriptions": "Beschrijvingen",
27
+ "descriptions off": "beschrijvingen uit",
28
+ "Audio Track": "Audiospoor",
29
+ "Volume Level": "Geluidsniveau",
30
+ "You aborted the media playback": "U heeft het afspelen van de media afgebroken",
31
+ "A network error caused the media download to fail part-way.": "Een netwerkfout heeft ervoor gezorgd dat het downloaden van de media halverwege is mislukt.",
32
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "De media kon niet worden geladen, dit komt doordat of de server of het netwerk mislukt of doordat het formaat niet wordt ondersteund.",
33
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Het afspelen van de media is afgebroken door een probleem met beschadeigde gegevens of doordat de media functies gebruikt die uw browser niet ondersteund.",
34
+ "No compatible source was found for this media.": "Er is geen geschikte bron voor deze media gevonden.",
35
+ "The media is encrypted and we do not have the keys to decrypt it.": "De media is versleuteld en we hebben de sleutels niet om deze te ontsleutelen.",
36
+ "Play Video": "Video afspelen",
37
+ "Close": "Sluiten",
38
+ "Close Modal Dialog": "Extra venster sluiten",
39
+ "Modal Window": "Extra venster",
40
+ "This is a modal window": "Dit is een extra venster",
41
+ "This modal can be closed by pressing the Escape key or activating the close button.": "Dit venster kan worden gesloten door op de Escape-toets te drukken of door de sluiten-knop te activeren.",
42
+ ", opens captions settings dialog": ", opent instellingen-venster voor bijschriften",
43
+ ", opens subtitles settings dialog": ", opent instellingen-venster voor ondertitelingen",
44
+ ", opens descriptions settings dialog": ", opent instellingen-venster voor beschrijvingen",
45
+ ", selected": ", geselecteerd",
46
+ "captions settings": "bijschriften-instellingen",
47
+ "subtitles settings": "ondertiteling-instellingen",
48
+ "descriptions settings": "beschrijvingen-instellingen",
49
+ "Text": "Tekst",
50
+ "White": "Wit",
51
+ "Black": "Zwart",
52
+ "Red": "Rood",
53
+ "Green": "Groen",
54
+ "Blue": "Blauw",
55
+ "Yellow": "Geel",
56
+ "Magenta": "Magenta",
57
+ "Cyan": "Cyaan",
58
+ "Background": "Achtergrond",
59
+ "Window": "Venster",
60
+ "Transparent": "Transparant",
61
+ "Semi-Transparent": "Semi-transparant",
62
+ "Opaque": "Ondoorzichtig",
63
+ "Font Size": "Lettergrootte",
64
+ "Text Edge Style": "Stijl tekstrand",
65
+ "None": "Geen",
66
+ "Raised": "Verhoogd",
67
+ "Depressed": "Ingedrukt",
68
+ "Uniform": "Uniform",
69
+ "Dropshadow": "Schaduw",
70
+ "Font Family": "Lettertype",
71
+ "Proportional Sans-Serif": "Proportioneel sans-serif",
72
+ "Monospace Sans-Serif": "Monospace sans-serif",
73
+ "Proportional Serif": "Proportioneel serif",
74
+ "Monospace Serif": "Monospace serif",
75
+ "Casual": "Luchtig",
76
+ "Script": "Script",
77
+ "Small Caps": "Kleine hoofdletters",
78
+ "Reset": "Herstellen",
79
+ "restore all settings to the default values": "alle instellingen naar de standaardwaarden herstellen",
80
+ "Done": "Klaar",
81
+ "Caption Settings Dialog": "Venster voor bijschriften-instellingen",
82
+ "Beginning of dialog window. Escape will cancel and close the window.": "Begin van dialoogvenster. Escape zal annuleren en het venster sluiten.",
83
+ "End of dialog window.": "Einde van dialoogvenster."
84
+});

+ 26
- 0
static/pygal/js/video-js/lang/nn.js 查看文件

@@ -0,0 +1,26 @@
1
+videojs.addLanguage("nn",{
2
+ "Play": "Spel",
3
+ "Pause": "Pause",
4
+ "Current Time": "Aktuell tid",
5
+ "Duration": "Varigheit",
6
+ "Remaining Time": "Tid attende",
7
+ "Stream Type": "Type straum",
8
+ "LIVE": "DIREKTE",
9
+ "Loaded": "Lasta inn",
10
+ "Progress": "Status",
11
+ "Fullscreen": "Fullskjerm",
12
+ "Non-Fullscreen": "Stenga fullskjerm",
13
+ "Mute": "Ljod av",
14
+ "Unmute": "Ljod på",
15
+ "Playback Rate": "Avspelingsrate",
16
+ "Subtitles": "Teksting på",
17
+ "subtitles off": "Teksting av",
18
+ "Captions": "Teksting for høyrselshemma på",
19
+ "captions off": "Teksting for høyrselshemma av",
20
+ "Chapters": "Kapitel",
21
+ "You aborted the media playback": "Du avbraut avspelinga.",
22
+ "A network error caused the media download to fail part-way.": "Ein nettverksfeil avbraut nedlasting av videoen.",
23
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Videoen kunne ikkje lastas ned, på grunn av ein nettverksfeil eller serverfeil, eller av di formatet ikkje er stoda.",
24
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Videoavspelinga blei broten på grunn av øydelagde data eller av di videoen ville gjera noe som nettlesaren din ikkje stodar.",
25
+ "No compatible source was found for this media.": "Fant ikke en kompatibel kilde for dette mediainnholdet."
26
+});

+ 34
- 0
static/pygal/js/video-js/lang/pl.js 查看文件

@@ -0,0 +1,34 @@
1
+videojs.addLanguage("pl",{
2
+ "Play": "Odtwarzaj",
3
+ "Pause": "Pauza",
4
+ "Current Time": "Aktualny czas",
5
+ "Duration": "Czas trwania",
6
+ "Remaining Time": "Pozostały czas",
7
+ "Stream Type": "Typ strumienia",
8
+ "LIVE": "NA ŻYWO",
9
+ "Loaded": "Załadowany",
10
+ "Progress": "Status",
11
+ "Fullscreen": "Pełny ekran",
12
+ "Non-Fullscreen": "Pełny ekran niedostępny",
13
+ "Mute": "Wyłącz dźwięk",
14
+ "Unmute": "Włącz dźwięk",
15
+ "Playback Rate": "Szybkość odtwarzania",
16
+ "Subtitles": "Napisy",
17
+ "subtitles off": "Napisy wyłączone",
18
+ "Captions": "Transkrypcja",
19
+ "captions off": "Transkrypcja wyłączona",
20
+ "Chapters": "Rozdziały",
21
+ "You aborted the media playback": "Odtwarzanie zostało przerwane",
22
+ "A network error caused the media download to fail part-way.": "Problemy z siecią spowodowały błąd przy pobieraniu materiału wideo.",
23
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Materiał wideo nie może być załadowany, ponieważ wystąpił problem z siecią lub format nie jest obsługiwany",
24
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Odtwarzanie materiału wideo zostało przerwane z powodu uszkodzonego pliku wideo lub z powodu błędu funkcji, które nie są wspierane przez przeglądarkę.",
25
+ "No compatible source was found for this media.": "Dla tego materiału wideo nie znaleziono kompatybilnego źródła.",
26
+ "Play Video": "Odtwarzaj wideo",
27
+ "Close": "Zamknij",
28
+ "Modal Window": "Okno Modala",
29
+ "This is a modal window": "To jest okno modala",
30
+ "This modal can be closed by pressing the Escape key or activating the close button.": "Ten modal możesz zamknąć naciskając przycisk Escape albo wybierając przycisk Zamknij.",
31
+ ", opens captions settings dialog": ", otwiera okno dialogowe ustawień transkrypcji",
32
+ ", opens subtitles settings dialog": ", otwiera okno dialogowe napisów",
33
+ ", selected": ", zaznaczone"
34
+});

+ 85
- 0
static/pygal/js/video-js/lang/pt-BR.js 查看文件

@@ -0,0 +1,85 @@
1
+videojs.addLanguage("pt-BR",{
2
+ "Audio Player": "Reprodutor de áudio",
3
+ "Video Player": "Reprodutor de vídeo",
4
+ "Play": "Tocar",
5
+ "Pause": "Pausar",
6
+ "Replay": "Tocar novamente",
7
+ "Current Time": "Tempo",
8
+ "Duration": "Duração",
9
+ "Remaining Time": "Tempo Restante",
10
+ "Stream Type": "Tipo de Stream",
11
+ "LIVE": "AO VIVO",
12
+ "Loaded": "Carregado",
13
+ "Progress": "Progresso",
14
+ "Progress Bar": "Barra de progresso",
15
+ "progress bar timing: currentTime={1} duration={2}": "{1} de {2}",
16
+ "Fullscreen": "Tela Cheia",
17
+ "Non-Fullscreen": "Tela Normal",
18
+ "Mute": "Mudo",
19
+ "Unmute": "Habilitar Som",
20
+ "Playback Rate": "Velocidade",
21
+ "Subtitles": "Legendas",
22
+ "subtitles off": "Sem Legendas",
23
+ "Captions": "Anotações",
24
+ "captions off": "Sem Anotações",
25
+ "Chapters": "Capítulos",
26
+ "Descriptions": "Descrições",
27
+ "descriptions off": "sem descrições",
28
+ "Audio Track": "Faixa de áudio",
29
+ "Volume Level": "Nível de volume",
30
+ "You aborted the media playback": "Você parou a execução do vídeo.",
31
+ "A network error caused the media download to fail part-way.": "Um erro na rede causou falha durante o download da mídia.",
32
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "A mídia não pode ser carregada, por uma falha de rede ou servidor ou o formato não é suportado.",
33
+ "No compatible source was found for this media.": "Nenhuma fonte foi encontrada para esta mídia.",
34
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "A reprodução foi interrompida devido à um problema de mídia corrompida ou porque a mídia utiliza funções que seu navegador não suporta.",
35
+ "The media is encrypted and we do not have the keys to decrypt it.": "A mídia está criptografada e não temos as chaves para descriptografar.",
36
+ "Play Video": "Tocar Vídeo",
37
+ "Close": "Fechar",
38
+ "Close Modal Dialog": "Fechar Diálogo Modal",
39
+ "Modal Window": "Janela Modal",
40
+ "This is a modal window": "Isso é uma janela-modal",
41
+ "This modal can be closed by pressing the Escape key or activating the close button.": "Esta janela pode ser fechada pressionando a tecla de Escape.",
42
+ ", opens captions settings dialog": ", abre as configurações de legendas de comentários",
43
+ ", opens subtitles settings dialog": ", abre as configurações de legendas",
44
+ ", opens descriptions settings dialog": ", abre as configurações",
45
+ ", selected": ", selecionada",
46
+ "captions settings": "configurações de legendas de comentários",
47
+ "subtitles settings": "configurações de legendas",
48
+ "descriptions settings": "configurações das descrições",
49
+ "Text": "Texto",
50
+ "White": "Branco",
51
+ "Black": "Preto",
52
+ "Red": "Vermelho",
53
+ "Green": "Verde",
54
+ "Blue": "Azul",
55
+ "Yellow": "Amarelo",
56
+ "Magenta": "Magenta",
57
+ "Cyan": "Ciano",
58
+ "Background": "Plano-de-Fundo",
59
+ "Window": "Janela",
60
+ "Transparent": "Transparente",
61
+ "Semi-Transparent": "Semi-Transparente",
62
+ "Opaque": "Opaco",
63
+ "Font Size": "Tamanho da Fonte",
64
+ "Text Edge Style": "Estilo da Borda",
65
+ "None": "Nenhum",
66
+ "Raised": "Elevado",
67
+ "Depressed": "Acachapado",
68
+ "Uniform": "Uniforme",
69
+ "Dropshadow": "Sombra de projeção",
70
+ "Font Family": "Família da Fonte",
71
+ "Proportional Sans-Serif": "Sans-Serif(Sem serifa) Proporcional",
72
+ "Monospace Sans-Serif": "Sans-Serif(Sem serifa) Monoespaçada",
73
+ "Proportional Serif": "Serifa Proporcional",
74
+ "Monospace Serif": "Serifa Monoespaçada",
75
+ "Casual": "Casual",
76
+ "Script": "Script",
77
+ "Small Caps": "Maiúsculas Pequenas",
78
+ "Reset": "Redefinir",
79
+ "restore all settings to the default values": "restaurar todas as configurações aos valores padrão",
80
+ "Done": "Salvar",
81
+ "Caption Settings Dialog": "Caíxa-de-Diálogo das configurações de Legendas",
82
+ "Beginning of dialog window. Escape will cancel and close the window.": "Iniciando a Janela-de-Diálogo. Pressionar Escape irá cancelar e fechar a janela.",
83
+ "End of dialog window.": "Fim da Janela-de-Diálogo",
84
+ "{1} is loading.": "{1} está carregando."
85
+});

+ 41
- 0
static/pygal/js/video-js/lang/pt-PT.js 查看文件

@@ -0,0 +1,41 @@
1
+videojs.addLanguage("pt-PT",{
2
+ "Play": "Reproduzir",
3
+ "Pause": "Parar",
4
+ "Replay": "Reiniciar",
5
+ "Current Time": "Tempo Atual",
6
+ "Duration": "Duração",
7
+ "Remaining Time": "Tempo Restante",
8
+ "Stream Type": "Tipo de Stream",
9
+ "LIVE": "EM DIRETO",
10
+ "Loaded": "Carregado",
11
+ "Progress": "Progresso",
12
+ "Fullscreen": "Ecrã inteiro",
13
+ "Non-Fullscreen": "Ecrã normal",
14
+ "Mute": "Desativar som",
15
+ "Unmute": "Ativar som",
16
+ "Playback Rate": "Velocidade de reprodução",
17
+ "Subtitles": "Legendas",
18
+ "subtitles off": "desativar legendas",
19
+ "Captions": "Anotações",
20
+ "captions off": "desativar anotações",
21
+ "Chapters": "Capítulos",
22
+ "Close Modal Dialog": "Fechar Janela Modal",
23
+ "Descriptions": "Descrições",
24
+ "descriptions off": "desativar descrições",
25
+ "Audio Track": "Faixa Áudio",
26
+ "You aborted the media playback": "Parou a reprodução do vídeo.",
27
+ "A network error caused the media download to fail part-way.": "Um erro na rede fez o vídeo falhar parcialmente.",
28
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "O vídeo não pode ser carregado, ou porque houve um problema na rede ou no servidor, ou porque formato do vídeo não é compatível.",
29
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "A reprodução foi interrompida por um problema com o vídeo ou porque o formato não é compatível com o seu navegador.",
30
+ "No compatible source was found for this media.": "Não foi encontrada uma fonte de vídeo compatível.",
31
+ "The media is encrypted and we do not have the keys to decrypt it.": "O vídeo está encriptado e não há uma chave para o desencriptar.",
32
+ "Play Video": "Reproduzir Vídeo",
33
+ "Close": "Fechar",
34
+ "Modal Window": "Janela Modal",
35
+ "This is a modal window": "Isto é uma janela modal",
36
+ "This modal can be closed by pressing the Escape key or activating the close button.": "Esta modal pode ser fechada pressionando a tecla ESC ou ativando o botão de fechar.",
37
+ ", opens captions settings dialog": ", abre janela com definições de legendas",
38
+ ", opens subtitles settings dialog": ", abre janela com definições de legendas",
39
+ ", opens descriptions settings dialog": ", abre janela com definições de descrições",
40
+ ", selected": ", seleccionado"
41
+});

+ 84
- 0
static/pygal/js/video-js/lang/ru.js 查看文件

@@ -0,0 +1,84 @@
1
+videojs.addLanguage("ru",{
2
+ "Audio Player": "Аудио проигрыватель",
3
+ "Video Player": "Видео проигрыватель",
4
+ "Play": "Воспроизвести",
5
+ "Pause": "Приостановить",
6
+ "Replay": "Воспроизвести снова",
7
+ "Current Time": "Текущее время",
8
+ "Duration": "Продолжительность",
9
+ "Remaining Time": "Оставшееся время",
10
+ "Stream Type": "Тип потока",
11
+ "LIVE": "ОНЛАЙН",
12
+ "Loaded": "Загрузка",
13
+ "Progress": "Прогресс",
14
+ "Progress Bar": "Индикатор загрузки",
15
+ "progress bar timing: currentTime={1} duration={2}": "{1} из {2}",
16
+ "Fullscreen": "Полноэкранный режим",
17
+ "Non-Fullscreen": "Неполноэкранный режим",
18
+ "Mute": "Без звука",
19
+ "Unmute": "Со звуком",
20
+ "Playback Rate": "Скорость воспроизведения",
21
+ "Subtitles": "Субтитры",
22
+ "subtitles off": "Субтитры выкл.",
23
+ "Captions": "Подписи",
24
+ "captions off": "Подписи выкл.",
25
+ "Chapters": "Главы",
26
+ "Descriptions": "Описания",
27
+ "descriptions off": "Отключить описания",
28
+ "Audio Track": "Звуковая дорожка",
29
+ "Volume Level": "Уровень громкости",
30
+ "You aborted the media playback": "Вы прервали воспроизведение видео",
31
+ "A network error caused the media download to fail part-way.": "Ошибка сети вызвала сбой во время загрузки видео.",
32
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Невозможно загрузить видео из-за сетевого или серверного сбоя либо формат не поддерживается.",
33
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Воспроизведение видео было приостановлено из-за повреждения либо в связи с тем, что видео использует функции, неподдерживаемые вашим браузером.",
34
+ "No compatible source was found for this media.": "Совместимые источники для этого видео отсутствуют.",
35
+ "The media is encrypted and we do not have the keys to decrypt it.": "Видео в зашифрованном виде, и у нас нет ключей для расшифровки.",
36
+ "Play Video": "Воспроизвести видео",
37
+ "Close": "Закрыть",
38
+ "Close Modal Dialog": "Закрыть модальное окно",
39
+ "Modal Window": "Модальное окно",
40
+ "This is a modal window": "Это модальное окно",
41
+ "This modal can be closed by pressing the Escape key or activating the close button.": "Модальное окно можно закрыть нажав Esc или кнопку закрытия окна.",
42
+ ", opens captions settings dialog": ", откроется диалог настройки подписей",
43
+ ", opens subtitles settings dialog": ", откроется диалог настройки субтитров",
44
+ ", opens descriptions settings dialog": ", откроется диалог настройки описаний",
45
+ ", selected": ", выбрано",
46
+ "captions settings": "настройки подписей",
47
+ "subtitles settings": "настройки субтитров",
48
+ "descriptions settings": "настройки описаний",
49
+ "Text": "Текст",
50
+ "White": "Белый",
51
+ "Black": "Черный",
52
+ "Red": "Красный",
53
+ "Green": "Зеленый",
54
+ "Blue": "Синий",
55
+ "Yellow": "Желтый",
56
+ "Magenta": "Пурпурный",
57
+ "Cyan": "Голубой",
58
+ "Background": "Фон",
59
+ "Window": "Окно",
60
+ "Transparent": "Прозрачный",
61
+ "Semi-Transparent": "Полупрозрачный",
62
+ "Opaque": "Прозрачность",
63
+ "Font Size": "Размер шрифта",
64
+ "Text Edge Style": "Стиль края текста",
65
+ "None": "Ничего",
66
+ "Raised": "Поднятый",
67
+ "Depressed": "Пониженный",
68
+ "Uniform": "Одинаковый",
69
+ "Dropshadow": "Тень",
70
+ "Font Family": "Шрифт",
71
+ "Proportional Sans-Serif": "Пропорциональный без засечек",
72
+ "Monospace Sans-Serif": "Моноширинный без засечек",
73
+ "Proportional Serif": "Пропорциональный с засечками",
74
+ "Monospace Serif": "Моноширинный с засечками",
75
+ "Casual": "Случайный",
76
+ "Script": "Письменный",
77
+ "Small Caps": "Малые прописные",
78
+ "Reset": "Сбросить",
79
+ "restore all settings to the default values": "сбросить все найстройки по умолчанию",
80
+ "Done": "Готово",
81
+ "Caption Settings Dialog": "Диалог настроек подписи",
82
+ "Beginning of dialog window. Escape will cancel and close the window.": "Начало диалоговго окна. Кнопка Escape закроет или отменит окно",
83
+ "End of dialog window.": "Конец диалогового окна."
84
+});

+ 84
- 0
static/pygal/js/video-js/lang/sk.js 查看文件

@@ -0,0 +1,84 @@
1
+videojs.addLanguage("sk",{
2
+ "Audio Player": "Zvukový prehrávač",
3
+ "Video Player": "Video prehrávač",
4
+ "Play": "Prehrať",
5
+ "Pause": "Pozastaviť",
6
+ "Replay": "Prehrať znova",
7
+ "Current Time": "Aktuálny čas",
8
+ "Duration": "Čas trvania",
9
+ "Remaining Time": "Zostávajúci čas",
10
+ "Stream Type": "Typ stopy",
11
+ "LIVE": "NAŽIVO",
12
+ "Loaded": "Načítané",
13
+ "Progress": "Priebeh",
14
+ "Progress Bar": "Ukazovateľ priebehu",
15
+ "progress bar timing: currentTime={1} duration={2}": "časovanie ukazovateľa priebehu: currentTime={1} duration={2}",
16
+ "Fullscreen": "Režim celej obrazovky",
17
+ "Non-Fullscreen": "Režim normálnej obrazovky",
18
+ "Mute": "Stlmiť",
19
+ "Unmute": "Zrušiť stlmenie",
20
+ "Playback Rate": "Rýchlosť prehrávania",
21
+ "Subtitles": "Titulky",
22
+ "subtitles off": "titulky vypnuté",
23
+ "Captions": "Popisky",
24
+ "captions off": "popisky vypnuté",
25
+ "Chapters": "Kapitoly",
26
+ "Descriptions": "Opisy",
27
+ "descriptions off": "opisy vypnuté",
28
+ "Audio Track": "Zvuková stopa",
29
+ "Volume Level": "Úroveň hlasitosti",
30
+ "You aborted the media playback": "Prerušili ste prehrávanie",
31
+ "A network error caused the media download to fail part-way.": "Sťahovanie súboru bolo zrušené pre chybu na sieti.",
32
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Súbor sa nepodarilo načítať pre chybu servera, sieťového pripojenia, alebo je formát súboru nepodporovaný.",
33
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Prehrávanie súboru bolo prerušené pre poškodené dáta, alebo súbor používa vlastnosti, ktoré váš prehliadač nepodporuje.",
34
+ "No compatible source was found for this media.": "Nebol nájdený žiaden kompatibilný zdroj pre tento súbor.",
35
+ "The media is encrypted and we do not have the keys to decrypt it.": "Súbor je zašifrovaný a nie je k dispozícii kľúč na rozšifrovanie.",
36
+ "Play Video": "Prehrať video",
37
+ "Close": "Zatvoriť",
38
+ "Close Modal Dialog": "Zatvoriť modálne okno",
39
+ "Modal Window": "Modálne okno",
40
+ "This is a modal window": "Toto je modálne okno",
41
+ "This modal can be closed by pressing the Escape key or activating the close button.": "Toto modálne okno je možné zatvoriť stlačením klávesy Escape, alebo aktivovaním tlačidla na zatvorenie.",
42
+ ", opens captions settings dialog": ", otvorí okno nastavení popiskov",
43
+ ", opens subtitles settings dialog": ", otvorí okno nastavení titulkov",
44
+ ", opens descriptions settings dialog": ", otvorí okno nastavení opisov",
45
+ ", selected": ", označené",
46
+ "captions settings": "nastavenia popiskov",
47
+ "subtitles settings": "nastavenia titulkov",
48
+ "descriptions settings": "nastavenia opisov",
49
+ "Text": "Text",
50
+ "White": "Biela",
51
+ "Black": "Čierna",
52
+ "Red": "Červená",
53
+ "Green": "Zelená",
54
+ "Blue": "Modrá",
55
+ "Yellow": "Žltá",
56
+ "Magenta": "Ružová",
57
+ "Cyan": "Tyrkysová",
58
+ "Background": "Pozadie",
59
+ "Window": "Okno",
60
+ "Transparent": "Priesvitné",
61
+ "Semi-Transparent": "Polopriesvitné",
62
+ "Opaque": "Plné",
63
+ "Font Size": "Veľkosť písma",
64
+ "Text Edge Style": "Typ okrajov písma",
65
+ "None": "Žiadne",
66
+ "Raised": "Zvýšené",
67
+ "Depressed": "Znížené",
68
+ "Uniform": "Pravidelné",
69
+ "Dropshadow": "S tieňom",
70
+ "Font Family": "Typ písma",
71
+ "Proportional Sans-Serif": "Proporčné bezpätkové",
72
+ "Monospace Sans-Serif": "Pravidelné, bezpätkové",
73
+ "Proportional Serif": "Proporčné pätkové",
74
+ "Monospace Serif": "Pravidelné pätkové",
75
+ "Casual": "Bežné",
76
+ "Script": "Písané",
77
+ "Small Caps": "Malé kapitálky",
78
+ "Reset": "Resetovať",
79
+ "restore all settings to the default values": "všetky nastavenia na základné hodnoty",
80
+ "Done": "Hotovo",
81
+ "Caption Settings Dialog": "Okno nastavení popiskov",
82
+ "Beginning of dialog window. Escape will cancel and close the window.": "Začiatok okna. Klávesa Escape zruší a zavrie okno.",
83
+ "End of dialog window.": "Koniec okna."
84
+});

+ 26
- 0
static/pygal/js/video-js/lang/sr.js 查看文件

@@ -0,0 +1,26 @@
1
+videojs.addLanguage("sr",{
2
+ "Play": "Pusti",
3
+ "Pause": "Pauza",
4
+ "Current Time": "Trenutno vrijeme",
5
+ "Duration": "Vrijeme trajanja",
6
+ "Remaining Time": "Preostalo vrijeme",
7
+ "Stream Type": "Način strimovanja",
8
+ "LIVE": "UŽIVO",
9
+ "Loaded": "Učitan",
10
+ "Progress": "Progres",
11
+ "Fullscreen": "Puni ekran",
12
+ "Non-Fullscreen": "Mali ekran",
13
+ "Mute": "Prigušen",
14
+ "Unmute": "Ne-prigušen",
15
+ "Playback Rate": "Stopa reprodukcije",
16
+ "Subtitles": "Podnaslov",
17
+ "subtitles off": "Podnaslov deaktiviran",
18
+ "Captions": "Titlovi",
19
+ "captions off": "Titlovi deaktivirani",
20
+ "Chapters": "Poglavlja",
21
+ "You aborted the media playback": "Isključili ste reprodukciju videa.",
22
+ "A network error caused the media download to fail part-way.": "Video se prestao preuzimati zbog greške na mreži.",
23
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Video se ne može reproducirati zbog servera, greške u mreži ili je format ne podržan.",
24
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Reprodukcija videa je zaustavljenja zbog greške u formatu ili zbog verzije vašeg pretraživača.",
25
+ "No compatible source was found for this media.": "Nije nađen nijedan kompatibilan izvor ovog videa."
26
+});

+ 26
- 0
static/pygal/js/video-js/lang/sv.js 查看文件

@@ -0,0 +1,26 @@
1
+videojs.addLanguage("sv",{
2
+ "Play": "Spela",
3
+ "Pause": "Pausa",
4
+ "Current Time": "Aktuell tid",
5
+ "Duration": "Total tid",
6
+ "Remaining Time": "Återstående tid",
7
+ "Stream Type": "Strömningstyp",
8
+ "LIVE": "LIVE",
9
+ "Loaded": "Laddad",
10
+ "Progress": "Förlopp",
11
+ "Fullscreen": "Fullskärm",
12
+ "Non-Fullscreen": "Ej fullskärm",
13
+ "Mute": "Ljud av",
14
+ "Unmute": "Ljud på",
15
+ "Playback Rate": "Uppspelningshastighet",
16
+ "Subtitles": "Text på",
17
+ "subtitles off": "Text av",
18
+ "Captions": "Text på",
19
+ "captions off": "Text av",
20
+ "Chapters": "Kapitel",
21
+ "You aborted the media playback": "Du har avbrutit videouppspelningen.",
22
+ "A network error caused the media download to fail part-way.": "Ett nätverksfel gjorde att nedladdningen av videon avbröts.",
23
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Det gick inte att ladda videon, antingen på grund av ett server- eller nätverksfel, eller för att formatet inte stöds.",
24
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Uppspelningen avbröts på grund av att videon är skadad, eller också för att videon använder funktioner som din webbläsare inte stöder.",
25
+ "No compatible source was found for this media.": "Det gick inte att hitta någon kompatibel källa för den här videon."
26
+});

+ 76
- 0
static/pygal/js/video-js/lang/tr.js 查看文件

@@ -0,0 +1,76 @@
1
+videojs.addLanguage("tr",{
2
+ "Play": "Oynat",
3
+ "Pause": "Duraklat",
4
+ "Replay": "Yeniden Oynat",
5
+ "Current Time": "Süre",
6
+ "Duration": "Toplam Süre",
7
+ "Remaining Time": "Kalan Süre",
8
+ "Stream Type": "Yayın Tipi",
9
+ "LIVE": "CANLI",
10
+ "Loaded": "Yüklendi",
11
+ "Progress": "Yükleniyor",
12
+ "Fullscreen": "Tam Ekran",
13
+ "Non-Fullscreen": "Küçük Ekran",
14
+ "Mute": "Ses Kapa",
15
+ "Unmute": "Ses Aç",
16
+ "Playback Rate": "Oynatma Hızı",
17
+ "Subtitles": "Altyazı",
18
+ "subtitles off": "Altyazı Kapalı",
19
+ "Captions": "Altyazı",
20
+ "captions off": "Altyazı Kapalı",
21
+ "Chapters": "Bölümler",
22
+ "Close Modal Dialog": "Dialogu Kapat",
23
+ "Descriptions": "Açıklamalar",
24
+ "descriptions off": "Açıklamalar kapalı",
25
+ "Audio Track": "Ses Dosyası",
26
+ "You aborted the media playback": "Video oynatmayı iptal ettiniz",
27
+ "A network error caused the media download to fail part-way.": "Video indirilirken bağlantı sorunu oluştu.",
28
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Video oynatılamadı, ağ ya da sunucu hatası veya belirtilen format desteklenmiyor.",
29
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Tarayıcınız desteklemediği için videoda hata oluştu.",
30
+ "No compatible source was found for this media.": "Video için kaynak bulunamadı.",
31
+ "The media is encrypted and we do not have the keys to decrypt it.": "Video, şifrelenmiş bir kaynaktan geliyor ve oynatmak için gerekli anahtar bulunamadı.",
32
+ "Play Video": "Videoyu Oynat",
33
+ "Close": "Kapat",
34
+ "Modal Window": "Modal Penceresi",
35
+ "This is a modal window": "Bu bir modal penceresidir",
36
+ "This modal can be closed by pressing the Escape key or activating the close button.": "Bu modal ESC tuşuna basarak ya da kapata tıklanarak kapatılabilir.",
37
+ ", opens captions settings dialog": ", altyazı ayarları menüsünü açar",
38
+ ", opens subtitles settings dialog": ", altyazı ayarları menüsünü açar",
39
+ ", opens descriptions settings dialog": ", açıklama ayarları menüsünü açar",
40
+ ", selected": ", seçildi",
41
+ "captions settings": "altyazı ayarları",
42
+ "subtitles settings": "altyazı ayarları",
43
+ "descriptions settings": "açıklama ayarları",
44
+ "Text": "Yazı",
45
+ "White": "Beyaz",
46
+ "Black": "Siyah",
47
+ "Red": "Kırmızı",
48
+ "Green": "Yeşil",
49
+ "Blue": "Mavi",
50
+ "Yellow": "Sarı",
51
+ "Magenta": "Macenta",
52
+ "Cyan": "Açık Mavi (Camgöbeği)",
53
+ "Background": "Arka plan",
54
+ "Window": "Pencere",
55
+ "Transparent": "Saydam",
56
+ "Semi-Transparent": "Yarı-Saydam",
57
+ "Opaque": "Mat",
58
+ "Font Size": "Yazı Boyutu",
59
+ "Text Edge Style": "Yazı Kenarlıkları",
60
+ "None": "Hiçbiri",
61
+ "Raised": "Kabartılmış",
62
+ "Depressed": "Yassı",
63
+ "Uniform": "Düz",
64
+ "Dropshadow": "Gölgeli",
65
+ "Font Family": "Yazı Tipi",
66
+ "Proportional Sans-Serif": "Orantılı Sans-Serif",
67
+ "Monospace Sans-Serif": "Eşaralıklı Sans-Serif",
68
+ "Proportional Serif": "Orantılı Serif",
69
+ "Monospace Serif": "Eşaralıklı Serif",
70
+ "Casual": "Gündelik",
71
+ "Script": "El Yazısı",
72
+ "Small Caps": "Küçük Boyutlu Büyük Harfli",
73
+ "Done": "Tamam",
74
+ "Caption Settings Dialog": "Altyazı Ayarları Menüsü",
75
+ "Beginning of dialog window. Escape will cancel and close the window.": "Diyalog penceresinin başlangıcı. ESC tuşu işlemi iptal edip pencereyi kapatacaktır."
76
+});

+ 40
- 0
static/pygal/js/video-js/lang/uk.js 查看文件

@@ -0,0 +1,40 @@
1
+videojs.addLanguage("uk",{
2
+ "Play": "Відтворити",
3
+ "Pause": "Призупинити",
4
+ "Current Time": "Поточний час",
5
+ "Duration": "Тривалість",
6
+ "Remaining Time": "Час, що залишився",
7
+ "Stream Type": "Тип потоку",
8
+ "LIVE": "НАЖИВО",
9
+ "Loaded": "Завантаження",
10
+ "Progress": "Прогрес",
11
+ "Fullscreen": "Повноекранний режим",
12
+ "Non-Fullscreen": "Неповноекранний режим",
13
+ "Mute": "Без звуку",
14
+ "Unmute": "Зі звуком",
15
+ "Playback Rate": "Швидкість відтворення",
16
+ "Subtitles": "Субтитри",
17
+ "subtitles off": "Без субтитрів",
18
+ "Captions": "Підписи",
19
+ "captions off": "Без підписів",
20
+ "Chapters": "Розділи",
21
+ "Close Modal Dialog": "Закрити модальний діалог",
22
+ "Descriptions": "Описи",
23
+ "descriptions off": "Без описів",
24
+ "Audio Track": "Аудіодоріжка",
25
+ "You aborted the media playback": "Ви припинили відтворення відео",
26
+ "A network error caused the media download to fail part-way.": "Помилка мережі викликала збій під час завантаження відео.",
27
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Неможливо завантажити відео через мережевий чи серверний збій або формат не підтримується.",
28
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Відтворення відео було припинено через пошкодження або у зв'язку з тим, що відео використовує функції, які не підтримуються вашим браузером.",
29
+ "No compatible source was found for this media.": "Сумісні джерела для цього відео відсутні.",
30
+ "The media is encrypted and we do not have the keys to decrypt it.": "Відео в зашифрованому вигляді, і ми не маємо ключі для розшифровки.",
31
+ "Play Video": "Відтворити відео",
32
+ "Close": "Закрити",
33
+ "Modal Window": "Модальне вікно",
34
+ "This is a modal window": "Це модальне вікно.",
35
+ "This modal can be closed by pressing the Escape key or activating the close button.": "Модальне вікно можна закрити, натиснувши клавішу Esc або кнопку закриття вікна.",
36
+ ", opens captions settings dialog": ", відкриється діалогове вікно налаштування підписів",
37
+ ", opens subtitles settings dialog": ", відкриється діалогове вікно налаштування субтитрів",
38
+ ", opens descriptions settings dialog": ", відкриється діалогове вікно налаштування описів",
39
+ ", selected": ", обраний"
40
+});

+ 84
- 0
static/pygal/js/video-js/lang/vi.js 查看文件

@@ -0,0 +1,84 @@
1
+videojs.addLanguage("vi",{
2
+ "Audio Player": "Trình phát Audio",
3
+ "Video Player": "Trình phát Video",
4
+ "Play": "Phát",
5
+ "Pause": "Tạm dừng",
6
+ "Replay": "Phát lại",
7
+ "Current Time": "Thời gian hiện tại",
8
+ "Duration": "Độ dài",
9
+ "Remaining Time": "Thời gian còn lại",
10
+ "Stream Type": "Kiểu Stream",
11
+ "LIVE": "TRỰC TIẾP",
12
+ "Loaded": "Đã tải",
13
+ "Progress": "Tiến trình",
14
+ "Progress Bar": "Thanh tiến trình",
15
+ "progress bar timing: currentTime={1} duration={2}": "{1} của {2}",
16
+ "Fullscreen": "Toàn màn hình",
17
+ "Non-Fullscreen": "Thoát toàn màn hình",
18
+ "Mute": "Tắt tiếng",
19
+ "Unmute": "Bật âm thanh",
20
+ "Playback Rate": "Tỉ lệ phát lại",
21
+ "Subtitles": "Phụ đề",
22
+ "subtitles off": "tắt phụ đề",
23
+ "Captions": "Chú thích",
24
+ "captions off": "tắt chú thích",
25
+ "Chapters": "Chương",
26
+ "Descriptions": "Mô tả",
27
+ "descriptions off": "tắt mô tả",
28
+ "Audio Track": "Track âm thanh",
29
+ "Volume Level": "Mức âm lượng",
30
+ "You aborted the media playback": "Bạn đã hủy việc phát lại media.",
31
+ "A network error caused the media download to fail part-way.": "Một lỗi mạng dẫn đến việc tải media bị lỗi.",
32
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "Video không tải được, mạng hay server có lỗi hoặc định dạng không được hỗ trợ.",
33
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "Phát media đã bị hủy do một sai lỗi hoặc media sử dụng những tính năng trình duyệt không hỗ trợ.",
34
+ "No compatible source was found for this media.": "Không có nguồn tương thích cho media này.",
35
+ "The media is encrypted and we do not have the keys to decrypt it.": "Media đã được mã hóa và chúng tôi không có để giải mã nó.",
36
+ "Play Video": "Phát Video",
37
+ "Close": "Đóng",
38
+ "Close Modal Dialog": "Đóng cửa sổ",
39
+ "Modal Window": "Cửa sổ",
40
+ "This is a modal window": "Đây là một cửa sổ",
41
+ "This modal can be closed by pressing the Escape key or activating the close button.": "Cửa sổ này có thể thoát bằng việc nhấn phím Esc hoặc kích hoạt nút đóng.",
42
+ ", opens captions settings dialog": ", mở hộp thoại cài đặt chú thích",
43
+ ", opens subtitles settings dialog": ", mở hộp thoại cài đặt phụ đề",
44
+ ", opens descriptions settings dialog": ", mở hộp thoại cài đặt mô tả",
45
+ ", selected": ", đã chọn",
46
+ "captions settings": "cài đặt chú thích",
47
+ "subtitles settings": "cài đặt phụ đề",
48
+ "descriptions settings": "cài đặt mô tả",
49
+ "Text": "Văn bản",
50
+ "White": "Trắng",
51
+ "Black": "Đen",
52
+ "Red": "Đỏ",
53
+ "Green": "Xanh lá cây",
54
+ "Blue": "Xanh da trời",
55
+ "Yellow": "Vàng",
56
+ "Magenta": "Đỏ tươi",
57
+ "Cyan": "Lam",
58
+ "Background": "Nền",
59
+ "Window": "Cửa sổ",
60
+ "Transparent": "Trong suốt",
61
+ "Semi-Transparent": "Bán trong suốt",
62
+ "Opaque": "Mờ",
63
+ "Font Size": "Kích cỡ phông chữ",
64
+ "Text Edge Style": "Dạng viền văn bản",
65
+ "None": "None",
66
+ "Raised": "Raised",
67
+ "Depressed": "Depressed",
68
+ "Uniform": "Uniform",
69
+ "Dropshadow": "Dropshadow",
70
+ "Font Family": "Phông chữ",
71
+ "Proportional Sans-Serif": "Proportional Sans-Serif",
72
+ "Monospace Sans-Serif": "Monospace Sans-Serif",
73
+ "Proportional Serif": "Proportional Serif",
74
+ "Monospace Serif": "Monospace Serif",
75
+ "Casual": "Casual",
76
+ "Script": "Script",
77
+ "Small Caps": "Small Caps",
78
+ "Reset": "Đặt lại",
79
+ "restore all settings to the default values": "khôi phục lại tất cả các cài đặt về giá trị mặc định",
80
+ "Done": "Xong",
81
+ "Caption Settings Dialog": "Hộp thoại cài đặt chú thích",
82
+ "Beginning of dialog window. Escape will cancel and close the window.": "Bắt đầu cửa sổ hộp thoại. Esc sẽ thoát và đóng cửa sổ.",
83
+ "End of dialog window.": "Kết thúc cửa sổ hộp thoại."
84
+});

+ 83
- 0
static/pygal/js/video-js/lang/zh-CN.js 查看文件

@@ -0,0 +1,83 @@
1
+videojs.addLanguage("zh-CN",{
2
+ "Play": "播放",
3
+ "Pause": "暂停",
4
+ "Current Time": "当前时间",
5
+ "Duration": "时长",
6
+ "Remaining Time": "剩余时间",
7
+ "Stream Type": "媒体流类型",
8
+ "LIVE": "直播",
9
+ "Loaded": "加载完毕",
10
+ "Progress": "进度",
11
+ "Fullscreen": "全屏",
12
+ "Non-Fullscreen": "退出全屏",
13
+ "Mute": "静音",
14
+ "Unmute": "取消静音",
15
+ "Playback Rate": "播放速度",
16
+ "Subtitles": "字幕",
17
+ "subtitles off": "关闭字幕",
18
+ "Captions": "内嵌字幕",
19
+ "captions off": "关闭内嵌字幕",
20
+ "Chapters": "节目段落",
21
+ "Close Modal Dialog": "关闭弹窗",
22
+ "Descriptions": "描述",
23
+ "descriptions off": "关闭描述",
24
+ "Audio Track": "音轨",
25
+ "You aborted the media playback": "视频播放被终止",
26
+ "A network error caused the media download to fail part-way.": "网络错误导致视频下载中途失败。",
27
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "视频因格式不支持或者服务器或网络的问题无法加载。",
28
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "由于视频文件损坏或是该视频使用了你的浏览器不支持的功能,播放终止。",
29
+ "No compatible source was found for this media.": "无法找到此视频兼容的源。",
30
+ "The media is encrypted and we do not have the keys to decrypt it.": "视频已加密,无法解密。",
31
+ "Play Video": "播放视频",
32
+ "Close": "关闭",
33
+ "Modal Window": "弹窗",
34
+ "This is a modal window": "这是一个弹窗",
35
+ "This modal can be closed by pressing the Escape key or activating the close button.": "可以按ESC按键或启用关闭按钮来关闭此弹窗。",
36
+ ", opens captions settings dialog": ", 开启标题设置弹窗",
37
+ ", opens subtitles settings dialog": ", 开启字幕设置弹窗",
38
+ ", opens descriptions settings dialog": ", 开启描述设置弹窗",
39
+ ", selected": ", 选择",
40
+ "captions settings": "字幕设定",
41
+ "Audio Player": "音频播放器",
42
+ "Video Player": "视频播放器",
43
+ "Replay": "重播",
44
+ "Progress Bar": "进度小节",
45
+ "Volume Level": "音量",
46
+ "subtitles settings": "字幕设定",
47
+ "descriptions settings": "描述设定",
48
+ "Text": "文字",
49
+ "White": "白",
50
+ "Black": "黑",
51
+ "Red": "红",
52
+ "Green": "绿",
53
+ "Blue": "蓝",
54
+ "Yellow": "黄",
55
+ "Magenta": "紫红",
56
+ "Cyan": "青",
57
+ "Background": "背景",
58
+ "Window": "视窗",
59
+ "Transparent": "透明",
60
+ "Semi-Transparent": "半透明",
61
+ "Opaque": "不透明",
62
+ "Font Size": "字体尺寸",
63
+ "Text Edge Style": "字体边缘样式",
64
+ "None": "无",
65
+ "Raised": "浮雕",
66
+ "Depressed": "压低",
67
+ "Uniform": "均匀",
68
+ "Dropshadow": "下阴影",
69
+ "Font Family": "字体库",
70
+ "Proportional Sans-Serif": "比例无细体",
71
+ "Monospace Sans-Serif": "单间隔无细体",
72
+ "Proportional Serif": "比例细体",
73
+ "Monospace Serif": "单间隔细体",
74
+ "Casual": "舒适",
75
+ "Script": "手写体",
76
+ "Small Caps": "小型大写字体",
77
+ "Reset": "重启",
78
+ "restore all settings to the default values": "恢复全部设定至预设值",
79
+ "Done": "完成",
80
+ "Caption Settings Dialog": "字幕设定视窗",
81
+ "Beginning of dialog window. Escape will cancel and close the window.": "开始对话视窗。离开会取消及关闭视窗",
82
+ "End of dialog window.": "结束对话视窗"
83
+});

+ 83
- 0
static/pygal/js/video-js/lang/zh-TW.js 查看文件

@@ -0,0 +1,83 @@
1
+videojs.addLanguage("zh-TW",{
2
+ "Play": "播放",
3
+ "Pause": "暫停",
4
+ "Current Time": "目前時間",
5
+ "Duration": "總共時間",
6
+ "Remaining Time": "剩餘時間",
7
+ "Stream Type": "串流類型",
8
+ "LIVE": "直播",
9
+ "Loaded": "載入完畢",
10
+ "Progress": "進度",
11
+ "Fullscreen": "全螢幕",
12
+ "Non-Fullscreen": "退出全螢幕",
13
+ "Mute": "靜音",
14
+ "Unmute": "取消靜音",
15
+ "Playback Rate": " 播放速率",
16
+ "Subtitles": "字幕",
17
+ "subtitles off": "關閉字幕",
18
+ "Captions": "內嵌字幕",
19
+ "captions off": "關閉內嵌字幕",
20
+ "Chapters": "章節",
21
+ "Close Modal Dialog": "關閉彈窗",
22
+ "Descriptions": "描述",
23
+ "descriptions off": "關閉描述",
24
+ "Audio Track": "音軌",
25
+ "You aborted the media playback": "影片播放已終止",
26
+ "A network error caused the media download to fail part-way.": "網路錯誤導致影片下載失敗。",
27
+ "The media could not be loaded, either because the server or network failed or because the format is not supported.": "影片因格式不支援或者伺服器或網路的問題無法載入。",
28
+ "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "由於影片檔案損毀或是該影片使用了您的瀏覽器不支援的功能,播放終止。",
29
+ "No compatible source was found for this media.": "無法找到相容此影片的來源。",
30
+ "The media is encrypted and we do not have the keys to decrypt it.": "影片已加密,無法解密。",
31
+ "Play Video": "播放影片",
32
+ "Close": "關閉",
33
+ "Modal Window": "對話框",
34
+ "This is a modal window": "這是一個對話框",
35
+ "This modal can be closed by pressing the Escape key or activating the close button.": "可以按ESC按鍵或啟用關閉按鈕來關閉此對話框。",
36
+ ", opens captions settings dialog": ", 開啟標題設定對話框",
37
+ ", opens subtitles settings dialog": ", 開啟字幕設定對話框",
38
+ ", opens descriptions settings dialog": ", 開啟描述設定對話框",
39
+ ", selected": ", 選擇",
40
+ "captions settings": "字幕設定",
41
+ "Audio Player": "音頻播放器",
42
+ "Video Player": "視頻播放器",
43
+ "Replay": "重播",
44
+ "Progress Bar": "進度小節",
45
+ "Volume Level": "音量",
46
+ "subtitles settings": "字幕設定",
47
+ "descriptions settings": "描述設定",
48
+ "Text": "文字",
49
+ "White": "白",
50
+ "Black": "黑",
51
+ "Red": "紅",
52
+ "Green": "綠",
53
+ "Blue": "藍",
54
+ "Yellow": "黃",
55
+ "Magenta": "紫紅",
56
+ "Cyan": "青",
57
+ "Background": "背景",
58
+ "Window": "視窗",
59
+ "Transparent": "透明",
60
+ "Semi-Transparent": "半透明",
61
+ "Opaque": "不透明",
62
+ "Font Size": "字型尺寸",
63
+ "Text Edge Style": "字型邊緣樣式",
64
+ "None": "無",
65
+ "Raised": "浮雕",
66
+ "Depressed": "壓低",
67
+ "Uniform": "均勻",
68
+ "Dropshadow": "下陰影",
69
+ "Font Family": "字型庫",
70
+ "Proportional Sans-Serif": "比例無細體",
71
+ "Monospace Sans-Serif": "單間隔無細體",
72
+ "Proportional Serif": "比例細體",
73
+ "Monospace Serif": "單間隔細體",
74
+ "Casual": "輕便的",
75
+ "Script": "手寫體",
76
+ "Small Caps": "小型大寫字體",
77
+ "Reset": "重置",
78
+ "restore all settings to the default values": "恢復全部設定至預設值",
79
+ "Done": "完成",
80
+ "Caption Settings Dialog": "字幕設定視窗",
81
+ "Beginning of dialog window. Escape will cancel and close the window.": "開始對話視窗。離開會取消及關閉視窗",
82
+ "End of dialog window.": "結束對話視窗"
83
+});

+ 1279
- 0
static/pygal/js/video-js/video-js.css
文件差异内容过多而无法显示
查看文件


+ 1
- 0
static/pygal/js/video-js/video-js.min.css
文件差异内容过多而无法显示
查看文件


+ 41033
- 0
static/pygal/js/video-js/video.cjs.js
文件差异内容过多而无法显示
查看文件


+ 41031
- 0
static/pygal/js/video-js/video.es.js
文件差异内容过多而无法显示
查看文件


+ 48091
- 0
static/pygal/js/video-js/video.js
文件差异内容过多而无法显示
查看文件


+ 12
- 0
static/pygal/js/video-js/video.min.js
文件差异内容过多而无法显示
查看文件


+ 29
- 0
templates/pygal/audio.html 查看文件

@@ -0,0 +1,29 @@
1
+{% extends "themes/"|add:settings.page_theme|add:"/base.html" %}
2
+
3
+{% block head_extensions %}
4
+  {% include 'pygal/repeat_head.html' %}
5
+{% endblock head_extensions %}
6
+
7
+
8
+{% block content %}
9
+  {% with item=item.prv %}
10
+    {% include "pygal/thumb.html" %}
11
+  {% endwith %}
12
+  
13
+  <div id="openModal" class="image">
14
+    {% with bar=item.tagbar slim=True %}{% include 'themes/'|add:settings.page_theme|add:'/menubar.html' %}{% endwith %}
15
+    <div class="image_bg">
16
+        <h1>{{ item.heading }}</h1>
17
+        <audio controls preload{% if item.is_repeatview %} autoplay{% endif %} controlsList="nodownload">
18
+          <source src="{{ item.url_item|safe }}" type="{{ item.mime_type }}">
19
+          Your browser does not support the audio element.
20
+        </audio>
21
+    </div>
22
+  </div>
23
+
24
+  {% with item=item.nxt %}
25
+    {% include "pygal/thumb.html" %}
26
+  {% endwith %}
27
+
28
+  <div class="clearfix"></div>
29
+{% endblock content %}

+ 8
- 0
templates/pygal/empty.html 查看文件

@@ -0,0 +1,8 @@
1
+{% extends "themes/"|add:settings.page_theme|add:"/base.html" %}
2
+
3
+{% block head_extensions %}
4
+  {% include 'pygal/repeat_head.html' %}
5
+{% endblock head_extensions %}
6
+
7
+{% block content %}
8
+{% endblock content %}

+ 9
- 0
templates/pygal/help.html 查看文件

@@ -0,0 +1,9 @@
1
+{% extends "themes/"|add:settings.page_theme|add:"/base.html" %}
2
+
3
+{% block head_extensions %}
4
+{% endblock head_extensions %}
5
+
6
+
7
+{% block content %}
8
+{{ help_content|safe }}
9
+{% endblock content %}

+ 42
- 0
templates/pygal/image.html 查看文件

@@ -0,0 +1,42 @@
1
+{% extends "themes/"|add:settings.page_theme|add:"/base.html" %}
2
+{% load tag_help %}
3
+
4
+{% block head_extensions %}
5
+  {% include 'pygal/repeat_head.html' %}
6
+{% endblock head_extensions %}
7
+
8
+{% block modal %}
9
+    <div id="openModal" class="modalDialog">
10
+      <div class="image_modal">
11
+        <img class="webnail_modal" src="{{ item.url_webnail|safe }}">
12
+      </div>
13
+      <a href="{{ item.url_userview|safe }}" class="close">X</a>
14
+    </div>
15
+{% endblock modal %}
16
+
17
+
18
+{% block content %}
19
+  {% with item=item.prv %}
20
+    {% include "pygal/thumb.html" %}
21
+  {% endwith %}
22
+  
23
+  <div class="image">
24
+    {% with bar=item.tagbar slim=True %}{% include 'themes/'|add:settings.page_theme|add:'/menubar.html' %}{% endwith %}
25
+    <div class="image_bg">
26
+      <div class="container">
27
+        <a href="{{ item.url_item|safe }}"><img class="webnail" src="{{ item.url_webnail|safe }}"></a>
28
+          {% for tag in item.tags %}
29
+          	{% if tag.has_valid_coordinates %} 
30
+        <a class="tag" title="{{ tag.text }}" {% if item.may_modify %}href="{% tag_editurl tag %}"{% endif %} style="width: {% tag_width tag item %}%; height: {% tag_height tag item %}%; left: {% tag_pos_x tag item %}%; top: {% tag_pos_y tag item %}%;"></a>
31
+          	{% endif %}
32
+          {% endfor %}
33
+        </div>
34
+    </div>
35
+  </div>
36
+
37
+  {% with item=item.nxt %}
38
+    {% include "pygal/thumb.html" %}
39
+  {% endwith %}
40
+
41
+  <div class="clearfix"></div>
42
+{% endblock content %}

+ 40
- 0
templates/pygal/infoview.html 查看文件

@@ -0,0 +1,40 @@
1
+{% extends "themes/"|add:settings.page_theme|add:"/base.html" %}
2
+
3
+{% block content %}
4
+      <h1>{{ heading.general }}</h1>
5
+      <table>
6
+        <tbody>
7
+        {% for line in item.general_information %}
8
+          <tr>
9
+          <td>{{ line.description }}:</td>
10
+          <td style="text-align:right;">{{ line.data }}</td>
11
+          </tr>
12
+        {% endfor %}
13
+        </tbody>
14
+      </table>
15
+      <h1>{{ heading.information }}</h1>
16
+      <table>
17
+        <tbody>
18
+        {% for line in item.item_information %}
19
+          <tr>
20
+          <td>{{ line.description }}:</td>
21
+          <td style="text-align:right;">{% if line.url %}<a href="{{ line.url|safe }}">{% endif %}{{ line.data }}{% if line.url %}</a>{% endif %}</td>
22
+          </tr>
23
+        {% endfor %}
24
+        </tbody>
25
+      </table>
26
+      {% if item.database_information|length > 0 %}
27
+        <h1>{{ heading.database }}</h1>
28
+        <table>
29
+          <tbody>
30
+          {% for line in item.database_information %}
31
+            <tr>
32
+            <td>{{ line.description }}:</td>
33
+            <td style="text-align:right;">{% if line.url %}<a href="{{ line.url|safe }}">{% endif %}{{ line.data }}{% if line.url %}</a>{% endif %}</td>
34
+            </tr>
35
+          {% endfor %}
36
+          </tbody>
37
+        </table>
38
+      {% endif %}
39
+  <div class="clearfix"></div>
40
+{% endblock content %}

+ 37
- 0
templates/pygal/other.html 查看文件

@@ -0,0 +1,37 @@
1
+{% extends "themes/"|add:settings.page_theme|add:"/base.html" %}
2
+
3
+{% block head_extensions %}
4
+  {% include 'pygal/repeat_head.html' %}
5
+{% endblock head_extensions %}
6
+
7
+
8
+{% block content %}
9
+  {% with item=item.prv %}
10
+    {% include "pygal/thumb.html" %}
11
+  {% endwith %}
12
+  
13
+  <div class="image" id="openModal">
14
+    {% with bar=item.tagbar slim=True %}{% include 'themes/'|add:settings.page_theme|add:'/menubar.html' %}{% endwith %}
15
+      <div class="image_bg">
16
+        <a href="{{ item.url_item|safe }}">
17
+          <h1>{{ item.heading }}</h1>
18
+        </a>
19
+        <table>
20
+          <tbody>
21
+            {% for line in item.item_information %}
22
+              <tr>
23
+                <td>{{ line.description }}:</td>
24
+                <td style="text-align:right;">{% if line.url %}<a href="{{ line.url|safe }}">{% endif %}{{ line.data }}{% if line.url %}</a>{% endif %}</td>
25
+              </tr>
26
+            {% endfor %}
27
+          </tbody>
28
+        </table>
29
+      </div>
30
+  </div>
31
+
32
+  {% with item=item.nxt %}
33
+    {% include "pygal/thumb.html" %}
34
+  {% endwith %}
35
+
36
+  <div class="clearfix"></div>
37
+{% endblock content %}

+ 20
- 0
templates/pygal/overview.html 查看文件

@@ -0,0 +1,20 @@
1
+{% extends "themes/"|add:settings.page_theme|add:"/base.html" %}
2
+
3
+{% block content %}
4
+  {% for entry in item.item_list %}
5
+    <div id="{{ entry.name }}" style="height: {{ thumbnail_size|add:100 }}px;width: {{ thumbnail_size|add:12 }}px;float: left; margin: 6px">
6
+      <div class="itemname">{{ entry.name }}</div>
7
+      <div class="datetime">{{ entry.date_txt }}</div>
8
+      {% with bar=entry.actionbar slim=True reduced=True %}{% include 'themes/'|add:settings.page_theme|add:'/menubar.html' %}{% endwith %}
9
+      <div class="image" style="margin: 0px;">
10
+        <a href="{{ entry.url_userview|safe }}">
11
+          <div class="image_bg">
12
+            <img style="max-width:{{ thumbnail_size }}px;max-height:{{ thumbnail_size }}px" src="{{ entry.url_thumbnail|safe }}" alt="{{ entry.name }}">
13
+          </div>
14
+        </a>
15
+      </div>
16
+    </div>  
17
+  {% endfor %}
18
+
19
+  <div class="clearfix"></div>
20
+{% endblock content %}

+ 26
- 0
templates/pygal/profile.html 查看文件

@@ -0,0 +1,26 @@
1
+{% extends "themes/"|add:settings.page_theme|add:"/base.html" %}
2
+{% load i18n %}
3
+
4
+{% block content %}
5
+  <form action="" method="post">
6
+    {% csrf_token %}
7
+    {% include 'users/profile_formdata.html' %}
8
+    <h1>{% trans "Xnail-Size" %}</h1>
9
+    <p><label for="id_thumbnail">{% trans "Thumbnail Size" %}:</label>
10
+    <select name="thumbnail_size" id="id_thumbnail">
11
+      {% for size in THUMBNAIL_SIZES %}
12
+        <option value="{{ size }}"{% if size == thumbnail_size %} selected{% endif %}>{{ size }}</option>
13
+      {% endfor %}
14
+    </select>
15
+
16
+    <p><label for="id_webnail">{% trans "Webnail Size" %}:</label>
17
+    <select name="webnail_size" id="id_webnail">
18
+      {% for size in WEBNAIL_SIZES %}
19
+        <option value="{{ size }}"{% if size == webnail_size %} selected{% endif %}>{{ size }}</option>
20
+      {% endfor %}
21
+    </select>
22
+
23
+    <input type="submit" value="{% trans "Save" %}" class="button" />
24
+  </form>
25
+
26
+{% endblock content %}

+ 5
- 0
templates/pygal/repeat_head.html 查看文件

@@ -0,0 +1,5 @@
1
+{% load l10n %}
2
+
3
+{% if item.is_repeatview %}
4
+  <meta http-equiv="refresh" content="{{ item.duration|unlocalize }}; url={{ item.nxt.url_repeatview|safe }}">
5
+{% endif %}

+ 58
- 0
templates/pygal/tagedit.html 查看文件

@@ -0,0 +1,58 @@
1
+{% extends "themes/"|add:settings.page_theme|add:"/base.html" %}
2
+{% load static %}
3
+{% load i18n %}
4
+{% load l10n %}
5
+
6
+{% block head_extensions %}
7
+	{% if enable_tag_area_selection %}
8
+    <link rel="stylesheet" type="text/css" href="{% static 'pygal/js/imgareaselect-js/css/imgareaselect-default.css' %}" />
9
+	<script src="{% static 'pygal/js/jquery.min.js' %}"></script>
10
+    <script src="{% static 'pygal/js/imgareaselect-js/jquery.imgareaselect.pack.js' %}"></script>
11
+    <script>
12
+      function preview(img, selection) {}
13
+        $(function () {
14
+          $('#tag_img').imgAreaSelect({
15
+		{% if form.has_valid_coordinates %}
16
+	            x1: {% widthratio form.value_tl_x factor_to_original 1 %},
17
+	            y1: {% widthratio form.value_tl_y factor_to_original 1 %},
18
+	            x2: {% widthratio form.value_br_x factor_to_original 1 %},
19
+	            y2: {% widthratio form.value_br_y factor_to_original 1 %},
20
+		{% endif %}
21
+        	  onSelectEnd: function (img, selection) {
22
+            $('input[name="topleft_x"]').val(Math.round(selection.x1 * {{ factor_to_original|unlocalize }}));
23
+            $('input[name="topleft_y"]').val(Math.round(selection.y1 * {{ factor_to_original|unlocalize }}));
24
+            $('input[name="bottomright_x"]').val(Math.round(selection.x2 * {{ factor_to_original|unlocalize }}));
25
+            $('input[name="bottomright_y"]').val(Math.round(selection.y2 * {{ factor_to_original|unlocalize }}));
26
+          }
27
+        });
28
+      });
29
+    </script>
30
+	{% endif %}
31
+{% endblock head_extensions %}
32
+
33
+
34
+{% block content %}
35
+	<form class="form" method="post">
36
+		{% csrf_token %}
37
+		<input type="hidden" name="next" id="id_next" value="{{ next }}">
38
+		{{ form.as_p }}
39
+		{% if enable_tag_area_selection %}
40
+        <p><img id="tag_img" width="{{ tag_img_width }}" height="{{ tag_img_height }}" src="{{ item.url_webnail }}" alt="{{ item.name }}"></p>
41
+			{% if form.has_valid_coordinates %}
42
+		<input type="hidden" name="topleft_x" id="id_topleft_x" value="{{ form.value_tl_x }}">
43
+		<input type="hidden" name="topleft_y" id="id_topleft_y" value="{{ form.value_tl_y }}">
44
+		<input type="hidden" name="bottomright_x" id="id_bottomright_x" value="{{ form.value_br_x }}">
45
+		<input type="hidden" name="bottomright_y" id="id_bottomright_y" value="{{ form.value_br_y }}">
46
+			{% else %}
47
+		<input type="hidden" name="topleft_x" id="id_topleft_x">
48
+		<input type="hidden" name="topleft_y" id="id_topleft_y">
49
+		<input type="hidden" name="bottomright_x" id="id_bottomright_x">
50
+		<input type="hidden" name="bottomright_y" id="id_bottomright_y">
51
+			{% endif %}
52
+		{% endif %}
53
+		<input type="submit" name="save" value="{% trans 'Save' %}" class="button" />
54
+		{% if enable_delete_button %}
55
+		<input type="submit" name="delete" value="{% trans 'Delete' %}" class="button" />
56
+		{% endif %}
57
+	</form>
58
+{% endblock content %}

+ 10
- 0
templates/pygal/thumb.html 查看文件

@@ -0,0 +1,10 @@
1
+  <div id="{{ item.name }}" style="height: {{ thumbnail_size }}px; float: left; margin: 6px">
2
+    <div class="itemname">{{ item.name }}</div>
3
+    <div class="image" style="margin: 0px;">
4
+      <a href="{{ item.url_userview|safe }}">
5
+        <div class="image_bg">
6
+          <img style="max-width:{{ thumbnail_size }}px;max-height:{{ thumbnail_size }}px" src="{{ item.url_thumbnail|safe }}" alt="{{ item.name }}">
7
+        </div>
8
+      </a>
9
+    </div>
10
+  </div>

+ 59
- 0
templates/pygal/video.html 查看文件

@@ -0,0 +1,59 @@
1
+{% extends "themes/"|add:settings.page_theme|add:"/base.html" %}
2
+{% load static %}
3
+
4
+{% block head_extensions %}
5
+  {% include 'pygal/repeat_head.html' %}
6
+
7
+  <link href="{% static 'pygal/js/video-js/video-js.css' %}" rel="stylesheet">
8
+  <script src="{% static 'pygal/js/video-js/video.js' %}"></script>
9
+  <script src="{% static 'pygal/js/jquery.min.js' %}"></script>
10
+{% endblock head_extensions %}
11
+
12
+{% block modal %}
13
+    <div id="openModal" class="modalDialog">
14
+      <div class="image_modal">
15
+  {% if item.use_internal_player %}
16
+      <video id="MY_VIDEO_2" class="video-js vjs-default-skin webnail_modal" data-setup="{}"{% if item.is_repeatview %} autoplay{% endif %} controls poster="{{ item.url_webnail|safe }}">
17
+        <source src="{{ item.url_item|safe }}" type='{{ item.mime_type }}'>
18
+        <p class="vjs-no-js">To view {{ main.name }} please enable JavaScript, and consider upgrading to a web browser that <a href="http://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a></p>
19
+      </video>
20
+  {% else %}
21
+      <img class="webnail" src="{{ item.url_webnail|safe }}" alt="{{ item.name }}">
22
+  {% endif %}
23
+      </div>
24
+      <a href="{{ item.url_userview|safe }}" class="close">X</a>
25
+    </div>
26
+{% endblock modal %}
27
+
28
+
29
+{% block content %}
30
+  {% with item=item.prv %}
31
+    {% include "pygal/thumb.html" %}
32
+  {% endwith %}
33
+  
34
+  {% if item.use_internal_player %}
35
+  <div class="image">
36
+    {% with bar=item.tagbar slim=True %}{% include 'themes/'|add:settings.page_theme|add:'/menubar.html' %}{% endwith %}
37
+    <div class="image_bg" >
38
+      <video id="MY_VIDEO_1" class="video-js vjs-default-skin webnail"  data-setup="{}" controls poster="{{ item.url_webnail|safe }}" preload width="{{ item.webnail_width }}" height="{{ item.webnail_height }}">
39
+        <source src="{{ item.url_item|safe }}" type='{{ item.mime_type }}'>
40
+        <p class="vjs-no-js">To view {{ main.name }} please enable JavaScript, and consider upgrading to a web browser that <a href="http://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a></p>
41
+      </video>
42
+    </div>
43
+  </div>
44
+  {% else %}
45
+  <div id="main" class="image">
46
+    <div class="image_bg">
47
+      <a href="{{ item.url_item|safe }}"><img class="webnail" src="{{ item.url_webnail|safe }}" alt="{{ item.name }}"></a>
48
+    </div>
49
+  </div>
50
+  {% endif %}
51
+
52
+
53
+
54
+  {% with item=item.nxt %}
55
+    {% include "pygal/thumb.html" %}
56
+  {% endwith %}
57
+
58
+  <div class="clearfix"></div>
59
+{% endblock content %}

+ 0
- 0
templatetags/__init__.py 查看文件


+ 29
- 0
templatetags/tag_help.py 查看文件

@@ -0,0 +1,29 @@
1
+from django import template
2
+import pygal
3
+
4
+register = template.Library()
5
+
6
+
7
+@register.simple_tag(name='tag_pos_x')
8
+def tag_pos_x(tag, wrapper_instance):
9
+    return 100. * (tag.topleft_x) / wrapper_instance.width
10
+
11
+
12
+@register.simple_tag(name='tag_pos_y')
13
+def tag_pos_y(tag, wrapper_instance):
14
+    return 100. * (tag.topleft_y) / wrapper_instance.height
15
+
16
+
17
+@register.simple_tag(name='tag_width')
18
+def tag_width(tag, wrapper_instance):
19
+    return 100. * (tag.bottomright_x - tag.topleft_x) / wrapper_instance.width
20
+
21
+
22
+@register.simple_tag(name='tag_height')
23
+def tag_height(tag, wrapper_instance):
24
+    return 100. * (tag.bottomright_y - tag.topleft_y) / wrapper_instance.height
25
+
26
+
27
+@register.simple_tag(name='tag_editurl', takes_context=True)
28
+def tag_editurl(context, tag):
29
+    return pygal.url_tagedit(context['request'], tag.id)

+ 3
- 0
tests.py 查看文件

@@ -0,0 +1,3 @@
1
+from django.test import TestCase
2
+
3
+# Create your tests here.

+ 26
- 0
urls.py 查看文件

@@ -0,0 +1,26 @@
1
+from django.shortcuts import redirect
2
+from django.urls import path
3
+from django.urls.base import reverse
4
+from . import views
5
+
6
+
7
+urlpatterns = [
8
+    #
9
+    # settings and other stuff
10
+    #
11
+    path('', lambda request: redirect(reverse('pygal-view', kwargs={'viewtype': 'user'}), permanent=False)),
12
+    path('profile', views.pygal_profile, name='pygal-profile'),
13
+    path('helpview/<str:page>', views.pygal_helpview, name='pygal-helpview'),
14
+    #
15
+    # changes
16
+    #
17
+    path('addtag/<path:rel_path>', views.pygal_addtag, name='pygal-addtag'),
18
+    path('edittag/<int:tag_id>', views.pygal_tagedit, name='pygal-edittag'),
19
+    path('setfavourite/<int:set_favourite>/<path:rel_path>', views.pygal_favourite, name='pygal-setfavourite'),
20
+    #
21
+    # views
22
+    #
23
+    path('userview/search', views.pygal_responses, name='search'),
24
+    path('<str:responsetype>/<str:datatype>/', views.pygal_responses, name='pygal-responses'),
25
+    path('<str:responsetype>/<str:datatype>/<path:rel_path>', views.pygal_responses, name='pygal-responses'),
26
+]

+ 306
- 0
views/__init__.py 查看文件

@@ -0,0 +1,306 @@
1
+from ..context import context_adaption
2
+from django.conf import settings
3
+from django.contrib import messages
4
+from django.contrib.auth.decorators import login_required
5
+from django.http import FileResponse, HttpResponseNotAllowed
6
+from django.shortcuts import render, HttpResponse, redirect
7
+from django.urls.base import reverse
8
+from django.utils.translation import gettext as _
9
+from ..forms import TagForm
10
+import fstools
11
+from ..help import help_pages
12
+from .image import get_image_instance, other
13
+from .infoviews import get_wrapper_instance as get_infoview_wrapper_instance
14
+import mimetypes
15
+from ..models import get_item_by_rel_path, get_item_type, TYPE_IMAGE, Tag, Item
16
+import os
17
+import pygal
18
+from ..queries import search_result_query
19
+import tempfile
20
+from themes import Context
21
+from users.views import profile_pre_actions, profile_post_actions
22
+from users.forms import UserProfileFormLanguageOnly
23
+from .userviews import get_wrapper_instance as get_userview_wrapper_instance
24
+import zipfile
25
+from pygal.views.userviews import query_view
26
+from pygal.views.image import mm_image
27
+
28
+
29
+def pygal_item_does_not_exist(request, context):
30
+    context.set_additional_title(_('Page does not exist!'))
31
+    messages.error(request, _('The Item for the given URL does not exist.'))
32
+    return render(request, 'pygal/empty.html', context=context)
33
+
34
+
35
+def pygal_access_denied(request, context):
36
+    context.set_additional_title(_('Access denied!'))
37
+    messages.error(request, _('Access Denied: You don\'t have access rights.'))
38
+    return render(request, 'pygal/empty.html', context=context)
39
+
40
+
41
+def get_next(request):
42
+    if not request.POST:
43
+        return request.GET.get('next', '/')
44
+    else:
45
+        return request.POST.get('next', '/')
46
+
47
+
48
+def pygal_responses(request, responsetype=pygal.RESP_TYPE_USERVIEW, datatype=pygal.DATA_TYPE_SEARCH, rel_path=''):
49
+    # raw and scaled items
50
+    if responsetype in [pygal.RESP_TYPE_RAWITEM, pygal.RESP_TYPE_WEBNAIL, pygal.RESP_TYPE_THUMBNAIL]:
51
+        return pygal_item(request, responsetype, rel_path)
52
+    # userview and infoview for items
53
+    elif responsetype in [pygal.RESP_TYPE_USERVIEW, pygal.RESP_TYPE_INFOVIEW] and datatype == pygal.DATA_TYPE_ITEM:
54
+        return pygal_userview_infoview(request, responsetype, rel_path)
55
+    # download
56
+    elif responsetype == pygal.RESP_TYPE_DOWNLOAD:
57
+        return pygal_download(request, datatype, rel_path)
58
+    # search
59
+    elif responsetype in [pygal.RESP_TYPE_USERVIEW, pygal.RESP_TYPE_INFOVIEW] and datatype == pygal.DATA_TYPE_SEARCH:
60
+        if not rel_path:
61
+            return pygal_search(request, rel_path)
62
+        else:
63
+            return pygal_userview_infoview(request, responsetype, rel_path)
64
+    else:
65
+        d = {
66
+            'responsetype': responsetype,
67
+            'datatype': datatype,
68
+        }
69
+        messages.error(request, _('No response implemented in pygal.views.pygal_responses for responsetype="%(responsetype)s" and datatype="%(datatype)s".') % d)
70
+        return redirect('/')
71
+
72
+
73
+def pygal_search(request, rel_path):
74
+    context = Context(request)      # needs to be executed first because of time mesurement
75
+    w = query_view(request, search_result_query(request))
76
+    context_adaption(context, request, '', wrapper_instance=w)
77
+    context.set_additional_title(w.name)
78
+    return w.render(context)
79
+
80
+
81
+def pygal_download(request, datatype, rel_path):
82
+    full_path = pygal.get_full_path(rel_path)
83
+    if os.path.isfile(full_path):
84
+        # Download a file
85
+        temp = open(full_path, 'rb')
86
+        fn = os.path.basename(full_path)
87
+    else:
88
+        if pygal.is_favouriteview(request) or pygal.is_searchview(request) and rel_path == '':
89
+            if pygal.is_favouriteview(request):
90
+                fn = 'favourites.zip'
91
+            else:
92
+                fn = 'search.zip'
93
+            # Download the favourites or search results
94
+            fp_list = []
95
+            for i in search_result_query(request):
96
+                if i.may_read(request.user):
97
+                    fp_list.append(pygal.get_full_path(i.rel_path))
98
+        else:
99
+            # Download the folder full_path
100
+            fn = (os.path.basename(full_path) or 'root') + '.zip'
101
+            fp_list = []
102
+            for i in get_item_by_rel_path(rel_path).all_itemslist():
103
+                if i.may_read(request.user):
104
+                    fp_list.append(pygal.get_full_path(i.rel_path))
105
+        #
106
+        # Create the Archive from fp_list
107
+        #
108
+        flat = pygal.is_flat(request)
109
+        temp = tempfile.TemporaryFile(dir=settings.TEMP_ROOT)
110
+        archive = zipfile.ZipFile(temp, 'w', zipfile.ZIP_STORED)
111
+        for fp in fp_list:
112
+            archive.write(fp, os.path.basename(pygal.get_rel_path(fp)) if flat else pygal.get_rel_path(fp))
113
+        archive.close()
114
+        temp.seek(0)
115
+    #
116
+    # return file response
117
+    #
118
+    response = FileResponse(temp, content_type=mimetypes.types_map.get(os.path.splitext(fn)[1]))
119
+    response['Content-Disposition'] = 'attachment; filename="%s"' % fn
120
+    return response
121
+
122
+
123
+def pygal_helpview(request, page='main'):
124
+    context = Context(request)      # needs to be executed first because of time mesurement
125
+    help_content = help_pages[page]
126
+    context_adaption(
127
+        context,                            # the base context
128
+        request,                            # the request object to be used in context_adaption
129
+        rel_path='',                      # the current help_page to identify which taskbar entry has to be highlighted
130
+        title=_('Help'),                    # the title for the page (template)
131
+        current_help_page=page,
132
+    )
133
+    context['help_content'] = help_content  # the help content itself (template)
134
+    return render(request, 'pygal/help.html', context=context)
135
+
136
+    context = Context(request)      # needs to be executed first because of time mesurement
137
+    context_adaption(context, request, '', title=_('Help'))
138
+    return help_data(request, context)
139
+
140
+
141
+def pygal_userview_infoview(request, responsetype, rel_path):
142
+    context = Context(request)      # needs to be executed first because of time mesurement
143
+    full_path = pygal.get_full_path(rel_path)
144
+    try:
145
+        if responsetype == pygal.RESP_TYPE_INFOVIEW:
146
+            w = get_infoview_wrapper_instance(full_path, request)
147
+        else:
148
+            w = get_userview_wrapper_instance(full_path, request)
149
+    except LookupError:
150
+        context_adaption(context, request, '')
151
+        return pygal_item_does_not_exist(request, context)
152
+    context_adaption(context, request, rel_path, wrapper_instance=w)
153
+    #
154
+    if w.may_read():
155
+        context.set_additional_title(w.name)
156
+        return w.render(context)
157
+    else:
158
+        return pygal_access_denied(request, context)
159
+
160
+
161
+def pygal_item(request, responsetype, rel_path):
162
+    full_path = pygal.get_full_path(rel_path)
163
+    i = get_item_by_rel_path(rel_path)
164
+    if not i.may_read(request.user):
165
+        im = other(full_path, request)  # This will return a mime icon instead of the image itself
166
+    else:
167
+        im = get_image_instance(full_path, request)
168
+    #
169
+    if responsetype == pygal.RESP_TYPE_THUMBNAIL:
170
+        return HttpResponse(im.thumbnail_picture(), content_type=im.mime_type_xnails)
171
+    elif responsetype == pygal.RESP_TYPE_WEBNAIL:
172
+        return HttpResponse(im.webnail_picture(), content_type=im.mime_type_xnails)
173
+    else:
174
+        if i.may_read(request.user):
175
+            mimetypes.init()
176
+            mime_type = mimetypes.types_map.get(os.path.splitext(full_path)[1])
177
+            data = open(full_path, 'rb').read()
178
+            return HttpResponse(data, content_type=mime_type)
179
+        else:
180
+            im = mm_image(os.path.join(os.path.dirname(__file__), 'forbidden.png'))
181
+            if responsetype == pygal.RESP_TYPE_THUMBNAIL:
182
+                im.resize(int(pygal.get_thumbnail_size(request) * .75))
183
+            return HttpResponse(im.image_data(), content_type='image/png')
184
+
185
+
186
+@login_required
187
+def pygal_profile(request):
188
+    context = Context(request)      # needs to be executed first because of time mesurement
189
+    current_thumbnail_size = pygal.get_thumbnail_size(request)
190
+    current_webnail_size = pygal.get_webnail_size(request)
191
+    profile_pre_actions(request, context, UserProfileFormLanguageOnly)
192
+    context_adaption(context, request, '', title=_('Profile for %(username)s') % {'username': request.user.username})
193
+    context['THUMBNAIL_SIZES'] = settings.THUMBNAIL_SIZES
194
+    context['thumbnail_size'] = current_thumbnail_size
195
+    context['WEBNAIL_SIZES'] = settings.WEBNAIL_SIZES
196
+    context['webnail_size'] = current_webnail_size
197
+    if request.POST:
198
+        # store thumbnail size, if changed
199
+        thumbnail_size = int(request.POST.get('thumbnail_size'))
200
+        if current_thumbnail_size != thumbnail_size:
201
+            pygal.set_thumbnail_size(request, thumbnail_size)
202
+            current_thumbnail_size = thumbnail_size
203
+            messages.info(request, _('The Thumbnail Size was set to "%d"') % (thumbnail_size))
204
+        # store webnail size, if changed
205
+        webnail_size = int(request.POST.get('webnail_size'))
206
+        if current_webnail_size != webnail_size:
207
+            pygal.set_webnail_size(request, webnail_size)
208
+            current_webnail_size = webnail_size
209
+            messages.info(request, _('The Webnail Size was set to "%d"') % (webnail_size))
210
+        return profile_post_actions(request, context)
211
+    else:
212
+        return render(request, 'pygal/profile.html', context=context)
213
+
214
+
215
+def tag_context_adaption(context, w, request, enable_tag_area_selection):
216
+    context['item'] = w
217
+    context['next'] = get_next(request)
218
+    context['enable_tag_area_selection'] = enable_tag_area_selection
219
+    if enable_tag_area_selection:
220
+        i_w, i_h = w.width, w.height
221
+        max_size = int(pygal.get_webnail_size(request) / 2)
222
+        factor_to_original = max(i_w, i_h) / max_size
223
+        context['factor_to_original'] = factor_to_original
224
+        context['tag_img_width'] = int(i_w / factor_to_original)
225
+        context['tag_img_height'] = int(i_h / factor_to_original)
226
+
227
+
228
+def pygal_addtag(request, rel_path):
229
+    context = Context(request)      # needs to be executed first because of time mesurement
230
+    full_path = pygal.get_full_path(rel_path)
231
+    w = get_userview_wrapper_instance(full_path, request)
232
+    if not w.may_modify():
233
+        messages.error(request, _('You don\'t have the rights to add a Tag to this Item.'))
234
+        return redirect(get_next(request))
235
+    else:
236
+        context_adaption(
237
+            context,
238
+            request,
239
+            rel_path,
240
+            title=_('Add Tag for %s') % w.name
241
+        )
242
+        tag_context_adaption(context, w, request, get_item_type(full_path) in [TYPE_IMAGE])
243
+        if request.POST:
244
+            tag = Tag(item=w.item)
245
+            form = TagForm(request.POST, instance=tag, factor_to_original=context['factor_to_original'])
246
+            if form.is_valid():
247
+                form.save()
248
+                messages.info(request, _('Thanks for adding a Tag to %s') % w.name)
249
+                return redirect(get_next(request))
250
+        else:
251
+            form = TagForm(factor_to_original=context['factor_to_original'])
252
+        context['form'] = form
253
+        return render(request, 'pygal/tagedit.html', context=context)
254
+
255
+
256
+def pygal_tagedit(request, tag_id):
257
+    context = Context(request)      # needs to be executed first because of time mesurement
258
+    try:
259
+        t = Tag.objects.get(pk=tag_id)
260
+    except Tag.DoesNotExist:
261
+        messages.error(request, _('Tag %d does not exist!') % tag_id)
262
+        return redirect(get_next(request))
263
+    if not t.item.may_modify(request.user):
264
+        messages.error(request, _('You don\'t have the rights to change Tag %(tag_id)d.') % {'tag_id': tag_id})
265
+        return redirect(get_next(request))
266
+    else:
267
+        w = get_userview_wrapper_instance(pygal.get_full_path(t.item.rel_path), request)
268
+        context_adaption(
269
+            context,
270
+            request,
271
+            t.item.rel_path,
272
+            title=_('Edit Tag %(tag_id)d for %(item_name)s') % {'tag_id': tag_id, 'item_name': w.name}
273
+        )
274
+        tag_context_adaption(context, w, request, t.item.type in [TYPE_IMAGE])
275
+        context['enable_delete_button'] = True
276
+        if request.POST:
277
+            if request.POST.get('save'):
278
+                form = TagForm(request.POST, instance=t, factor_to_original=context['factor_to_original'])
279
+                if form.is_valid():
280
+                    form.save()
281
+                    messages.info(request, _('Thanks for editing Tag %(tag_id)d to %(item_name)s') % {'tag_id': tag_id, 'item_name': w.name})
282
+                    return redirect(get_next(request))
283
+            else:
284
+                t.delete()
285
+                messages.info(request, _('Tag %(tag_id)d of %(item_name)s has been deleted.') % {'tag_id': tag_id, 'item_name': w.name})
286
+                return redirect(get_next(request))
287
+        else:
288
+            form = TagForm(instance=t, factor_to_original=context['factor_to_original'])
289
+        context['form'] = form
290
+        return render(request, 'pygal/tagedit.html', context=context)
291
+
292
+
293
+def pygal_favourite(request, set_favourite, rel_path):
294
+    nxt = request.GET.get('next', '/')
295
+    item = get_item_by_rel_path(rel_path)
296
+    if not set_favourite and request.user in item.favourite_of.all():
297
+        item.favourite_of.remove(request.user)
298
+        item.save()
299
+        if nxt.startswith(pygal.url_favouriteview(request)):
300
+            nxt = pygal.url_favouriteview(request)
301
+    elif set_favourite and request.user not in item.favourite_of.all():
302
+        item.favourite_of.add(request.user)
303
+        item.save()
304
+    else:
305
+        messages.error(request, 'Favourite setting is already in targetstate!')
306
+    return redirect(nxt + '#%s' % item.name)

二进制
views/forbidden.png 查看文件


+ 284
- 0
views/image.py 查看文件

@@ -0,0 +1,284 @@
1
+from django.conf import settings
2
+import fstools
3
+import io
4
+import logging
5
+import mimetypes
6
+from ..models import get_item_type, TYPE_IMAGE, TYPE_VIDEO
7
+import os
8
+from PIL import Image, ImageEnhance, ExifTags
9
+import platform
10
+import pygal
11
+import subprocess
12
+
13
+# Get a logger instance
14
+logger = logging.getLogger("CACHING")
15
+
16
+
17
+def get_image_class(full_path):
18
+    return {
19
+        TYPE_IMAGE: image,
20
+        TYPE_VIDEO: video,
21
+    }.get(get_item_type(full_path), other)
22
+
23
+
24
+def get_image_instance(full_path, request):
25
+    return get_image_class(full_path)(full_path, request)
26
+
27
+
28
+class mm_image(object):
29
+    JOIN_TOP_LEFT = 1
30
+    JOIN_TOP_RIGHT = 2
31
+    JOIN_BOT_LEFT = 3
32
+    JOIN_BOT_RIGHT = 4
33
+    JOIN_CENTER = 5
34
+
35
+    def __init__(self, imagepath_handle_image):
36
+        self.imagepath_handle_image = imagepath_handle_image
37
+        self.__image__ = None
38
+        #
39
+
40
+    def __init_image__(self):
41
+        if self.__image__ is None:
42
+            if type(self.imagepath_handle_image) is mm_image:
43
+                self.__image__ = self.imagepath_handle_image.__image__
44
+            elif type(self.imagepath_handle_image) is Image.Image:
45
+                self.__image__ = self.imagepath_handle_image
46
+            else:
47
+                self.__image__ = Image.open(self.imagepath_handle_image)
48
+
49
+    def orientation(self):
50
+        self.__init_image__()
51
+        exif_tags = dict((v, k) for k, v in ExifTags.TAGS.items())
52
+        try:
53
+            return dict(self.__image__._getexif().items())[exif_tags['Orientation']]
54
+        except AttributeError:
55
+            return 1
56
+        except KeyError:
57
+            return 1
58
+
59
+    def copy(self):
60
+        self.__init_image__()
61
+        return mm_image(self.__image__.copy())
62
+
63
+    def save(self, *args, **kwargs):
64
+        self.__init_image__()
65
+        im = self.__image__.copy().convert('RGB')
66
+        im.save(*args, **kwargs)
67
+
68
+    def resize(self, max_size):
69
+        self.__init_image__()
70
+        #
71
+        # resize
72
+        #
73
+        x, y = self.__image__.size
74
+        xy_max = max(x, y)
75
+        self.__image__ = self.__image__.resize((int(x * float(max_size) / xy_max), int(y * float(max_size) / xy_max)), Image.NEAREST).rotate(0)
76
+
77
+    def rotate_by_orientation(self, orientation):
78
+        self.__init_image__()
79
+        #
80
+        # rotate
81
+        #
82
+        angle = {3: 180, 6: 270, 8: 90}.get(orientation)
83
+        if angle is not None:
84
+            self.__image__ = self.__image__.rotate(angle, expand=True)
85
+
86
+    def __rgba_copy__(self):
87
+        self.__init_image__()
88
+        if self.__image__.mode != 'RGBA':
89
+            return self.__image__.convert('RGBA')
90
+        else:
91
+            return self.__image__.copy()
92
+
93
+    def join(self, image, joint_pos=JOIN_TOP_RIGHT, opacity=0.7):
94
+        """
95
+        This joins another picture to this one.
96
+
97
+        :param picture_edit picture: The picture to be joint.
98
+        :param joint_pos: The position of picture in this picture. See also self.JOIN_*
99
+        :param float opacity: The opacity of picture when joint (value between 0 and 1).
100
+
101
+        .. note::
102
+          joint_pos makes only sense if picture is smaller than this picture.
103
+        """
104
+        self.__init_image__()
105
+        #
106
+        im2 = image.__rgba_copy__()
107
+        # change opacity of im2
108
+        alpha = im2.split()[3]
109
+        alpha = ImageEnhance.Brightness(alpha).enhance(opacity)
110
+        im2.putalpha(alpha)
111
+
112
+        self.__image__ = self.__rgba_copy__()
113
+
114
+        # create a transparent layer
115
+        layer = Image.new('RGBA', self.__image__.size, (0, 0, 0, 0))
116
+        # draw im2 in layer
117
+        if joint_pos == self.JOIN_TOP_LEFT:
118
+            layer.paste(im2, (0, 0))
119
+        elif joint_pos == self.JOIN_TOP_RIGHT:
120
+            layer.paste(im2, ((self.__image__.size[0] - im2.size[0]), 0))
121
+        elif joint_pos == self.JOIN_BOT_LEFT:
122
+            layer.paste(im2, (0, (self.__image__.size[1] - im2.size[1])))
123
+        elif joint_pos == self.JOIN_BOT_RIGHT:
124
+            layer.paste(im2, ((self.__image__.size[0] - im2.size[0]), (self.__image__.size[1] - im2.size[1])))
125
+        elif joint_pos == self.JOIN_CENTER:
126
+            layer.paste(im2, (int((self.__image__.size[0] - im2.size[0]) / 2), int((self.__image__.size[1] - im2.size[1]) / 2)))
127
+
128
+        self.__image__ = Image.composite(layer, self.__image__, layer)
129
+
130
+    def image_data(self):
131
+        self.__init_image__()
132
+        #
133
+        # create return value
134
+        #
135
+        im = self.__image__.copy().convert('RGB')
136
+        output = io.BytesIO()
137
+        im.save(output, format='JPEG')
138
+        return output.getvalue()
139
+
140
+
141
+class mm_video(object):
142
+    def __init__(self, full_path):
143
+        self.full_path = full_path
144
+
145
+    def image(self):
146
+        if platform.system() == 'Linux':
147
+            cmd = 'ffmpeg -ss 0.5 -i "' + self.full_path + '" -vframes 1 -f image2pipe pipe:1 2> /dev/null'
148
+        else:
149
+            cmd = 'ffmpeg -ss 0.5 -i "' + self.full_path + '" -vframes 1 -f image2pipe pipe:1 2> NULL'
150
+        data = subprocess.check_output(cmd, shell=True)
151
+        ffmpeg_handle = io.BytesIO(data)
152
+        im = Image.open(ffmpeg_handle)
153
+        return mm_image(im.copy())
154
+
155
+
156
+class base_item(object):
157
+    MIME_TYPES = {
158
+        '.ada': 'text/x/adasrc',
159
+        '.hex': 'text/x/hex',
160
+        '.jpg': 'image/x/generic',
161
+        '.jpeg': 'image/x/generic',
162
+        '.jpe': 'image/x/generic',
163
+        '.png': 'image/x/generic',
164
+        '.tif': 'image/x/generic',
165
+        '.tiff': 'image/x/generic',
166
+        '.gif': 'image/x/generic',
167
+        '.avi': 'video/x/generic',
168
+        '.mpg': 'video/x/generic',
169
+        '.mpeg': 'video/x/generic',
170
+        '.mpe': 'video/x/generic',
171
+        '.mov': 'video/x/generic',
172
+        '.qt': 'video/x/generic',
173
+        '.mp4': 'video/x/generic',
174
+        '.webm': 'video/x/generic',
175
+        '.ogv': 'video/x/generic',
176
+        '.flv': 'video/x/generic',
177
+        '.3gp': 'video/x/generic',
178
+    }
179
+
180
+    def __init__(self, full_path, request):
181
+        self.full_path = full_path
182
+        self.request = request
183
+        self.rel_path = pygal.get_rel_path(full_path)
184
+        #
185
+        ext = os.path.splitext(self.full_path)[1].lower()
186
+        self.mime_type = self.MIME_TYPES.get(ext, mimetypes.types_map.get(ext, 'unknown'))
187
+
188
+    def __cache_image_folder__(self, rel_path):
189
+        return os.path.join(settings.XNAIL_ROOT, rel_path.replace('_', '__').replace('/', '_'))
190
+
191
+    def __cache_image_name__(self, max_size):
192
+        filename = '%04d_%02x_%s.jpg' % (max_size, self.XNAIL_VERSION_NUMBER, fstools.uid(self.full_path, None))
193
+        return os.path.join(self.__cache_image_folder__(self.rel_path), filename)
194
+
195
+    def __delete_cache_image__(self, max_size):
196
+        for fn in fstools.filelist(self.__cache_image_folder__(self.rel_path), '%04d*' % max_size):
197
+            try:
198
+                os.remove(fn)
199
+            except OSError:
200
+                pass    # possibly file is already removed by another process
201
+
202
+
203
+class image(base_item, mm_image):
204
+    XNAIL_VERSION_NUMBER = 1
205
+
206
+    def __init__(self, *args, **kwargs):
207
+        base_item.__init__(self, *args, **kwargs)
208
+        mm_image.__init__(self, self.full_path)
209
+        self.mime_type_xnails = mimetypes.types_map.get('.jpg', 'unknown')
210
+
211
+    def get_resized_image_data(self, max_size):
212
+        cache_filename = self.__cache_image_name__(max_size)
213
+        if not os.path.exists(cache_filename):
214
+            logger.info('Creating xnail-%d for %s', max_size, self.rel_path)
215
+            self.__delete_cache_image__(max_size)
216
+            im = self.copy()
217
+            im.resize(max_size)
218
+            im.rotate_by_orientation(self.orientation())
219
+            #
220
+            # create cache file
221
+            #
222
+            fstools.mkdir(os.path.dirname(cache_filename))
223
+            with open(cache_filename, 'wb') as fh:
224
+                im.save(fh, format='JPEG')
225
+            #
226
+            return im.image_data()
227
+        else:
228
+            return open(cache_filename, 'rb').read()
229
+
230
+    def thumbnail_picture(self):
231
+        return self.get_resized_image_data(pygal.get_thumbnail_max_size(self.request))
232
+
233
+    def webnail_picture(self):
234
+        return self.get_resized_image_data(pygal.get_webnail_size(self.request))
235
+
236
+
237
+class video(base_item, mm_video):
238
+    XNAIL_VERSION_NUMBER = 1
239
+
240
+    def __init__(self, *args, **kwargs):
241
+        base_item.__init__(self, *args, **kwargs)
242
+        mm_video.__init__(self, self.full_path)
243
+        self.mime_type_xnails = mimetypes.types_map.get('.jpg', 'unknown')
244
+
245
+    def get_resized_image_data(self, max_size):
246
+        cache_filename = self.__cache_image_name__(max_size)
247
+        if not os.path.exists(cache_filename):
248
+            logger.info('Creating xnail-%d for %s', max_size, self.rel_path)
249
+            self.__delete_cache_image__(max_size)
250
+            im = self.image()
251
+            im.resize(max_size)
252
+            overlay = mm_image(os.path.join(os.path.dirname(__file__), 'video.png'))
253
+            im.join(overlay)
254
+            #
255
+            # create cache file
256
+            #
257
+            fstools.mkdir(os.path.dirname(cache_filename))
258
+            with open(cache_filename, 'wb') as fh:
259
+                im.save(fh, format='JPEG')
260
+            #
261
+            return im.image_data()
262
+        else:
263
+            return open(cache_filename, 'rb').read()
264
+
265
+    def thumbnail_picture(self):
266
+        return image.thumbnail_picture(self)
267
+
268
+    def webnail_picture(self):
269
+        return image.webnail_picture(self)
270
+
271
+
272
+class other(base_item):
273
+    def __init__(self, *args, **kwargs):
274
+        base_item.__init__(self, *args, **kwargs)
275
+        self.mime_type_xnails = mimetypes.types_map.get('.png', 'unknown')
276
+
277
+    def thumbnail_picture(self):
278
+        fn = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'mimetype_icons', '%s.png' % (self.mime_type).replace('/', '-'))
279
+        if not os.path.exists(fn):
280
+            fn = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'mimetype_icons', 'unknown.png')
281
+        return open(fn, 'rb').read()
282
+
283
+    def webnail_picture(self):
284
+        return self.thumbnail_picture()

+ 123
- 0
views/infoviews.py 查看文件

@@ -0,0 +1,123 @@
1
+from django.utils.translation import gettext as _
2
+import fractions
3
+import geo
4
+from ..models import get_item_type, TYPE_AUDIO, TYPE_FOLDER, TYPE_IMAGE, TYPE_VIDEO
5
+from .userviews import base_view as base_userview
6
+
7
+
8
+def get_wrapper_class(full_path):
9
+    return {
10
+        TYPE_FOLDER: folder_view,
11
+        TYPE_IMAGE: image_view,
12
+        TYPE_VIDEO: video_view,
13
+        TYPE_AUDIO: audio_view,
14
+    }.get(get_item_type(full_path), base_view)
15
+
16
+
17
+def get_wrapper_instance(full_path, request):
18
+    return get_wrapper_class(full_path)(request, full_path)
19
+
20
+
21
+class base_view(base_userview):
22
+    def __init__(self, *args, **kwargs):
23
+        base_userview.__init__(self, *args, **kwargs)
24
+        self.template = 'pygal/infoview.html'
25
+
26
+    def context_adaption(self, context):
27
+        base_userview.context_adaption(self, context)
28
+        context['heading'] = {
29
+            'general': _('General'),
30
+            'information': _('Information'),
31
+            'database': _('Database')
32
+        }
33
+
34
+    @property
35
+    def database_information(self):
36
+        rv = []
37
+        if self.request.user.is_superuser:
38
+            if self.item is not None:
39
+                rv.append({'description': 'DB-Item', 'data': 'Item (%d)' % self.item.id, 'url': self.item.get_admin_url()})
40
+            i = 0
41
+            for t in self.item.tag_set.all():
42
+                i += 1
43
+                rv.append({'description': 'TAG%d - %s' % (i % 10, t.text), 'data': 'Tag (%d)' % t.id, 'url': t.get_admin_url()})
44
+        return rv
45
+
46
+    @property
47
+    def item_information(self):
48
+        return []
49
+
50
+    @property
51
+    def upload_information(self):
52
+        rv = []
53
+        return rv
54
+
55
+
56
+class folder_view(base_view):
57
+    def __init__(self, *args, **kwargs):
58
+        base_view.__init__(self, *args, **kwargs)
59
+
60
+    @property
61
+    def item_information(self):
62
+        rv = []
63
+        rv.append({'description': _('Number of Folder(s)'), 'data': self.item.item_data.num_folders, 'url': None})
64
+        rv.append({'description': _('Number of Audio File(s)'), 'data': self.item.item_data.num_audio, 'url': None})
65
+        rv.append({'description': _('Number of Image(s)'), 'data': self.item.item_data.num_images, 'url': None})
66
+        rv.append({'description': _('Number of Other File(s)'), 'data': self.item.item_data.num_other, 'url': None})
67
+        rv.append({'description': _('Number of Video(s)'), 'data': self.item.item_data.num_videos, 'url': None})
68
+        return rv
69
+
70
+
71
+class image_view(base_view):
72
+    def __init__(self, *args, **kwargs):
73
+        base_view.__init__(self, *args, **kwargs)
74
+
75
+    @property
76
+    def item_information(self):
77
+        rv = []
78
+        rv.append({'description': _('Camera'), 'data': '%s - %s' % (self.item.item_data.camera_vendor, self.item.item_data.camera_model), 'url': None})
79
+        rv.append({'description': _('Resolution'), 'data': '%d x %d' % (self.item.item_data.width, self.item.item_data.height), 'url': None})
80
+        gps_data = self.item.item_data.gps
81
+        if gps_data is not None:
82
+            c = geo.gps.coordinate(**gps_data)
83
+            rv.append({'description': _('GPS'), 'data': str(c), 'url': geo.osm.landmark_link(c)})
84
+        rv.append({'description': _('Exposure Program'), 'data': self.item.item_data.exposure_program, 'url': None})
85
+        f = fractions.Fraction(self.item.item_data.exposure_time).limit_denominator()
86
+        rv.append({'description': _('Exposure Time'), 'data': '%d/%d s' % (f.numerator, f.denominator), 'url': None})
87
+        rv.append({'description': _('ISO'), 'data': self.item.item_data.iso, 'url': None})
88
+        rv.append({'description': _('Focal Length'), 'data': self.item.item_data.focal_length, 'url': None})
89
+        rv.append({'description': _('Apeture'), 'data': self.item.item_data.f_number, 'url': None})
90
+        rv.append({'description': _('Flash'), 'data': self.item.item_data.flash, 'url': None})
91
+        rv.append({'description': _('Orientation'), 'data': self.item.item_data.orientation, 'url': None})
92
+        return rv
93
+
94
+
95
+class video_view(base_view):
96
+    def __init__(self, *args, **kwargs):
97
+        base_view.__init__(self, *args, **kwargs)
98
+
99
+    @property
100
+    def item_information(self):
101
+        rv = []
102
+        rv.append({'description': _('Duration'), 'data': self.__duration_txt__(self.item.item_data.duration), 'url': None})
103
+        rv.append({'description': _('Resolution'), 'data': '%d x %d' % (self.item.item_data.width, self.item.item_data.height), 'url': None})
104
+        rv.append({'description': _('Ratio'), 'data': self.item.item_data.ratio, 'url': None})
105
+        return rv
106
+
107
+
108
+class audio_view(base_view):
109
+    def __init__(self, *args, **kwargs):
110
+        base_view.__init__(self, *args, **kwargs)
111
+
112
+    @property
113
+    def item_information(self):
114
+        rv = []
115
+        rv.append({'description': _('Artist'), 'data': self.item.item_data.artist, 'url': None})
116
+        rv.append({'description': _('Album'), 'data': self.item.item_data.album, 'url': None})
117
+        rv.append({'description': _('Year'), 'data': self.item.item_data.year, 'url': None})
118
+        rv.append({'description': _('Title'), 'data': self.item.item_data.title, 'url': None})
119
+        rv.append({'description': _('Track'), 'data': self.item.item_data.track, 'url': None})
120
+        rv.append({'description': _('Duration'), 'data': self.__duration_txt__(self.item.item_data.duration), 'url': None})
121
+        rv.append({'description': _('Genre'), 'data': self.item.item_data.genre, 'url': None})
122
+        rv.append({'description': _('Bitrate'), 'data': self.__size_txt__(self.item.item_data.bitrate), 'url': None})
123
+        return rv

二进制
views/mimetype_icons/application-epub+zip.png 查看文件


二进制
views/mimetype_icons/application-gzip.png 查看文件


二进制
views/mimetype_icons/application-illustrator.png 查看文件


二进制
views/mimetype_icons/application-java-archive.png 查看文件


二进制
views/mimetype_icons/application-javascript.png 查看文件


+ 0
- 0
views/mimetype_icons/application-msword-template.png 查看文件


部分文件因为文件数量过多而无法显示

正在加载...
取消
保存