Add "Stop Sync" button
Stops all syncing (not just file syncing like in 4.0) as soon as possible.
This commit is contained in:
parent
7729dcafc0
commit
1da18e4ca7
9 changed files with 134 additions and 42 deletions
|
@ -112,6 +112,7 @@
|
|||
max-width: 28px;
|
||||
}
|
||||
|
||||
#zotero-tb-sync-stop .toolbarbutton-icon,
|
||||
#zotero-tb-sync-error .toolbarbutton-icon {
|
||||
width: 16px;
|
||||
}
|
||||
|
|
|
@ -282,6 +282,7 @@ Zotero.Sync.Storage.Engine.prototype.start = Zotero.Promise.coroutine(function*
|
|||
|
||||
|
||||
Zotero.Sync.Storage.Engine.prototype.stop = function () {
|
||||
Zotero.debug("Stopping file syncing for " + this.library.name);
|
||||
for (let type in this.queues) {
|
||||
this.queues[type].stop();
|
||||
}
|
||||
|
|
|
@ -49,7 +49,6 @@ Zotero.Sync.Data.Engine = function (options) {
|
|||
this.libraryID = options.libraryID;
|
||||
this.library = Zotero.Libraries.get(options.libraryID);
|
||||
this.libraryTypeID = this.library.libraryTypeID;
|
||||
this.requests = [];
|
||||
this.uploadBatchSize = 25;
|
||||
this.uploadDeletionBatchSize = 50;
|
||||
|
||||
|
@ -93,6 +92,8 @@ Zotero.Sync.Data.Engine.prototype.start = Zotero.Promise.coroutine(function* ()
|
|||
this.libraryTypeID = info.userID;
|
||||
}
|
||||
|
||||
this._statusCheck();
|
||||
|
||||
// Check if we've synced this library with the current architecture yet
|
||||
var libraryVersion = this.library.libraryVersion;
|
||||
if (!libraryVersion || libraryVersion == -1) {
|
||||
|
@ -101,6 +102,8 @@ Zotero.Sync.Data.Engine.prototype.start = Zotero.Promise.coroutine(function* ()
|
|||
libraryVersion = this.library.libraryVersion;
|
||||
}
|
||||
|
||||
this._statusCheck();
|
||||
|
||||
// Perform a full sync if necessary, passing the getVersions() results if available.
|
||||
//
|
||||
// The full-sync flag (libraryID == -1) is set at the end of a successful upgrade, so this
|
||||
|
@ -120,6 +123,8 @@ Zotero.Sync.Data.Engine.prototype.start = Zotero.Promise.coroutine(function* ()
|
|||
|
||||
sync:
|
||||
while (true) {
|
||||
this._statusCheck();
|
||||
|
||||
let downloadResult, uploadResult;
|
||||
|
||||
try {
|
||||
|
@ -199,17 +204,11 @@ Zotero.Sync.Data.Engine.prototype.start = Zotero.Promise.coroutine(function* ()
|
|||
|
||||
|
||||
/**
|
||||
* Stop all active requests
|
||||
*
|
||||
* @return {Promise<PromiseInspection[]>} Promise from Zotero.Promise.settle()
|
||||
* Stop the sync process
|
||||
*/
|
||||
Zotero.Sync.Data.Engine.prototype.stop = function () {
|
||||
var funcs;
|
||||
var request;
|
||||
while (request = this.requests.shift()) {
|
||||
funcs.push(() => request.stop());
|
||||
}
|
||||
return Zotero.Promise.settle(funcs);
|
||||
Zotero.debug("Stopping sync for " + this.library.name);
|
||||
this._stopping = true;
|
||||
}
|
||||
|
||||
|
||||
|
@ -254,7 +253,7 @@ Zotero.Sync.Data.Engine.prototype._startDownload = Zotero.Promise.coroutine(func
|
|||
// Get other object types
|
||||
//
|
||||
for (let objectType of Zotero.DataObjectUtilities.getTypesForLibrary(this.libraryID)) {
|
||||
this._failedCheck();
|
||||
this._statusCheck();
|
||||
|
||||
// For items, fetch top-level items first
|
||||
//
|
||||
|
@ -496,7 +495,7 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = async function (objectType,
|
|||
keys.forEach(key => objectData[key] = null);
|
||||
|
||||
while (true) {
|
||||
this._failedCheck();
|
||||
this._statusCheck();
|
||||
|
||||
// Get data we've downloaded in a previous loop but failed to process
|
||||
var json = [];
|
||||
|
@ -537,7 +536,7 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = async function (objectType,
|
|||
await Zotero.Promise.map(
|
||||
json,
|
||||
async function (batch) {
|
||||
this._failedCheck();
|
||||
this._statusCheck();
|
||||
|
||||
Zotero.debug(`Processing batch of downloaded ${objectTypePlural} in ${this.library.name}`);
|
||||
|
||||
|
@ -559,6 +558,10 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = async function (objectType,
|
|||
this._getOptions({
|
||||
onObjectProcessed: () => {
|
||||
num++;
|
||||
// Check for stop every 5 items
|
||||
if (num % 5 == 0) {
|
||||
this._statusCheck();
|
||||
}
|
||||
},
|
||||
// Increase the notifier batch size as we go, so that new items start coming in
|
||||
// one by one but then switch to larger chunks
|
||||
|
@ -616,7 +619,7 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = async function (objectType,
|
|||
await this._restoreRestoredCollectionItems(restored);
|
||||
}
|
||||
|
||||
this._failedCheck();
|
||||
this._statusCheck();
|
||||
|
||||
// If all requests were successful, such that we had a chance to see all keys, remove keys we
|
||||
// didn't see from the sync queue so they don't keep being retried forever
|
||||
|
@ -665,6 +668,8 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = async function (objectType,
|
|||
|
||||
// Show conflict resolution window
|
||||
if (conflicts.length) {
|
||||
this._statusCheck();
|
||||
|
||||
let results = await Zotero.Sync.Data.Local.processConflicts(
|
||||
objectType, this.libraryID, conflicts, this._getOptions()
|
||||
);
|
||||
|
@ -827,6 +832,8 @@ Zotero.Sync.Data.Engine.prototype._downloadDeletions = Zotero.Promise.coroutine(
|
|||
}
|
||||
|
||||
if (conflicts.length) {
|
||||
this._statusCheck();
|
||||
|
||||
// Sort conflicts by Date Modified
|
||||
conflicts.sort(function (a, b) {
|
||||
var d1 = a.left.dateModified;
|
||||
|
@ -936,6 +943,8 @@ Zotero.Sync.Data.Engine.prototype._startUpload = Zotero.Promise.coroutine(functi
|
|||
|
||||
// Get unsynced local objects for each object type
|
||||
for (let objectType of Zotero.DataObjectUtilities.getTypesForLibrary(this.libraryID)) {
|
||||
this._statusCheck();
|
||||
|
||||
let objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType);
|
||||
let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType);
|
||||
|
||||
|
@ -1015,6 +1024,8 @@ Zotero.Sync.Data.Engine.prototype._startUpload = Zotero.Promise.coroutine(functi
|
|||
try {
|
||||
Zotero.debug(JSON.stringify(objectIDs));
|
||||
for (let objectType in objectIDs) {
|
||||
this._statusCheck();
|
||||
|
||||
libraryVersion = yield this._uploadObjects(
|
||||
objectType, objectIDs[objectType], libraryVersion
|
||||
);
|
||||
|
@ -1022,6 +1033,8 @@ Zotero.Sync.Data.Engine.prototype._startUpload = Zotero.Promise.coroutine(functi
|
|||
|
||||
Zotero.debug(JSON.stringify(objectDeletions));
|
||||
for (let objectType in objectDeletions) {
|
||||
this._statusCheck();
|
||||
|
||||
libraryVersion = yield this._uploadDeletions(
|
||||
objectType, objectDeletions[objectType], libraryVersion
|
||||
);
|
||||
|
@ -1562,7 +1575,7 @@ Zotero.Sync.Data.Engine.prototype._fullSync = Zotero.Promise.coroutine(function*
|
|||
|
||||
// Get object types
|
||||
for (let objectType of Zotero.DataObjectUtilities.getTypesForLibrary(this.libraryID)) {
|
||||
this._failedCheck();
|
||||
this._statusCheck();
|
||||
|
||||
let objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType);
|
||||
let ObjectType = Zotero.Utilities.capitalize(objectType);
|
||||
|
@ -1774,6 +1787,19 @@ Zotero.Sync.Data.Engine.prototype._checkObjectUploadError = Zotero.Promise.corou
|
|||
});
|
||||
|
||||
|
||||
Zotero.Sync.Data.Engine.prototype._statusCheck = function () {
|
||||
this._stopCheck();
|
||||
this._failedCheck();
|
||||
}
|
||||
|
||||
|
||||
Zotero.Sync.Data.Engine.prototype._stopCheck = function () {
|
||||
if (!this._stopping) return;
|
||||
Zotero.debug("Sync stopped for " + this.library.name);
|
||||
throw new Zotero.Sync.UserCancelledException;
|
||||
}
|
||||
|
||||
|
||||
Zotero.Sync.Data.Engine.prototype._failedCheck = function () {
|
||||
if (this.stopOnError && this.failed) {
|
||||
Zotero.logError("Stopping on error");
|
||||
|
|
|
@ -44,7 +44,7 @@ Zotero.Sync.Data.FullTextEngine = function (options) {
|
|||
this.setStatus = options.setStatus || function () {};
|
||||
this.onError = options.onError || function (e) {};
|
||||
this.stopOnError = options.stopOnError;
|
||||
this.requestPromises = [];
|
||||
this._stopping = false;
|
||||
this.failed = false;
|
||||
}
|
||||
|
||||
|
@ -61,6 +61,8 @@ Zotero.Sync.Data.FullTextEngine.prototype.start = Zotero.Promise.coroutine(funct
|
|||
Zotero.debug("Library version hasn't changed -- skipping full-text download");
|
||||
}
|
||||
|
||||
this._stopCheck();
|
||||
|
||||
yield this._upload();
|
||||
})
|
||||
|
||||
|
@ -95,22 +97,26 @@ Zotero.Sync.Data.FullTextEngine.prototype._download = Zotero.Promise.coroutine(f
|
|||
}
|
||||
|
||||
this.requestPromises = [];
|
||||
for (let key of keys) {
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=449811
|
||||
let tmpKey = key;
|
||||
this.requestPromises.push(
|
||||
|
||||
yield Zotero.Promise.map(
|
||||
keys,
|
||||
(key) => {
|
||||
this._stopCheck();
|
||||
this.apiClient.getFullTextForItem(
|
||||
this.library.libraryType, this.library.libraryTypeID, key
|
||||
)
|
||||
.then(function (results) {
|
||||
.then((results) => {
|
||||
this._stopCheck();
|
||||
if (!results) return;
|
||||
return Zotero.Fulltext.setItemContent(
|
||||
this.libraryID, tmpKey, results.data, results.version
|
||||
this.libraryID, key, results.data, results.version
|
||||
)
|
||||
}.bind(this))
|
||||
})
|
||||
},
|
||||
// Prepare twice the number of concurrent requests
|
||||
{ concurrency: 8 }
|
||||
);
|
||||
}
|
||||
yield Zotero.Promise.all(this.requestPromises);
|
||||
|
||||
yield Zotero.FullText.setLibraryVersion(this.libraryID, results.libraryVersion);
|
||||
});
|
||||
|
||||
|
@ -125,6 +131,8 @@ Zotero.Sync.Data.FullTextEngine.prototype._upload = Zotero.Promise.coroutine(fun
|
|||
|
||||
let lastItemID = 0;
|
||||
while (true) {
|
||||
this._stopCheck();
|
||||
|
||||
let objs = yield Zotero.FullText.getUnsyncedContent(this.libraryID, {
|
||||
maxSize: this.MAX_BATCH_SIZE,
|
||||
maxItems: this.MAX_BATCH_ITEMS,
|
||||
|
@ -190,6 +198,13 @@ Zotero.Sync.Data.FullTextEngine.prototype._upload = Zotero.Promise.coroutine(fun
|
|||
|
||||
|
||||
Zotero.Sync.Data.FullTextEngine.prototype.stop = Zotero.Promise.coroutine(function* () {
|
||||
// TODO: Cancel requests
|
||||
throw new Error("Unimplemented");
|
||||
// TODO: Cancel requests?
|
||||
this._stopping = true;
|
||||
})
|
||||
|
||||
|
||||
Zotero.Sync.Data.FullTextEngine.prototype._stopCheck = function () {
|
||||
if (!this._stopping) return;
|
||||
Zotero.debug("Full-text sync stopped for " + this.library.name);
|
||||
throw new Zotero.Sync.UserCancelledException;
|
||||
}
|
||||
|
|
|
@ -68,10 +68,10 @@ Zotero.Sync.Runner_Module = function (options = {}) {
|
|||
var _autoSyncTimer;
|
||||
var _firstInSession = true;
|
||||
var _syncInProgress = false;
|
||||
var _stopping = false;
|
||||
var _manualSyncRequired = false; // TODO: make public?
|
||||
|
||||
var _syncEngines = [];
|
||||
var _storageEngines = [];
|
||||
var _currentEngine = null;
|
||||
var _storageControllers = {};
|
||||
|
||||
var _lastSyncStatus;
|
||||
|
@ -117,6 +117,7 @@ Zotero.Sync.Runner_Module = function (options = {}) {
|
|||
return false;
|
||||
}
|
||||
_syncInProgress = true;
|
||||
_stopping = false;
|
||||
|
||||
yield Zotero.Notifier.trigger('start', 'sync', []);
|
||||
|
||||
|
@ -139,9 +140,10 @@ Zotero.Sync.Runner_Module = function (options = {}) {
|
|||
let client = this.getAPIClient({ apiKey });
|
||||
let keyInfo = yield this.checkAccess(client, options);
|
||||
|
||||
_stopCheck();
|
||||
|
||||
let emptyLibraryContinue = yield this.checkEmptyLibrary(keyInfo);
|
||||
if (!emptyLibraryContinue) {
|
||||
yield this.end(options);
|
||||
Zotero.debug("Syncing cancelled because user library is empty");
|
||||
return false;
|
||||
}
|
||||
|
@ -150,7 +152,6 @@ Zotero.Sync.Runner_Module = function (options = {}) {
|
|||
.getService(Components.interfaces.nsIWindowMediator);
|
||||
let lastWin = wm.getMostRecentWindow("navigator:browser");
|
||||
if (!(yield Zotero.Sync.Data.Local.checkUser(lastWin, keyInfo.userID, keyInfo.username))) {
|
||||
yield this.end(options);
|
||||
Zotero.debug("User cancelled sync on username mismatch");
|
||||
return false;
|
||||
}
|
||||
|
@ -179,6 +180,8 @@ Zotero.Sync.Runner_Module = function (options = {}) {
|
|||
options.libraries ? Array.from(options.libraries) : []
|
||||
);
|
||||
|
||||
_stopCheck();
|
||||
|
||||
// If items not yet loaded for libraries we need, load them now
|
||||
for (let libraryID of librariesToSync) {
|
||||
let library = Zotero.Libraries.get(libraryID);
|
||||
|
@ -187,10 +190,14 @@ Zotero.Sync.Runner_Module = function (options = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
_stopCheck();
|
||||
|
||||
// Sync data and files, and then repeat if necessary
|
||||
let attempt = 1;
|
||||
let successfulLibraries = new Set(librariesToSync);
|
||||
while (librariesToSync.length) {
|
||||
_stopCheck();
|
||||
|
||||
if (attempt > 3) {
|
||||
// TODO: Back off and/or nicer error
|
||||
throw new Error("Too many sync attempts -- stopping");
|
||||
|
@ -201,6 +208,8 @@ Zotero.Sync.Runner_Module = function (options = {}) {
|
|||
successfulLibraries.delete(libraryID);
|
||||
});
|
||||
|
||||
_stopCheck();
|
||||
|
||||
// Run file sync on all libraries that passed the last data sync
|
||||
librariesToSync = yield _doFileSync(nextLibraries, engineOptions);
|
||||
if (librariesToSync.length) {
|
||||
|
@ -208,6 +217,8 @@ Zotero.Sync.Runner_Module = function (options = {}) {
|
|||
continue;
|
||||
}
|
||||
|
||||
_stopCheck();
|
||||
|
||||
// Run full-text sync on all libraries that haven't failed a data sync
|
||||
librariesToSync = yield _doFullTextSync([...successfulLibraries], engineOptions);
|
||||
if (librariesToSync.length) {
|
||||
|
@ -550,13 +561,15 @@ Zotero.Sync.Runner_Module = function (options = {}) {
|
|||
var _doDataSync = Zotero.Promise.coroutine(function* (libraries, options, skipUpdateLastSyncTime) {
|
||||
var successfulLibraries = [];
|
||||
for (let libraryID of libraries) {
|
||||
_stopCheck();
|
||||
try {
|
||||
let opts = {};
|
||||
Object.assign(opts, options);
|
||||
opts.libraryID = libraryID;
|
||||
|
||||
let engine = new Zotero.Sync.Data.Engine(opts);
|
||||
yield engine.start();
|
||||
_currentEngine = new Zotero.Sync.Data.Engine(opts);
|
||||
yield _currentEngine.start();
|
||||
_currentEngine = null;
|
||||
successfulLibraries.push(libraryID);
|
||||
}
|
||||
catch (e) {
|
||||
|
@ -597,6 +610,7 @@ Zotero.Sync.Runner_Module = function (options = {}) {
|
|||
this.setSyncStatus(Zotero.getString('sync.status.syncingFiles'));
|
||||
var resyncLibraries = []
|
||||
for (let libraryID of libraries) {
|
||||
_stopCheck();
|
||||
try {
|
||||
let opts = {};
|
||||
Object.assign(opts, options);
|
||||
|
@ -611,8 +625,9 @@ Zotero.Sync.Runner_Module = function (options = {}) {
|
|||
throw new Error("Too many file sync attempts for library " + libraryID);
|
||||
}
|
||||
tries--;
|
||||
let engine = new Zotero.Sync.Storage.Engine(opts);
|
||||
let results = yield engine.start();
|
||||
_currentEngine = new Zotero.Sync.Storage.Engine(opts);
|
||||
let results = yield _currentEngine.start();
|
||||
_currentEngine = null;
|
||||
if (results.syncRequired) {
|
||||
resyncLibraries.push(libraryID);
|
||||
}
|
||||
|
@ -624,6 +639,15 @@ Zotero.Sync.Runner_Module = function (options = {}) {
|
|||
}
|
||||
}
|
||||
catch (e) {
|
||||
if (e instanceof Zotero.Sync.UserCancelledException) {
|
||||
if (e.advanceToNextLibrary) {
|
||||
Zotero.debug("Storage sync cancelled for library " + libraryID + " -- "
|
||||
+ "advancing to next library");
|
||||
continue;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
Zotero.debug("File sync failed for library " + libraryID);
|
||||
Zotero.logError(e);
|
||||
this.checkError(e);
|
||||
|
@ -652,15 +676,21 @@ Zotero.Sync.Runner_Module = function (options = {}) {
|
|||
this.setSyncStatus(Zotero.getString('sync.status.syncingFullText'));
|
||||
var resyncLibraries = [];
|
||||
for (let libraryID of libraries) {
|
||||
_stopCheck();
|
||||
try {
|
||||
let opts = {};
|
||||
Object.assign(opts, options);
|
||||
opts.libraryID = libraryID;
|
||||
|
||||
let engine = new Zotero.Sync.Data.FullTextEngine(opts);
|
||||
yield engine.start();
|
||||
_currentEngine = new Zotero.Sync.Data.FullTextEngine(opts);
|
||||
yield _currentEngine.start();
|
||||
_currentEngine = null;
|
||||
}
|
||||
catch (e) {
|
||||
if (e instanceof Zotero.Sync.UserCancelledException) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (e instanceof Zotero.HTTP.UnexpectedStatusException && e.status == 412) {
|
||||
resyncLibraries.push(libraryID);
|
||||
continue;
|
||||
|
@ -762,8 +792,11 @@ Zotero.Sync.Runner_Module = function (options = {}) {
|
|||
|
||||
|
||||
this.stop = function () {
|
||||
_syncEngines.forEach(engine => engine.stop());
|
||||
_storageEngines.forEach(engine => engine.stop());
|
||||
this.setSyncStatus(Zotero.getString('sync.stopping'));
|
||||
_stopping = true;
|
||||
if (_currentEngine) {
|
||||
_currentEngine.stop();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -1143,14 +1176,17 @@ Zotero.Sync.Runner_Module = function (options = {}) {
|
|||
|
||||
// Update sync icon
|
||||
var syncIcon = doc.getElementById('zotero-tb-sync');
|
||||
var stopIcon = doc.getElementById('zotero-tb-sync-stop');
|
||||
if (state == 'animate') {
|
||||
syncIcon.setAttribute('status', state);
|
||||
// Disable button while spinning
|
||||
syncIcon.disabled = true;
|
||||
stopIcon.hidden = false;
|
||||
}
|
||||
else {
|
||||
syncIcon.removeAttribute('status');
|
||||
syncIcon.disabled = false;
|
||||
stopIcon.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1386,4 +1422,11 @@ Zotero.Sync.Runner_Module = function (options = {}) {
|
|||
// Set as .apiKey on Runner in tests or set in login manager
|
||||
return _apiKey || Zotero.Sync.Data.Local.getAPIKey()
|
||||
})
|
||||
|
||||
|
||||
function _stopCheck() {
|
||||
if (_stopping) {
|
||||
throw new Zotero.Sync.UserCancelledException;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -210,6 +210,10 @@
|
|||
</toolbarbutton>
|
||||
</hbox>
|
||||
<hbox align="center" pack="end">
|
||||
<toolbarbutton id="zotero-tb-sync-stop"
|
||||
tooltiptext="&zotero.sync.stop;"
|
||||
oncommand="this.hidden = true; Zotero.Sync.Runner.stop()"
|
||||
hidden="true"/>
|
||||
<hbox id="zotero-tb-sync-progress-box" hidden="true" align="center">
|
||||
<!-- TODO: localize -->
|
||||
<toolbarbutton id="zotero-tb-sync-storage-cancel"
|
||||
|
|
|
@ -237,6 +237,7 @@
|
|||
|
||||
<!ENTITY zotero.integration.references.label "References in Bibliography">
|
||||
|
||||
<!ENTITY zotero.sync.stop "Stop Sync">
|
||||
<!ENTITY zotero.sync.error "Sync Error">
|
||||
<!ENTITY zotero.sync.storage.progress "Progress:">
|
||||
<!ENTITY zotero.sync.storage.downloads "Downloads:">
|
||||
|
|
|
@ -865,6 +865,7 @@ styles.abbreviations.missingInfo = The abbreviations file "%1$S" does not specif
|
|||
|
||||
sync.sync = Sync
|
||||
sync.syncWith = Sync with %S
|
||||
sync.stopping = Stopping…
|
||||
sync.cancel = Cancel Sync
|
||||
sync.openSyncPreferences = Open Sync Preferences
|
||||
sync.resetGroupAndSync = Reset Group and Sync
|
||||
|
|
|
@ -555,7 +555,7 @@
|
|||
list-style-image: url('chrome://zotero/skin/toolbar-go-arrow.png');
|
||||
}
|
||||
|
||||
#zotero-tb-sync-storage-cancel
|
||||
#zotero-tb-sync-stop
|
||||
{
|
||||
list-style-image: url(chrome://zotero/skin/control_stop_blue.png);
|
||||
margin-right: 0;
|
||||
|
@ -745,7 +745,7 @@
|
|||
.zotero-menuitem-create-report { list-style-image: url('chrome://zotero/skin/treeitem-report@2x.png'); }
|
||||
#zotero-tb-advanced-search { list-style-image: url('chrome://zotero/skin/toolbar-advanced-search@2x.png'); }
|
||||
#zotero-tb-locate { list-style-image: url('chrome://zotero/skin/toolbar-go-arrow@2x.png'); }
|
||||
#zotero-tb-sync-storage-cancel { list-style-image: url(chrome://zotero/skin/control_stop_blue@2x.png); margin-right: 0; }
|
||||
#zotero-tb-sync-stop { list-style-image: url(chrome://zotero/skin/control_stop_blue@2x.png); margin-right: 0; }
|
||||
#zotero-tb-sync-error { list-style-image: url(chrome://zotero/skin/error@2x.png); }
|
||||
#zotero-tb-sync-error[state=warning] { list-style-image: url(chrome://zotero/skin/warning@2x.png); }
|
||||
#zotero-pane-stack[fullscreenmode="true"] #zotero-tb-fullscreen { list-style-image: url('chrome://zotero/skin/toolbar-fullscreen-top@2x.png'); }
|
||||
|
|
Loading…
Add table
Reference in a new issue