2015-11-17 19:20:13 +01:00
|
|
|
// Copyright (c) 2012 The Chromium Embedded Framework Authors.
|
|
|
|
// Portions copyright (c) 2012 The Chromium Authors. All rights reserved.
|
|
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
|
|
// found in the LICENSE file.
|
|
|
|
|
|
|
|
#include "libcef/browser/native/file_dialog_runner_mac.h"
|
|
|
|
|
|
|
|
#import <Cocoa/Cocoa.h>
|
|
|
|
#import <CoreServices/CoreServices.h>
|
|
|
|
|
2020-09-22 21:54:02 +02:00
|
|
|
#include "libcef/browser/alloy/alloy_browser_host_impl.h"
|
2015-11-17 19:20:13 +01:00
|
|
|
|
|
|
|
#include "base/mac/mac_util.h"
|
2019-02-01 17:42:40 +01:00
|
|
|
#include "base/stl_util.h"
|
2015-11-17 19:20:13 +01:00
|
|
|
#include "base/strings/string_split.h"
|
|
|
|
#include "base/strings/string_util.h"
|
|
|
|
#include "base/strings/sys_string_conversions.h"
|
|
|
|
#include "base/strings/utf_string_conversions.h"
|
|
|
|
#include "base/threading/thread_restrictions.h"
|
2016-07-14 03:35:07 +02:00
|
|
|
#include "cef/grit/cef_strings.h"
|
2017-02-01 21:24:20 +01:00
|
|
|
#include "chrome/grit/generated_resources.h"
|
2015-11-17 19:20:13 +01:00
|
|
|
#include "net/base/mime_util.h"
|
|
|
|
#include "ui/base/l10n/l10n_util.h"
|
2016-06-23 19:42:00 +02:00
|
|
|
#include "ui/strings/grit/ui_strings.h"
|
2015-11-17 19:20:13 +01:00
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
base::string16 GetDescriptionFromMimeType(const std::string& mime_type) {
|
|
|
|
// Check for wild card mime types and return an appropriate description.
|
|
|
|
static const struct {
|
|
|
|
const char* mime_type;
|
|
|
|
int string_id;
|
|
|
|
} kWildCardMimeTypes[] = {
|
2017-05-17 11:29:28 +02:00
|
|
|
{"audio", IDS_AUDIO_FILES},
|
|
|
|
{"image", IDS_IMAGE_FILES},
|
|
|
|
{"text", IDS_TEXT_FILES},
|
|
|
|
{"video", IDS_VIDEO_FILES},
|
2015-11-17 19:20:13 +01:00
|
|
|
};
|
|
|
|
|
2019-02-01 17:42:40 +01:00
|
|
|
for (size_t i = 0; i < base::size(kWildCardMimeTypes); ++i) {
|
2015-11-17 19:20:13 +01:00
|
|
|
if (mime_type == std::string(kWildCardMimeTypes[i].mime_type) + "/*")
|
|
|
|
return l10n_util::GetStringUTF16(kWildCardMimeTypes[i].string_id);
|
|
|
|
}
|
|
|
|
|
|
|
|
return base::string16();
|
|
|
|
}
|
|
|
|
|
2017-05-17 11:29:28 +02:00
|
|
|
void AddFilters(NSPopUpButton* button,
|
2015-11-17 19:20:13 +01:00
|
|
|
const std::vector<base::string16>& accept_filters,
|
|
|
|
bool include_all_files,
|
2017-05-17 11:29:28 +02:00
|
|
|
std::vector<std::vector<base::string16>>* all_extensions) {
|
2015-11-17 19:20:13 +01:00
|
|
|
for (size_t i = 0; i < accept_filters.size(); ++i) {
|
|
|
|
const base::string16& filter = accept_filters[i];
|
|
|
|
if (filter.empty())
|
|
|
|
continue;
|
|
|
|
|
|
|
|
std::vector<base::string16> extensions;
|
|
|
|
base::string16 description;
|
|
|
|
|
|
|
|
size_t sep_index = filter.find('|');
|
|
|
|
if (sep_index != std::string::npos) {
|
|
|
|
// Treat as a filter of the form "Filter Name|.ext1;.ext2;.ext3".
|
|
|
|
description = filter.substr(0, sep_index);
|
|
|
|
|
2017-05-17 11:29:28 +02:00
|
|
|
const std::vector<base::string16>& ext = base::SplitString(
|
|
|
|
filter.substr(sep_index + 1), base::ASCIIToUTF16(";"),
|
|
|
|
base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
|
2015-11-17 19:20:13 +01:00
|
|
|
for (size_t x = 0; x < ext.size(); ++x) {
|
|
|
|
const base::string16& file_ext = ext[x];
|
|
|
|
if (!file_ext.empty() && file_ext[0] == '.')
|
|
|
|
extensions.push_back(file_ext);
|
|
|
|
}
|
|
|
|
} else if (filter[0] == '.') {
|
|
|
|
// Treat as an extension beginning with the '.' character.
|
|
|
|
extensions.push_back(filter);
|
|
|
|
} else {
|
|
|
|
// Otherwise convert mime type to one or more extensions.
|
|
|
|
const std::string& ascii = base::UTF16ToASCII(filter);
|
|
|
|
std::vector<base::FilePath::StringType> ext;
|
|
|
|
net::GetExtensionsForMimeType(ascii, &ext);
|
|
|
|
if (!ext.empty()) {
|
|
|
|
for (size_t x = 0; x < ext.size(); ++x)
|
|
|
|
extensions.push_back(base::ASCIIToUTF16("." + ext[x]));
|
|
|
|
description = GetDescriptionFromMimeType(ascii);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (extensions.empty())
|
|
|
|
continue;
|
|
|
|
|
|
|
|
// Don't display a crazy number of extensions since the NSPopUpButton width
|
|
|
|
// will keep growing.
|
|
|
|
const size_t kMaxExtensions = 10;
|
|
|
|
|
|
|
|
base::string16 ext_str;
|
|
|
|
for (size_t x = 0; x < std::min(kMaxExtensions, extensions.size()); ++x) {
|
|
|
|
const base::string16& pattern = base::ASCIIToUTF16("*") + extensions[x];
|
|
|
|
if (x != 0)
|
|
|
|
ext_str += base::ASCIIToUTF16(";");
|
|
|
|
ext_str += pattern;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (extensions.size() > kMaxExtensions)
|
|
|
|
ext_str += base::ASCIIToUTF16(";...");
|
|
|
|
|
|
|
|
if (description.empty()) {
|
|
|
|
description = ext_str;
|
|
|
|
} else {
|
|
|
|
description +=
|
|
|
|
base::ASCIIToUTF16(" (") + ext_str + base::ASCIIToUTF16(")");
|
|
|
|
}
|
|
|
|
|
|
|
|
[button addItemWithTitle:base::SysUTF16ToNSString(description)];
|
|
|
|
|
|
|
|
all_extensions->push_back(extensions);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add the *.* filter, but only if we have added other filters (otherwise it
|
|
|
|
// is implied).
|
|
|
|
if (include_all_files && !all_extensions->empty()) {
|
|
|
|
[button addItemWithTitle:base::SysUTF8ToNSString("All Files (*)")];
|
|
|
|
all_extensions->push_back(std::vector<base::string16>());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
// Used to manage the file type filter in the NSSavePanel/NSOpenPanel.
|
|
|
|
@interface CefFilterDelegate : NSObject {
|
|
|
|
@private
|
|
|
|
NSSavePanel* panel_;
|
2017-05-17 11:29:28 +02:00
|
|
|
std::vector<std::vector<base::string16>> extensions_;
|
2015-11-17 19:20:13 +01:00
|
|
|
int selected_index_;
|
|
|
|
}
|
|
|
|
- (id)initWithPanel:(NSSavePanel*)panel
|
2017-05-17 11:29:28 +02:00
|
|
|
andAcceptFilters:(const std::vector<base::string16>&)accept_filters
|
|
|
|
andFilterIndex:(int)index;
|
2015-11-17 19:20:13 +01:00
|
|
|
- (void)setFilter:(int)index;
|
|
|
|
- (int)filter;
|
|
|
|
- (void)filterSelectionChanged:(id)sender;
|
|
|
|
- (void)setFileExtension;
|
|
|
|
@end
|
|
|
|
|
|
|
|
@implementation CefFilterDelegate
|
|
|
|
|
|
|
|
- (id)initWithPanel:(NSSavePanel*)panel
|
2017-05-17 11:29:28 +02:00
|
|
|
andAcceptFilters:(const std::vector<base::string16>&)accept_filters
|
|
|
|
andFilterIndex:(int)index {
|
2015-11-17 19:20:13 +01:00
|
|
|
if (self = [super init]) {
|
|
|
|
DCHECK(panel);
|
|
|
|
panel_ = panel;
|
|
|
|
selected_index_ = 0;
|
|
|
|
|
2017-05-17 11:29:28 +02:00
|
|
|
NSPopUpButton* button = [[NSPopUpButton alloc] init];
|
2015-11-17 19:20:13 +01:00
|
|
|
AddFilters(button, accept_filters, true, &extensions_);
|
|
|
|
[button sizeToFit];
|
|
|
|
[button setTarget:self];
|
|
|
|
[button setAction:@selector(filterSelectionChanged:)];
|
|
|
|
|
|
|
|
if (index < static_cast<int>(extensions_.size())) {
|
|
|
|
[button selectItemAtIndex:index];
|
|
|
|
[self setFilter:index];
|
|
|
|
}
|
|
|
|
|
|
|
|
[panel_ setAccessoryView:button];
|
|
|
|
}
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set the current filter index.
|
|
|
|
- (void)setFilter:(int)index {
|
|
|
|
DCHECK(index >= 0 && index < static_cast<int>(extensions_.size()));
|
|
|
|
selected_index_ = index;
|
|
|
|
|
|
|
|
// Set the selectable file types. For open panels this limits the files that
|
|
|
|
// can be selected. For save panels this applies a default file extenion when
|
|
|
|
// the dialog is dismissed if none is already provided.
|
|
|
|
NSMutableArray* acceptArray = nil;
|
|
|
|
if (!extensions_[index].empty()) {
|
|
|
|
acceptArray = [[NSMutableArray alloc] init];
|
|
|
|
for (size_t i = 0; i < extensions_[index].size(); ++i) {
|
2017-05-17 11:29:28 +02:00
|
|
|
[acceptArray
|
|
|
|
addObject:base::SysUTF16ToNSString(extensions_[index][i].substr(1))];
|
2015-11-17 19:20:13 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
[panel_ setAllowedFileTypes:acceptArray];
|
|
|
|
|
|
|
|
if (![panel_ isKindOfClass:[NSOpenPanel class]]) {
|
|
|
|
// For save panels set the file extension.
|
|
|
|
[self setFileExtension];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns the current filter index.
|
|
|
|
- (int)filter {
|
|
|
|
return selected_index_;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Called when the selected filter is changed via the NSPopUpButton.
|
|
|
|
- (void)filterSelectionChanged:(id)sender {
|
2017-05-17 11:29:28 +02:00
|
|
|
NSPopUpButton* button = (NSPopUpButton*)sender;
|
2015-11-17 19:20:13 +01:00
|
|
|
[self setFilter:[button indexOfSelectedItem]];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set the extension on the currently selected file name.
|
|
|
|
- (void)setFileExtension {
|
|
|
|
const std::vector<base::string16>& filter = extensions_[selected_index_];
|
|
|
|
if (filter.empty()) {
|
|
|
|
// All extensions are allowed so don't change anything.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
base::FilePath path(base::SysNSStringToUTF8([panel_ nameFieldStringValue]));
|
|
|
|
|
|
|
|
// If the file name currently includes an extension from |filter| then don't
|
|
|
|
// change anything.
|
|
|
|
base::string16 extension = base::UTF8ToUTF16(path.Extension());
|
|
|
|
if (!extension.empty()) {
|
|
|
|
for (size_t i = 0; i < filter.size(); ++i) {
|
|
|
|
if (filter[i] == extension)
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Change the extension to the first value in |filter|.
|
|
|
|
path = path.ReplaceExtension(base::UTF16ToUTF8(filter[0]));
|
|
|
|
[panel_ setNameFieldStringValue:base::SysUTF8ToNSString(path.value())];
|
|
|
|
}
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
2020-03-04 01:29:39 +01:00
|
|
|
CefFileDialogRunnerMac::CefFileDialogRunnerMac() : weak_ptr_factory_(this) {}
|
|
|
|
|
2020-09-22 21:54:02 +02:00
|
|
|
void CefFileDialogRunnerMac::Run(AlloyBrowserHostImpl* browser,
|
2020-03-04 01:29:39 +01:00
|
|
|
const FileChooserParams& params,
|
|
|
|
RunFileChooserCallback callback) {
|
|
|
|
callback_ = std::move(callback);
|
|
|
|
|
|
|
|
int filter_index = params.selected_accept_filter;
|
|
|
|
NSView* owner = CAST_CEF_WINDOW_HANDLE_TO_NSVIEW(browser->GetWindowHandle());
|
|
|
|
auto weak_this = weak_ptr_factory_.GetWeakPtr();
|
|
|
|
|
|
|
|
if (params.mode == blink::mojom::FileChooserParams::Mode::kOpen ||
|
|
|
|
params.mode == blink::mojom::FileChooserParams::Mode::kOpenMultiple ||
|
|
|
|
params.mode == blink::mojom::FileChooserParams::Mode::kUploadFolder) {
|
|
|
|
RunOpenFileDialog(weak_this, params, owner, filter_index);
|
|
|
|
} else if (params.mode == blink::mojom::FileChooserParams::Mode::kSave) {
|
|
|
|
RunSaveFileDialog(weak_this, params, owner, filter_index);
|
|
|
|
} else {
|
|
|
|
NOTIMPLEMENTED();
|
|
|
|
}
|
|
|
|
}
|
2015-11-17 19:20:13 +01:00
|
|
|
|
2020-03-04 01:29:39 +01:00
|
|
|
// static
|
|
|
|
void CefFileDialogRunnerMac::RunOpenFileDialog(
|
|
|
|
base::WeakPtr<CefFileDialogRunnerMac> weak_this,
|
|
|
|
const CefFileDialogRunner::FileChooserParams& params,
|
|
|
|
NSView* view,
|
|
|
|
int filter_index) {
|
2015-11-17 19:20:13 +01:00
|
|
|
NSOpenPanel* openPanel = [NSOpenPanel openPanel];
|
|
|
|
|
|
|
|
base::string16 title;
|
|
|
|
if (!params.title.empty()) {
|
|
|
|
title = params.title;
|
|
|
|
} else {
|
|
|
|
title = l10n_util::GetStringUTF16(
|
2018-10-02 14:14:11 +02:00
|
|
|
params.mode == blink::mojom::FileChooserParams::Mode::kOpen
|
2017-05-17 11:29:28 +02:00
|
|
|
? IDS_OPEN_FILE_DIALOG_TITLE
|
2018-10-02 14:14:11 +02:00
|
|
|
: (params.mode ==
|
|
|
|
blink::mojom::FileChooserParams::Mode::kOpenMultiple
|
2017-05-17 11:29:28 +02:00
|
|
|
? IDS_OPEN_FILES_DIALOG_TITLE
|
|
|
|
: IDS_SELECT_FOLDER_DIALOG_TITLE));
|
2015-11-17 19:20:13 +01:00
|
|
|
}
|
|
|
|
[openPanel setTitle:base::SysUTF16ToNSString(title)];
|
|
|
|
|
|
|
|
std::string filename, directory;
|
|
|
|
if (!params.default_file_name.empty()) {
|
2018-10-02 14:14:11 +02:00
|
|
|
if (params.mode == blink::mojom::FileChooserParams::Mode::kUploadFolder ||
|
2015-11-17 19:20:13 +01:00
|
|
|
params.default_file_name.EndsWithSeparator()) {
|
|
|
|
// The value is only a directory.
|
|
|
|
directory = params.default_file_name.value();
|
|
|
|
} else {
|
|
|
|
// The value is a file name and possibly a directory.
|
|
|
|
filename = params.default_file_name.BaseName().value();
|
|
|
|
directory = params.default_file_name.DirName().value();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!filename.empty()) {
|
|
|
|
[openPanel setNameFieldStringValue:base::SysUTF8ToNSString(filename)];
|
|
|
|
}
|
|
|
|
if (!directory.empty()) {
|
2017-05-17 11:29:28 +02:00
|
|
|
[openPanel setDirectoryURL:[NSURL fileURLWithPath:base::SysUTF8ToNSString(
|
|
|
|
directory)]];
|
2015-11-17 19:20:13 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
CefFilterDelegate* filter_delegate = nil;
|
2018-10-02 14:14:11 +02:00
|
|
|
if (params.mode != blink::mojom::FileChooserParams::Mode::kUploadFolder &&
|
2015-11-17 19:20:13 +01:00
|
|
|
!params.accept_types.empty()) {
|
|
|
|
// Add the file filter control.
|
|
|
|
filter_delegate =
|
|
|
|
[[CefFilterDelegate alloc] initWithPanel:openPanel
|
2016-06-15 19:43:58 +02:00
|
|
|
andAcceptFilters:params.accept_types
|
|
|
|
andFilterIndex:filter_index];
|
2015-11-17 19:20:13 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Further panel configuration.
|
|
|
|
[openPanel setAllowsOtherFileTypes:YES];
|
2018-10-02 14:14:11 +02:00
|
|
|
[openPanel setAllowsMultipleSelection:
|
|
|
|
(params.mode ==
|
|
|
|
blink::mojom::FileChooserParams::Mode::kOpenMultiple)];
|
2017-05-17 11:29:28 +02:00
|
|
|
[openPanel
|
2018-10-02 14:14:11 +02:00
|
|
|
setCanChooseFiles:(params.mode !=
|
|
|
|
blink::mojom::FileChooserParams::Mode::kUploadFolder)];
|
2017-05-17 11:29:28 +02:00
|
|
|
[openPanel
|
2018-10-02 14:14:11 +02:00
|
|
|
setCanChooseDirectories:(params.mode == blink::mojom::FileChooserParams::
|
|
|
|
Mode::kUploadFolder)];
|
2015-11-17 19:20:13 +01:00
|
|
|
[openPanel setShowsHiddenFiles:!params.hidereadonly];
|
|
|
|
|
|
|
|
// Show panel.
|
2017-05-17 11:29:28 +02:00
|
|
|
[openPanel
|
|
|
|
beginSheetModalForWindow:[view window]
|
|
|
|
completionHandler:^(NSInteger returnCode) {
|
|
|
|
int filter_index_to_use = (filter_delegate != nil)
|
|
|
|
? [filter_delegate filter]
|
|
|
|
: filter_index;
|
|
|
|
if (returnCode == NSFileHandlingPanelOKButton) {
|
|
|
|
std::vector<base::FilePath> files;
|
|
|
|
files.reserve(openPanel.URLs.count);
|
|
|
|
for (NSURL* url in openPanel.URLs) {
|
|
|
|
if (url.isFileURL)
|
|
|
|
files.push_back(base::FilePath(url.path.UTF8String));
|
|
|
|
}
|
2020-03-04 01:29:39 +01:00
|
|
|
std::move(weak_this->callback_)
|
|
|
|
.Run(filter_index_to_use, files);
|
2017-05-17 11:29:28 +02:00
|
|
|
} else {
|
2020-03-04 01:29:39 +01:00
|
|
|
std::move(weak_this->callback_)
|
|
|
|
.Run(filter_index_to_use, std::vector<base::FilePath>());
|
2017-05-17 11:29:28 +02:00
|
|
|
}
|
|
|
|
}];
|
2015-11-17 19:20:13 +01:00
|
|
|
}
|
|
|
|
|
2020-03-04 01:29:39 +01:00
|
|
|
// static
|
|
|
|
void CefFileDialogRunnerMac::RunSaveFileDialog(
|
|
|
|
base::WeakPtr<CefFileDialogRunnerMac> weak_this,
|
|
|
|
const CefFileDialogRunner::FileChooserParams& params,
|
|
|
|
NSView* view,
|
|
|
|
int filter_index) {
|
2015-11-17 19:20:13 +01:00
|
|
|
NSSavePanel* savePanel = [NSSavePanel savePanel];
|
|
|
|
|
|
|
|
base::string16 title;
|
|
|
|
if (!params.title.empty())
|
|
|
|
title = params.title;
|
|
|
|
else
|
|
|
|
title = l10n_util::GetStringUTF16(IDS_SAVE_AS_DIALOG_TITLE);
|
|
|
|
[savePanel setTitle:base::SysUTF16ToNSString(title)];
|
|
|
|
|
|
|
|
std::string filename, directory;
|
|
|
|
if (!params.default_file_name.empty()) {
|
|
|
|
if (params.default_file_name.EndsWithSeparator()) {
|
|
|
|
// The value is only a directory.
|
|
|
|
directory = params.default_file_name.value();
|
|
|
|
} else {
|
|
|
|
// The value is a file name and possibly a directory.
|
|
|
|
filename = params.default_file_name.BaseName().value();
|
|
|
|
directory = params.default_file_name.DirName().value();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!filename.empty()) {
|
|
|
|
[savePanel setNameFieldStringValue:base::SysUTF8ToNSString(filename)];
|
|
|
|
}
|
|
|
|
if (!directory.empty()) {
|
2017-05-17 11:29:28 +02:00
|
|
|
[savePanel setDirectoryURL:[NSURL fileURLWithPath:base::SysUTF8ToNSString(
|
|
|
|
directory)]];
|
2015-11-17 19:20:13 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
CefFilterDelegate* filter_delegate = nil;
|
|
|
|
if (!params.accept_types.empty()) {
|
|
|
|
// Add the file filter control.
|
|
|
|
filter_delegate =
|
|
|
|
[[CefFilterDelegate alloc] initWithPanel:savePanel
|
2016-06-15 19:43:58 +02:00
|
|
|
andAcceptFilters:params.accept_types
|
|
|
|
andFilterIndex:filter_index];
|
2015-11-17 19:20:13 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
[savePanel setAllowsOtherFileTypes:YES];
|
|
|
|
[savePanel setShowsHiddenFiles:!params.hidereadonly];
|
|
|
|
|
|
|
|
// Show panel.
|
2017-05-17 11:29:28 +02:00
|
|
|
[savePanel
|
|
|
|
beginSheetModalForWindow:view.window
|
|
|
|
completionHandler:^(NSInteger resultCode) {
|
|
|
|
int filter_index_to_use = (filter_delegate != nil)
|
|
|
|
? [filter_delegate filter]
|
|
|
|
: filter_index;
|
|
|
|
if (resultCode == NSFileHandlingPanelOKButton) {
|
|
|
|
NSURL* url = savePanel.URL;
|
|
|
|
const char* path = url.path.UTF8String;
|
|
|
|
std::vector<base::FilePath> files(1, base::FilePath(path));
|
2020-03-04 01:29:39 +01:00
|
|
|
std::move(weak_this->callback_)
|
|
|
|
.Run(filter_index_to_use, files);
|
2017-05-17 11:29:28 +02:00
|
|
|
} else {
|
2020-03-04 01:29:39 +01:00
|
|
|
std::move(weak_this->callback_)
|
|
|
|
.Run(filter_index_to_use, std::vector<base::FilePath>());
|
2017-05-17 11:29:28 +02:00
|
|
|
}
|
|
|
|
}];
|
2015-11-17 19:20:13 +01:00
|
|
|
}
|