Desktop version of the API for offline mode

This commit is contained in:
Artur
2025-10-01 18:59:46 +03:00
parent 6923eac440
commit dfa3ecbfe1
4 changed files with 343 additions and 150 deletions

View File

@ -53,6 +53,7 @@
</div>
<button id="saveConfigBtn" class="btn-text-default i18n" style="margin-top: 16px; width: 100%;">Save</button>
<button id="saveAsTextBtn" class="btn-text-default i18n" style="margin-top: 16px; width: 100%;">Save as text</button>
<button id="useDesktopApp" class="btn-text-default i18n" style="margin-top: 16px; width: 100%;">I have the Zotero application running</button>
</div>
<div id="mainState" class="flexCol flexSize display-none">

File diff suppressed because one or more lines are too long

View File

@ -16,6 +16,7 @@
*
*/
(function () {
var mode = "online"; // "offline", "online"
var counter = 0; // счетчик отправленных запросов (используется чтобы знать показывать "not found" или нет)
var displayNoneClass = "display-none";
var blurClass = "blur";
@ -119,7 +120,8 @@
refreshBtn: document.getElementById('refreshBtn'),
saveAsTextBtn: document.getElementById('saveAsTextBtn'),
synchronizeBtn: document.getElementById('synchronizeBtn'),
checkOmitAuthor: document.getElementById('omitAuthor')
checkOmitAuthor: document.getElementById('omitAuthor'),
useDesktopApp: document.getElementById('useDesktopApp')
};
var selectedScroller;
@ -127,106 +129,75 @@
window.Asc.plugin.init = function () {
showLoader(true);
setTimeout(function () { searchField.focus(); },100);
updateCslItems(true, false, false, false);
sdk = window.Asc.plugin.zotero.api({});
initSdkApis();
window.Asc.plugin.onTranslate = applyTranslations;
elements.logoutLink.onclick = function (e) {
sdk.clearSettings();
switchAuthState('config');
return true;
};
function searchFor(text) {
if (elements.mainState.classList.contains(displayNoneClass)) return;
text = text.trim();
if (!text) return;
if (text == lastSearch.text) return;
lastSearch.text = text;
lastSearch.obj = null;
lastSearch.groups = [];
clearLibrary();
var groups = sdk.getUserGropus();
loadLibrary(sdk.items(text), true, true, !groups.length, false, true);
if (groups.length) {
for (var i = 0; i < groups.length; i++) {
loadLibrary(sdk.groups(lastSearch.text, groups[i]), true, false, (i == groups.length -1), true, true );
}
}
};
elements.searchField.onkeypress = function (e) {
if (e.keyCode == 13) searchFor(e.target.value);
};
elements.searchField.onblur = function (e) {
setTimeout(function () { searchFor(e.target.value); }, 500);
};
elements.searchField.onkeyup = function (e) {
switchClass(elements.searchClear, displayNoneClass, !e.target.value);
};
elements.searchClear.onclick = function (e) {
if (e.target.classList.contains(displayNoneClass)) return true;
switchClass(elements.searchClear, displayNoneClass, true);
elements.searchField.value = "";
lastSearch.text = "";
clearLibrary();
return true;
};
elements.cancelBtn.onclick = function (e) {
var ids = [];
for (var id in selected.items) {
ids.push(id);
}
for (var i = 0; i < ids.length; i++) {
removeSelected(ids[i]);
}
};
elements.saveConfigBtn.onclick = function (e) {
var apikey = elements.apiKeyConfigField.value.trim();
if (apikey) {
sdk.setApiKey(apikey)
.then(function () {
switchAuthState("main");
}).catch(function () {
showError(getMessage("Invalid API key"));
});
}
};
elements.refreshBtn.onclick = function() {
showLoader(true);
updateCslItems(true, true, false, false);
};
elements.synchronizeBtn.onclick = function() {
synchronizeData();
};
elements.insertBibBtn.onclick = function() {
showLoader(true);
// TODO #there
// updateCslItems(true, false, true, false);
updateCslItems(true, true, true, false);
};
elements.insertLinkBtn.onclick = function() {
showLoader(true);
updateCslItems(true, false, false, true);
};
elements.saveAsTextBtn.onclick = function() {
showLoader(true);
saveAs();
}
addEventListeners();
selectedScroller = initScrollBox(elements.selectedHolder, elements.selectedThumb);
docsScroller = initScrollBox(elements.docsHolder, elements.docsThumb, checkDocsScroll);
fetch("https://www.zotero.org/styles-files/styles.json")
.then(function (resp) { return resp.json(); })
initSelectBoxes();
elements.styleSelectList.onopen = function () {
elements.styleSelectList.style.width = (elements.styleWrapper.clientWidth - 2) + "px";
}
};
window.Asc.plugin.onThemeChanged = function(theme)
{
window.Asc.plugin.onThemeChangedBase(theme);
var rules = '.selectArrow > span { background-color: ' + window.Asc.plugin.theme['text-normal'] + '}\n';
rules += '.link { color : ' + window.Asc.plugin.theme['text-normal'] + ';}\n';
rules += '.control.select { background-color : ' + window.Asc.plugin.theme['background-normal'] + ';}\n';
rules += '.control { color : ' + window.Asc.plugin.theme['text-normal'] + '; border-color : ' + window.Asc.plugin.theme['border-regular-control'] + '}\n';
rules += '.selectList > span { background-color: ' + window.Asc.plugin.theme['background-normal'] + '; ';
rules += 'color : ' + window.Asc.plugin.theme['text-normal'] + '; }\n';
rules += '.selectList > span:hover { background-color : ' + window.Asc.plugin.theme['highlight-button-hover'] + '; color : ' + window.Asc.plugin.theme['text-normal'] + '}\n';
rules += '.selectList > span[selected=""] { background-color : ' + window.Asc.plugin.theme['highlight-button-pressed'] + ';' + '; color : ' + window.Asc.plugin.theme['text-normal'] + '}';
var styleTheme = document.createElement('style');
styleTheme.type = 'text/css';
styleTheme.innerHTML = rules;
document.getElementsByTagName('head')[0].appendChild(styleTheme);
};
function initSdkApis() {
sdk.isApiAvailable().then(function(availableApis) {
console.log('apis', availableApis);
if (availableApis.desktop) {
switchAuthState('main');
return true;
}
if (availableApis.online) {
sdk.setUseDesktopApp(false);
if (sdk.hasSettings()) {
switchAuthState('main');
} else {
switchAuthState('config');
}
return true;
}
if (availableApis.permissionNeeded) {
console.warn('You need to open access to the API');
console.warn('Edit -> Settings -> Advanced -> Allow other applications on this computer to communicate with Zotero');
console.warn('Restart Zotero');
return false;
}
console.warn('You are offline');
console.warn('Zotero is not running');
return false;
}).then(function(isApiAvailable) {
isApiAvailable && loadStyles();
});
}
function loadStyles() {
sdk.getStyles()
.then(function (json) {
var lastStyle = getLastUsedStyle();
var found = false;
@ -303,7 +274,105 @@
}
})
.catch(function (err) { });
}
function addEventListeners() {
elements.useDesktopApp.onclick = function() {
showLoader(true);
initSdkApis().finally(function() {
showLoader(false);
});
};
elements.logoutLink.onclick = function (e) {
sdk.clearSettings();
switchAuthState('config');
return true;
};
function searchFor(text) {
console.log('searchFor', text);
if (elements.mainState.classList.contains(displayNoneClass)) return;
text = text.trim();
if (!text) return;
if (text == lastSearch.text) return;
lastSearch.text = text;
lastSearch.obj = null;
lastSearch.groups = [];
clearLibrary();
var groups = sdk.getUserGropus();
loadLibrary(sdk.items(text), true, true, !groups.length, false, true);
if (groups.length) {
for (var i = 0; i < groups.length; i++) {
loadLibrary(sdk.groups(lastSearch.text, groups[i]), true, false, (i == groups.length -1), true, true );
}
}
};
elements.searchField.onkeypress = function (e) {
if (e.keyCode == 13) searchFor(e.target.value);
};
elements.searchField.onblur = function (e) {
setTimeout(function () { searchFor(e.target.value); }, 500);
};
elements.searchField.onkeyup = function (e) {
switchClass(elements.searchClear, displayNoneClass, !e.target.value);
};
elements.searchClear.onclick = function (e) {
if (e.target.classList.contains(displayNoneClass)) return true;
switchClass(elements.searchClear, displayNoneClass, true);
elements.searchField.value = "";
lastSearch.text = "";
clearLibrary();
return true;
};
elements.cancelBtn.onclick = function (e) {
var ids = [];
for (var id in selected.items) {
ids.push(id);
}
for (var i = 0; i < ids.length; i++) {
removeSelected(ids[i]);
}
};
elements.saveConfigBtn.onclick = function (e) {
var apikey = elements.apiKeyConfigField.value.trim();
if (apikey) {
sdk.setApiKey(apikey)
.then(function () {
switchAuthState("main");
}).catch(function () {
showError(getMessage("Invalid API key"));
});
}
};
elements.refreshBtn.onclick = function() {
showLoader(true);
updateCslItems(true, true, false, false);
};
elements.synchronizeBtn.onclick = function() {
synchronizeData();
};
elements.insertBibBtn.onclick = function() {
showLoader(true);
// TODO #there
// updateCslItems(true, false, true, false);
updateCslItems(true, true, true, false);
};
elements.insertLinkBtn.onclick = function() {
showLoader(true);
updateCslItems(true, false, false, true);
};
elements.saveAsTextBtn.onclick = function() {
showLoader(true);
saveAs();
}
elements.styleSelect.oninput = function (e, filter) {
var input = elements.styleSelect;
var filter = filter !== undefined ? filter : input.value.toLowerCase();
@ -349,35 +418,7 @@
});
selectedLocale = val;
};
initSelectBoxes();
elements.styleSelectList.onopen = function () {
elements.styleSelectList.style.width = (elements.styleWrapper.clientWidth - 2) + "px";
}
if (sdk.hasSettings()) {
switchAuthState('main');
} else {
switchAuthState('config');
}
};
window.Asc.plugin.onThemeChanged = function(theme)
{
window.Asc.plugin.onThemeChangedBase(theme);
var rules = '.selectArrow > span { background-color: ' + window.Asc.plugin.theme['text-normal'] + '}\n';
rules += '.link { color : ' + window.Asc.plugin.theme['text-normal'] + ';}\n';
rules += '.control.select { background-color : ' + window.Asc.plugin.theme['background-normal'] + ';}\n';
rules += '.control { color : ' + window.Asc.plugin.theme['text-normal'] + '; border-color : ' + window.Asc.plugin.theme['border-regular-control'] + '}\n';
rules += '.selectList > span { background-color: ' + window.Asc.plugin.theme['background-normal'] + '; ';
rules += 'color : ' + window.Asc.plugin.theme['text-normal'] + '; }\n';
rules += '.selectList > span:hover { background-color : ' + window.Asc.plugin.theme['highlight-button-hover'] + '; color : ' + window.Asc.plugin.theme['text-normal'] + '}\n';
rules += '.selectList > span[selected=""] { background-color : ' + window.Asc.plugin.theme['highlight-button-pressed'] + ';' + '; color : ' + window.Asc.plugin.theme['text-normal'] + '}';
var styleTheme = document.createElement('style');
styleTheme.type = 'text/css';
styleTheme.innerHTML = rules;
document.getElementsByTagName('head')[0].appendChild(styleTheme);
};
}
var scrollBoxes = [];
function initScrollBox(holder, thumb, onscroll) {
@ -1003,8 +1044,6 @@
retrieveLocale: function (id) { return locales[id]; },
retrieveItem: function (id) {
var item = CSLCitationStorage.get(id);
console.log(id);
console.log(item.toOldJSON());
return item.toOldJSON();
}
},
@ -1104,7 +1143,7 @@
bUpdateItems = true;
}
keys.push(citationID);
keysL = keysL.concat(CSLCitationStorage.get(citationID).getSuppressAuthors());
keysL.push(CSLCitationStorage.get(citationID).getSuppressAuthors());
}
try {
@ -1116,27 +1155,25 @@
formatter.updateItems(arrIds);
}
var objects = [];
keys.forEach(function(element) {
removeSelected(element);
objects.push(CSLCitationStorage.get(element));
});
var element = keys[0];
var obj = CSLCitationStorage.get(element);
removeSelected(element);
// TODO может ещё очистить поиск (подумать над этим)
elements.tempDiv.innerHTML = formatter.makeCitationCluster(keysL);
objects.forEach(function(obj) {
var field = {
"Value" : citPrefixNew + ' ' + citSuffixNew + JSON.stringify(obj.toJSON()),
"Content" : elements.tempDiv.innerText
};
window.Asc.plugin.executeMethod("AddAddinField", [field], function() {
showLoader(false);
// TODO есть проблема, что в плагине мы индексы обновили, а вот в документе нет (по идее надо обновить и индексы в документе перед вставкой)
// но тогда у нас уедет селект и новое поле вставится не там, поэтому пока обновлять приходится в конце
// такая же проблем с вставкой библиографии (при обнолении индексов в плагине надо бы их обновлять и в документе тоже)
updateCslItems(true, true, false, false);
});
elements.tempDiv.innerHTML = formatter.makeCitationCluster(keysL[0]);
var field = {
"Value" : citPrefixNew + ' ' + citSuffixNew + JSON.stringify(obj.toJSON()),
"Content" : elements.tempDiv.innerText
};
window.Asc.plugin.executeMethod("AddAddinField", [field], function() {
showLoader(false);
// TODO есть проблема, что в плагине мы индексы обновили, а вот в документе нет (по идее надо обновить и индексы в документе перед вставкой)
// но тогда у нас уедет селект и новое поле вставится не там, поэтому пока обновлять приходится в конце
// такая же проблем с вставкой библиографии (при обнолении индексов в плагине надо бы их обновлять и в документе тоже)
updateCslItems(true, true, false, false);
});
} catch (e) {
showError(e);
}

View File

@ -17,12 +17,67 @@
*/
(function () {
if (!window.Asc.plugin.zotero) window.Asc.plugin.zotero = {};
window.Asc.plugin.zotero.isLocalDesktop = (function(){
if (window.navigator && window.navigator.userAgent.toLowerCase().indexOf("ascdesktopeditor") < 0)
return false;
if (window.location && window.location.protocol == "file:")
return true;
if (window.document && window.document.currentScript && 0 == window.document.currentScript.src.indexOf("file:///"))
return true;
return false;
})();
window.Asc.plugin.zotero.isOnlineAvailable = !window.Asc.plugin.zotero.isLocalDesktop;
window.Asc.plugin.zotero.api = function (cfg) {
var apiKey;
var userId;
var userId = 0;
var userGroups = [];
var baseUrl = cfg.baseUrl || "https://api.zotero.org/";
var restApiUrl = "https://api.zotero.org/";
var desktopApiUrl = "http://127.0.0.1:23119/api/"; // users/0/items"
var baseUrl = cfg.baseUrl || restApiUrl;
var STYLES_URL = "https://www.zotero.org/styles-files/styles.json";
var STYLES_LOCAL_URL = "./resources/csl/styles.json";
function getRequestWithOfflineSupport(url) {
if (window.Asc.plugin.zotero.isOnlineAvailable) {
return getRequest(url);
} else {
return getDesktopRequest(url.href);
}
}
/**
* Make a GET request to the local Zotero API.
* @param {string} url - URL of the request.
* @returns {Promise<{responseStatus: number, responseText: string, status: string, statusCode: number}>}
*/
function getDesktopRequest(url) {
return new Promise(function (resolve, reject) {
window.AscSimpleRequest.createRequest({
url: url,
method: "GET",
headers: {
"Zotero-API-Version": "3",
"User-Agent": "AscDesktopEditor",
},
complete: function(e) {
resolve(e);
},
error: function(e) {
console.error(e);
if ( e.statusCode == -102 ) e.statusCode = 404;
reject({error: e.statusCode, message: "Internal error"});
}
});
});
}
/**
* Make a GET request to the online Zotero API.
* @param {string} url - URL of the request.
* @returns {Promise<{body: ReadableStream, bodyUsed: boolean, headers: Headers, ok: boolean, redirected: boolean, status: string, statusText: string, type: string, url: string}>}
*/
function getRequest(url) {
return new Promise(function (resolve, reject) {
var headers = {
@ -32,7 +87,10 @@
fetch(url, {
headers: headers
}).then(function (res) {
if (!res.ok) throw new Error(res.status + " " + res.statusText);
if (!res.ok) {
reject(new Error(res.status + " " + res.statusText));
return;
};
resolve(res);
}).catch(function (err) {
reject(err);
@ -42,8 +100,11 @@
function buildGetRequest(path, query) {
var url = new URL(path, baseUrl);
if (!window.Asc.plugin.zotero.isOnlineAvailable) {
url = new URL(path, desktopApiUrl);
}
for (var key in query) url.searchParams.append(key, query[key]);
return getRequest(url);
return getRequestWithOfflineSupport(url);
}
function items(search, itemsID) {
@ -56,7 +117,12 @@
} else if (itemsID) {
props.itemKey = itemsID.join(',');
}
parseItemsResponse(buildGetRequest("users/" + userId + "/items", props), resolve, reject, userId);
if (window.Asc.plugin.zotero.isOnlineAvailable) {
parseItemsResponse(buildGetRequest("users/" + userId + "/items", props), resolve, reject, userId);
} else {
parseDesktopItemsResponse(buildGetRequest("users/" + userId + "/items", props), resolve, reject, userId);
}
});
}
@ -70,7 +136,26 @@
} else if (itemsID) {
props.itemKey = itemsID.join(',');
}
parseItemsResponse(buildGetRequest("groups/" + groupId + "/items", props), resolve, reject, groupId);
if (window.Asc.plugin.zotero.isOnlineAvailable) {
parseItemsResponse(buildGetRequest("groups/" + groupId + "/items", props), resolve, reject, groupId);
} else {
parseDesktopItemsResponse(buildGetRequest("groups/" + groupId + "/items", props), resolve, reject, groupId);
}
});
}
/**
* @returns {Promise}
*/
function getStyles() {
return new Promise(function (resolve, reject) {
if (window.Asc.plugin.zotero.isOnlineAvailable) {
return fetch(STYLES_URL)
.then(function (resp) { return resp.json(); });
} else {
return fetch(STYLES_LOCAL_URL)
.then(function (resp) { return resp.json(); });
}
});
}
@ -165,6 +250,32 @@
userGroups = [];
}
/**
* @param {Promise} promise - promise from items request
* @param {function} resolve - resolve function for returned promise
* @param {function} reject - reject function for returned promise
* @param {string} id - id of request
* @returns {Promise<{items: {items: Array<Object>}, id: string}}>} promise with items and optional next function
*/
function parseDesktopItemsResponse(promise, resolve, reject, id) {
promise.then(function (res) {
var obj = {
items: {items: JSON.parse(res.responseText)},
id: id
};
resolve(obj);
}).catch(function (err) {
reject(err);
});
}
/**
* @param {Promise} promise - promise from items request
* @param {function} resolve - resolve function for returned promise
* @param {function} reject - reject function for returned promise
* @param {string} id - id of request
* @returns {Promise<{items: {items: Array<Object>}, id: string}}>} promise with items and optional next function
*/
function parseItemsResponse(promise, resolve, reject, id) {
promise.then(function (res) {
res.json().then(function (json) {
@ -174,6 +285,7 @@
id: id
};
if (links.next) {
console.error('next', links.next);
obj.next = function () {
return new Promise(function (rs, rj) {
parseItemsResponse(getRequest(links.next), rs, rj, id);
@ -202,6 +314,45 @@
return links;
}
/**
* @returns {Promise<{desktop: boolean, online: boolean, permissionNeeded: boolean}>}
* TODO: add Promise.allSettled for getRequest and getDesktopRequest
*/
function isApiAvailable() {
return new Promise(function (resolve) {
let apiAvailable = {
desktop: false,
online: false,
permissionNeeded: false
};
getRequest(restApiUrl).then(function (res) {
return res.status === 200;
}).catch(function() {
return false;
}).then(function (isOnlineAvailable) {
apiAvailable.online = isOnlineAvailable;
return getDesktopRequest(desktopApiUrl)
}).then(function (res) {
if (res.status == 403) {
apiAvailable.permissionNeeded = true;
}
return res.responseStatus === 200;
}).catch(function() {
return false;
}).then(function (isDesktopAvailable) {
apiAvailable.desktop = isDesktopAvailable;
window.Asc.plugin.zotero.isLocalDesktop = apiAvailable.desktop;
window.Asc.plugin.zotero.isOnlineAvailable = apiAvailable.online;
resolve(apiAvailable);
});
});
}
function setUseDesktopApp(isUseDesktopApp) {
}
return {
items: items,
groups: groups,
@ -210,7 +361,10 @@
hasSettings: getSettings,
clearSettings: clearSettings,
setApiKey: setApiKey,
getUserId: getUserId
getUserId: getUserId,
isApiAvailable: isApiAvailable,
setUseDesktopApp: setUseDesktopApp,
getStyles: getStyles
}
}
})();