зеркало из https://github.com/mozilla/gecko-dev.git
Bug 865916: create a Character Encoding widget and subview. r=gkruitbosch,dao
This commit is contained in:
Родитель
467438f2ca
Коммит
4d1dea9ead
|
@ -106,6 +106,20 @@
|
|||
<vbox id="PanelUI-developerItems"/>
|
||||
</panelview>
|
||||
|
||||
<panelview id="PanelUI-characterEncodingView" flex="1">
|
||||
<label value="&charsetMenu.label;"/>
|
||||
<toolbarbutton label="&charsetCustomize.label;"
|
||||
oncommand="PanelUI.onCharsetCustomizeCommand();"/>
|
||||
|
||||
<vbox id="PanelUI-characterEncodingView-customlist"
|
||||
class="PanelUI-characterEncodingView-list"/>
|
||||
<vbox>
|
||||
<label value="&charsetMenuAutodet.label;"/>
|
||||
<vbox id="PanelUI-characterEncodingView-autodetect"
|
||||
class="PanelUI-characterEncodingView-list"/>
|
||||
</vbox>
|
||||
</panelview>
|
||||
|
||||
</panelmultiview>
|
||||
<popupset id="customizationContextMenus">
|
||||
<menupopup id="customizationContextMenu">
|
||||
|
|
|
@ -280,6 +280,17 @@ const PanelUI = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Open a dialog window that allow the user to customize listed character sets.
|
||||
*/
|
||||
onCharsetCustomizeCommand: function() {
|
||||
this.hide();
|
||||
window.openDialog("chrome://global/content/customizeCharset.xul",
|
||||
"PrefWindow",
|
||||
"chrome,modal=yes,resizable=yes",
|
||||
"browser");
|
||||
},
|
||||
|
||||
/**
|
||||
* Signal that we're about to make a lot of changes to the contents of the
|
||||
* panels all at once. For performance, we ignore the mutations.
|
||||
|
|
|
@ -129,22 +129,31 @@ let CustomizableUIInternal = {
|
|||
this._defineBuiltInWidgets();
|
||||
this.loadSavedState();
|
||||
|
||||
let panelPlacements = [
|
||||
"edit-controls",
|
||||
"zoom-controls",
|
||||
"new-window-button",
|
||||
"privatebrowsing-button",
|
||||
"save-page-button",
|
||||
"print-button",
|
||||
"history-panelmenu",
|
||||
"fullscreen-button",
|
||||
"find-button",
|
||||
"preferences-button",
|
||||
"add-ons-button",
|
||||
];
|
||||
let showCharacterEncoding = Services.prefs.getComplexValue(
|
||||
"browser.menu.showCharacterEncoding",
|
||||
Ci.nsIPrefLocalizedString
|
||||
).data;
|
||||
if (showCharacterEncoding == "true") {
|
||||
panelPlacements.push("characterencoding-button");
|
||||
}
|
||||
|
||||
this.registerArea(CustomizableUI.AREA_PANEL, {
|
||||
anchor: "PanelUI-menu-button",
|
||||
type: CustomizableUI.TYPE_MENU_PANEL,
|
||||
defaultPlacements: [
|
||||
"edit-controls",
|
||||
"zoom-controls",
|
||||
"new-window-button",
|
||||
"privatebrowsing-button",
|
||||
"save-page-button",
|
||||
"print-button",
|
||||
"history-panelmenu",
|
||||
"fullscreen-button",
|
||||
"find-button",
|
||||
"preferences-button",
|
||||
"add-ons-button",
|
||||
]
|
||||
defaultPlacements: panelPlacements
|
||||
});
|
||||
this.registerArea(CustomizableUI.AREA_NAVBAR, {
|
||||
legacy: true,
|
||||
|
@ -1515,6 +1524,7 @@ let CustomizableUIInternal = {
|
|||
//XXXunf Log some warnings here, when the data provided isn't up to scratch.
|
||||
normalizeWidget: function(aData, aSource) {
|
||||
let widget = {
|
||||
implementation: aData,
|
||||
source: aSource || "addon",
|
||||
instances: new Map(),
|
||||
currentArea: null,
|
||||
|
@ -1532,6 +1542,9 @@ let CustomizableUIInternal = {
|
|||
return null;
|
||||
}
|
||||
|
||||
delete widget.implementation.currentArea;
|
||||
widget.implementation.__defineGetter__("currentArea", function() widget.currentArea);
|
||||
|
||||
const kReqStringProps = ["id"];
|
||||
for (let prop of kReqStringProps) {
|
||||
if (typeof aData[prop] != "string") {
|
||||
|
@ -1573,9 +1586,8 @@ let CustomizableUIInternal = {
|
|||
|
||||
widget.disabled = aData.disabled === true;
|
||||
|
||||
widget.onClick = typeof aData.onClick == "function" ? aData.onClick : null;
|
||||
|
||||
widget.onCreated = typeof aData.onCreated == "function" ? aData.onCreated : null;
|
||||
this.wrapWidgetEventHandler("onClick", widget);
|
||||
this.wrapWidgetEventHandler("onCreated", widget);
|
||||
|
||||
if (widget.type == "button") {
|
||||
widget.onCommand = typeof aData.onCommand == "function" ?
|
||||
|
@ -1589,16 +1601,10 @@ let CustomizableUIInternal = {
|
|||
}
|
||||
widget.viewId = aData.viewId;
|
||||
|
||||
widget.onViewShowing = typeof aData.onViewShowing == "function" ?
|
||||
aData.onViewShowing :
|
||||
null;
|
||||
widget.onViewHiding = typeof aData.onViewHiding == "function" ?
|
||||
aData.onViewHiding :
|
||||
null;
|
||||
this.wrapWidgetEventHandler("onViewShowing", widget);
|
||||
this.wrapWidgetEventHandler("onViewHiding", widget);
|
||||
} else if (widget.type == "custom") {
|
||||
widget.onBuild = typeof aData.onBuild == "function" ?
|
||||
aData.onBuild :
|
||||
null;
|
||||
this.wrapWidgetEventHandler("onBuild", widget);
|
||||
}
|
||||
|
||||
if (gPalette.has(widget.id)) {
|
||||
|
@ -1608,6 +1614,27 @@ let CustomizableUIInternal = {
|
|||
return widget;
|
||||
},
|
||||
|
||||
wrapWidgetEventHandler: function(aEventName, aWidget) {
|
||||
if (typeof aWidget.implementation[aEventName] != "function") {
|
||||
aWidget[aEventName] = null;
|
||||
return;
|
||||
}
|
||||
aWidget[aEventName] = function(...aArgs) {
|
||||
// Wrap inside a try...catch to properly log errors, until bug 862627 is
|
||||
// fixed, which in turn might help bug 503244.
|
||||
try {
|
||||
// Don't copy the function to the normalized widget object, instead
|
||||
// keep it on the original object provided to the API so that
|
||||
// additional methods can be implemented and used by the event
|
||||
// handlers.
|
||||
return aWidget.implementation[aEventName].apply(aWidget.implementation,
|
||||
aArgs);
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
destroyWidget: function(aWidgetId) {
|
||||
let widget = gPalette.get(aWidgetId);
|
||||
if (!widget) {
|
||||
|
@ -1948,7 +1975,7 @@ this.CustomizableUI = {
|
|||
},
|
||||
removePanelCloseListeners: function(aPanel) {
|
||||
CustomizableUIInternal.removePanelCloseListeners(aPanel);
|
||||
},
|
||||
}
|
||||
};
|
||||
Object.freeze(this.CustomizableUI);
|
||||
|
||||
|
|
|
@ -12,6 +12,9 @@ Cu.import("resource://gre/modules/Services.jsm");
|
|||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
|
||||
"resource://gre/modules/PlacesUtils.jsm");
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "CharsetManager",
|
||||
"@mozilla.org/charset-converter-manager;1",
|
||||
"nsICharsetConverterManager");
|
||||
|
||||
const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
const kPrefCustomizationDebug = "browser.uiCustomization.debug";
|
||||
|
@ -589,4 +592,214 @@ const CustomizableWidgets = [{
|
|||
node.setAttribute("disabled", "true");
|
||||
}
|
||||
}
|
||||
}, {
|
||||
id: "characterencoding-button",
|
||||
type: "view",
|
||||
viewId: "PanelUI-characterEncodingView",
|
||||
removable: true,
|
||||
defaultArea: CustomizableUI.AREA_PANEL,
|
||||
allowedAreas: [CustomizableUI.AREA_PANEL],
|
||||
maybeDisableMenu: function(aDocument) {
|
||||
let window = aDocument.defaultView;
|
||||
return !(window.gBrowser &&
|
||||
window.gBrowser.docShell &&
|
||||
window.gBrowser.docShell.mayEnableCharacterEncodingMenu);
|
||||
},
|
||||
getCharsetList: function(aSection, aDocument) {
|
||||
let currCharset = aDocument.defaultView.content.document.characterSet;
|
||||
|
||||
let list = "";
|
||||
try {
|
||||
let pref = "intl.charsetmenu.browser." + aSection;
|
||||
list = Services.prefs.getComplexValue(pref,
|
||||
Ci.nsIPrefLocalizedString).data;
|
||||
} catch (e) {}
|
||||
|
||||
list = list.trim();
|
||||
if (!list)
|
||||
return [];
|
||||
|
||||
list = list.split(",");
|
||||
|
||||
let items = [];
|
||||
for (let charset of list) {
|
||||
charset = charset.trim();
|
||||
|
||||
let notForBrowser = false;
|
||||
try {
|
||||
notForBrowser = CharsetManager.getCharsetData(charset,
|
||||
"notForBrowser");
|
||||
} catch (e) {}
|
||||
|
||||
if (notForBrowser)
|
||||
continue;
|
||||
|
||||
let title = charset;
|
||||
try {
|
||||
title = CharsetManager.getCharsetTitle(charset);
|
||||
} catch (e) {}
|
||||
|
||||
items.push({value: charset, name: title, current: charset == currCharset});
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
getAutoDetectors: function(aDocument) {
|
||||
let detectorEnum = CharsetManager.GetCharsetDetectorList();
|
||||
let currDetector;
|
||||
try {
|
||||
currDetector = Services.prefs.getComplexValue(
|
||||
"intl.charset.detector", Ci.nsIPrefLocalizedString).data;
|
||||
} catch (e) {}
|
||||
if (!currDetector)
|
||||
currDetector = "off";
|
||||
currDetector = "chardet." + currDetector;
|
||||
|
||||
let items = [];
|
||||
|
||||
while (detectorEnum.hasMore()) {
|
||||
let detector = detectorEnum.getNext();
|
||||
|
||||
let title = detector;
|
||||
try {
|
||||
title = CharsetManager.getCharsetTitle(detector);
|
||||
} catch (e) {}
|
||||
|
||||
items.push({value: detector, name: title, current: detector == currDetector});
|
||||
}
|
||||
|
||||
items.sort((aItem1, aItem2) => {
|
||||
return aItem1.name.localeCompare(aItem2.name);
|
||||
});
|
||||
|
||||
return items;
|
||||
},
|
||||
populateList: function(aDocument, aContainerId, aSection) {
|
||||
let containerElem = aDocument.getElementById(aContainerId);
|
||||
|
||||
while (containerElem.firstChild) {
|
||||
containerElem.removeChild(containerElem.firstChild);
|
||||
}
|
||||
|
||||
containerElem.addEventListener("command", this.onCommand, false);
|
||||
|
||||
let list = [];
|
||||
if (aSection == "autodetect") {
|
||||
list = this.getAutoDetectors(aDocument);
|
||||
} else if (aSection == "browser") {
|
||||
let staticList = this.getCharsetList("static", aDocument);
|
||||
let cacheList = this.getCharsetList("cache", aDocument);
|
||||
// Combine lists, and de-duplicate.
|
||||
let checkedIn = new Set();
|
||||
for (let item of staticList.concat(cacheList)) {
|
||||
let itemName = item.name.toLowerCase();
|
||||
if (!checkedIn.has(itemName)) {
|
||||
list.push(item);
|
||||
checkedIn.add(itemName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the appearance of the buttons when it's not possible to
|
||||
// customize encoding.
|
||||
let disabled = this.maybeDisableMenu(aDocument);
|
||||
for (let item of list) {
|
||||
let elem = aDocument.createElementNS(kNSXUL, "toolbarbutton");
|
||||
elem.setAttribute("label", item.name);
|
||||
elem.section = aSection;
|
||||
elem.value = item.value;
|
||||
if (item.current)
|
||||
elem.setAttribute("current", "true");
|
||||
if (disabled)
|
||||
elem.setAttribute("disabled", "true");
|
||||
containerElem.appendChild(elem);
|
||||
}
|
||||
},
|
||||
onViewShowing: function(aEvent) {
|
||||
let document = aEvent.target.ownerDocument;
|
||||
|
||||
this.populateList(document,
|
||||
"PanelUI-characterEncodingView-customlist",
|
||||
"browser");
|
||||
this.populateList(document,
|
||||
"PanelUI-characterEncodingView-autodetect",
|
||||
"autodetect");
|
||||
},
|
||||
onCommand: function(aEvent) {
|
||||
let node = aEvent.target;
|
||||
if (!node.hasAttribute || !node.section) {
|
||||
return;
|
||||
}
|
||||
|
||||
CustomizableUI.hidePanelForNode(node);
|
||||
let window = node.ownerDocument.defaultView;
|
||||
let section = node.section;
|
||||
let value = node.value;
|
||||
|
||||
// The behavior as implemented here is directly based off of the
|
||||
// `MultiplexHandler()` method in browser.js.
|
||||
if (section == "browser") {
|
||||
window.BrowserSetForcedCharacterSet(value);
|
||||
} else if (section == "autodetect") {
|
||||
value = value.replace(/^chardet\./, "");
|
||||
if (value == "off") {
|
||||
value = "";
|
||||
}
|
||||
// Set the detector pref.
|
||||
try {
|
||||
let str = Cc["@mozilla.org/supports-string;1"]
|
||||
.createInstance(Ci.nsISupportsString);
|
||||
str.data = value;
|
||||
Services.prefs.setComplexValue("intl.charset.detector", Ci.nsISupportsString, str);
|
||||
} catch (e) {
|
||||
Cu.reportError("Failed to set the intl.charset.detector preference.");
|
||||
}
|
||||
// Prepare a browser page reload with a changed charset.
|
||||
window.BrowserCharsetReload();
|
||||
}
|
||||
},
|
||||
onCreated: function(aNode) {
|
||||
const kPanelId = "PanelUI-popup";
|
||||
let document = aNode.ownerDocument;
|
||||
|
||||
let updateButton = () => {
|
||||
if (this.maybeDisableMenu(document))
|
||||
aNode.setAttribute("disabled", "true");
|
||||
else
|
||||
aNode.removeAttribute("disabled");
|
||||
};
|
||||
|
||||
if (this.currentArea == CustomizableUI.AREA_PANEL) {
|
||||
let panel = document.getElementById(kPanelId);
|
||||
panel.addEventListener("popupshowing", updateButton);
|
||||
}
|
||||
|
||||
let listener = {
|
||||
onWidgetAdded: (aWidgetId, aArea) => {
|
||||
if (aWidgetId != this.id)
|
||||
return;
|
||||
if (aArea == CustomizableUI.AREA_PANEL) {
|
||||
let panel = document.getElementById(kPanelId);
|
||||
panel.addEventListener("popupshowing", updateButton);
|
||||
}
|
||||
},
|
||||
onWidgetRemoved: (aWidgetId, aPrevArea) => {
|
||||
if (aWidgetId != this.id)
|
||||
return;
|
||||
if (aPrevArea == CustomizableUI.AREA_PANEL) {
|
||||
let panel = document.getElementById(kPanelId);
|
||||
panel.removeEventListener("popupshowing", updateButton);
|
||||
}
|
||||
},
|
||||
onWidgetInstanceRemoved: (aWidgetId, aDoc) => {
|
||||
if (aWidgetId != this.id || aDoc != document)
|
||||
return;
|
||||
|
||||
CustomizableUI.removeListener(listener);
|
||||
let panel = aDoc.getElementById(kPanelId);
|
||||
panel.removeEventListener("popupshowing", updateButton);
|
||||
}
|
||||
};
|
||||
CustomizableUI.addListener(listener);
|
||||
}
|
||||
}];
|
||||
|
|
|
@ -63,5 +63,10 @@ copy-button.tooltiptext = Copy
|
|||
paste-button.label = Paste
|
||||
paste-button.tooltiptext = Paste
|
||||
|
||||
# LOCALIZATION NOTE (feed-button.tooltiptext): Use the unicode ellipsis char,
|
||||
# \u2026, or use "..." if \u2026 doesn't suit traditions in your locale.
|
||||
feed-button.label = Subscribe
|
||||
feed-button.tooltiptext = Subscribe to this page…
|
||||
|
||||
characterencoding-button.label = Character Encoding
|
||||
characterencoding-button.tooltiptext = Character encoding
|
||||
|
|
|
@ -715,18 +715,18 @@ toolbar > .customization-target > toolbarpaletteitem > #history-panelmenu {
|
|||
list-style-image: url("moz-icon://stock/gtk-home?size=menu&state=disabled");
|
||||
}
|
||||
|
||||
#characterencoding-panelmenu[customizableui-areatype="toolbar"],
|
||||
#characterencoding-button[customizableui-areatype="toolbar"],
|
||||
#developer-button[customizableui-areatype="toolbar"] {
|
||||
list-style-image: url(chrome://browser/skin/menuPanel.png);
|
||||
}
|
||||
|
||||
#characterencoding-panelmenu[customizableui-areatype="toolbar"] > .toolbarbutton-icon,
|
||||
#characterencoding-button[customizableui-areatype="toolbar"] > .toolbarbutton-icon,
|
||||
#developer-button[customizableui-areatype="toolbar"] > .toolbarbutton-icon {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
#characterencoding-panelmenu[customizableui-areatype="toolbar"] {
|
||||
-moz-image-region: rect(0px, 216px, 24px, 192px);
|
||||
#characterencoding-button[customizableui-areatype="toolbar"] {
|
||||
-moz-image-region: rect(0px, 480px, 32px, 448px);
|
||||
}
|
||||
|
||||
#developer-button[customizableui-areatype="toolbar"] {
|
||||
|
|
|
@ -475,10 +475,14 @@ toolbarbutton.bookmark-item > menupopup {
|
|||
-moz-image-region: rect(18px, 306px, 36px, 288px);
|
||||
}
|
||||
|
||||
#charset-button@toolbarButtonPressed@ {
|
||||
#characterencoding-button@toolbarButtonPressed@ {
|
||||
-moz-image-region: rect(18px, 324px, 36px, 306px);
|
||||
}
|
||||
|
||||
#characterencoding-button[open] {
|
||||
-moz-image-region: rect(36px, 324px, 54px, 306px);
|
||||
}
|
||||
|
||||
#new-window-button@toolbarButtonPressed@ {
|
||||
-moz-image-region: rect(18px, 342px, 36px, 324px);
|
||||
}
|
||||
|
@ -696,6 +700,18 @@ toolbarbutton.bookmark-item > menupopup {
|
|||
-moz-image-region: rect(36px, 576px, 72px, 540px);
|
||||
}
|
||||
|
||||
#characterencoding-button[customizableui-areatype="toolbar"] {
|
||||
-moz-image-region: rect(0, 648px, 36px, 612px);
|
||||
}
|
||||
|
||||
#characterencoding-button[customizableui-areatype="toolbar"]:hover:active:not([disabled="true"]) {
|
||||
-moz-image-region: rect(36px, 648px, 72px, 612px);
|
||||
}
|
||||
|
||||
#characterencoding-button[customizableui-areatype="toolbar"][open] {
|
||||
-moz-image-region: rect(72px, 648px, 108px, 612px);
|
||||
}
|
||||
|
||||
#new-window-button[customizableui-areatype="toolbar"] {
|
||||
-moz-image-region: rect(0, 684px, 36px, 648px);
|
||||
}
|
||||
|
@ -918,6 +934,11 @@ toolbarbutton.bookmark-item > menupopup {
|
|||
-moz-image-region: rect(0px, 896px, 64px, 832px);
|
||||
}
|
||||
|
||||
#characterencoding-button[customizableui-areatype="menu-panel"],
|
||||
toolbarpaletteitem[place="palette"] > #characterencoding-button {
|
||||
-moz-image-region: rect(0, 960px, 64px, 896px);
|
||||
}
|
||||
|
||||
#new-window-button[customizableui-areatype="menu-panel"],
|
||||
toolbarpaletteitem[place="palette"] > #new-window-button {
|
||||
-moz-image-region: rect(0px, 1024px, 64px, 960px);
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
%filter substitution
|
||||
|
||||
%define primaryToolbarButtons #back-button, #forward-button, #home-button, #print-button, #downloads-button, #downloads-indicator, #bookmarks-menu-button, #new-tab-button, #new-window-button, #cut-button, #copy-button, #paste-button, #fullscreen-button, #zoom-out-button, #zoom-reset-button, #zoom-in-button, #sync-button, #feed-button, #alltabs-button, #tabview-button, #webrtc-status-button, #social-share-button, #open-file-button, #find-button, #developer-button, #preferences-button, #privatebrowsing-button, #save-page-button, #add-ons-button, #history-panelmenu, #nav-bar-overflow-button, #PanelUI-menu-button
|
||||
%define primaryToolbarButtons #back-button, #forward-button, #home-button, #print-button, #downloads-button, #downloads-indicator, #bookmarks-menu-button, #new-tab-button, #new-window-button, #cut-button, #copy-button, #paste-button, #fullscreen-button, #zoom-out-button, #zoom-reset-button, #zoom-in-button, #sync-button, #feed-button, #alltabs-button, #tabview-button, #webrtc-status-button, #social-share-button, #open-file-button, #find-button, #developer-button, #preferences-button, #privatebrowsing-button, #save-page-button, #add-ons-button, #history-panelmenu, #nav-bar-overflow-button, #PanelUI-menu-button, #characterencoding-button
|
||||
|
|
|
@ -352,3 +352,17 @@ toolbarbutton.panel-multiview-anchor {
|
|||
border-bottom-left-radius: 2px;
|
||||
}
|
||||
|
||||
.PanelUI-characterEncodingView-list > toolbarbutton[current] {
|
||||
-moz-padding-start: 2px;
|
||||
}
|
||||
|
||||
.PanelUI-characterEncodingView-list > toolbarbutton[current] > .toolbarbutton-text,
|
||||
#customizationui-widget-panel .PanelUI-characterEncodingView-list > toolbarbutton[current] > .toolbarbutton-text {
|
||||
-moz-padding-start: 0px;
|
||||
}
|
||||
|
||||
.PanelUI-characterEncodingView-list > toolbarbutton[current]::before {
|
||||
content: "✓";
|
||||
display: -moz-box;
|
||||
width: 12px;
|
||||
}
|
||||
|
|
|
@ -60,6 +60,11 @@ toolbarpaletteitem[place="palette"] > #social-share-button {
|
|||
-moz-image-region: rect(0px, 448px, 32px, 416px);
|
||||
}
|
||||
|
||||
#characterencoding-button[customizableui-areatype="menu-panel"],
|
||||
toolbarpaletteitem[place="palette"] > #characterencoding-button {
|
||||
-moz-image-region: rect(0px, 480px, 32px, 448px);
|
||||
}
|
||||
|
||||
#new-window-button[customizableui-areatype="menu-panel"],
|
||||
toolbarpaletteitem[place="palette"] > #new-window-button {
|
||||
-moz-image-region: rect(0px, 512px, 32px, 480px);
|
||||
|
|
|
@ -66,6 +66,10 @@
|
|||
-moz-image-region: rect(0, 288px, 18px, 270px);
|
||||
}
|
||||
|
||||
#characterencoding-button[customizableui-areatype="toolbar"]{
|
||||
-moz-image-region: rect(0, 324px, 18px, 306px);
|
||||
}
|
||||
|
||||
#new-window-button[customizableui-areatype="toolbar"] {
|
||||
-moz-image-region: rect(0, 342px, 18px, 324px);
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче