diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..47c3064 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,36 @@ +name: Build Website Test + +on: + pull_request_target: + types: [opened, reopened, synchronize] + branches: + - master + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-versions: ["18.12.0"] + + steps: + - name: "Checkout Project" + uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.head.sha }} + + + - name: Setup NodeJS Environment ${{ matrix.node-versions }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + + - name: "Install Node Module" + run: npm install --force + + - name: "Build" + run: npm run build \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..23011b1 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,42 @@ +name: Deploy Website + +on: + push: + branches: + - master + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-versions: ["18.12.0"] + + steps: + - name: "Checkout Project" + uses: actions/checkout@v2 + with: + persist-credentials: false + fetch-depth: 0 + + - name: Setup NodeJS Environment ${{ matrix.node-versions }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + + - name: "Install Node Module" + run: npm install --force + + - name: "Build" + run: npm run build + + - name: "Deploy" + uses: ad-m/github-push-action@master + with: + github_token: ${{ secrets.API_TOKEN_GITHUB }} + repository: 'FutureScholarsOrg/paperlib-website' + force: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0de27e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +**/node_modules +**/dist +**/.DS_Store +.vscode diff --git a/.vitepress/cache/deps/_metadata.json b/.vitepress/cache/deps/_metadata.json new file mode 100644 index 0000000..2f18988 --- /dev/null +++ b/.vitepress/cache/deps/_metadata.json @@ -0,0 +1,43 @@ +{ + "hash": "7a709cd1", + "configHash": "a3e21e4f", + "lockfileHash": "76300b38", + "browserHash": "b8b8dbb8", + "optimized": { + "vue": { + "src": "../../../node_modules/.pnpm/vue@3.4.3/node_modules/vue/dist/vue.runtime.esm-bundler.js", + "file": "vue.js", + "fileHash": "f93c9df4", + "needsInterop": false + }, + "vitepress > @vue/devtools-api": { + "src": "../../../node_modules/.pnpm/@vue+devtools-api@6.5.1/node_modules/@vue/devtools-api/lib/esm/index.js", + "file": "vitepress___@vue_devtools-api.js", + "fileHash": "df7054a3", + "needsInterop": false + }, + "vitepress > @vueuse/integrations/useFocusTrap": { + "src": "../../../node_modules/.pnpm/@vueuse+integrations@10.7.1_focus-trap@7.5.4_vue@3.4.3/node_modules/@vueuse/integrations/useFocusTrap.mjs", + "file": "vitepress___@vueuse_integrations_useFocusTrap.js", + "fileHash": "d1b171c1", + "needsInterop": false + }, + "vitepress > mark.js/src/vanilla.js": { + "src": "../../../node_modules/.pnpm/mark.js@8.11.1/node_modules/mark.js/src/vanilla.js", + "file": "vitepress___mark__js_src_vanilla__js.js", + "fileHash": "95dad6ec", + "needsInterop": false + }, + "vitepress > minisearch": { + "src": "../../../node_modules/.pnpm/minisearch@6.3.0/node_modules/minisearch/dist/es/index.js", + "file": "vitepress___minisearch.js", + "fileHash": "17688ff9", + "needsInterop": false + } + }, + "chunks": { + "chunk-4LAYHTU6": { + "file": "chunk-4LAYHTU6.js" + } + } +} \ No newline at end of file diff --git a/.vitepress/cache/deps/chunk-4LAYHTU6.js b/.vitepress/cache/deps/chunk-4LAYHTU6.js new file mode 100644 index 0000000..6319df2 --- /dev/null +++ b/.vitepress/cache/deps/chunk-4LAYHTU6.js @@ -0,0 +1,11224 @@ +// node_modules/.pnpm/@vue+shared@3.4.3/node_modules/@vue/shared/dist/shared.esm-bundler.js +function makeMap(str, expectsLowerCase) { + const set2 = new Set(str.split(",")); + return expectsLowerCase ? (val) => set2.has(val.toLowerCase()) : (val) => set2.has(val); +} +var EMPTY_OBJ = true ? Object.freeze({}) : {}; +var EMPTY_ARR = true ? Object.freeze([]) : []; +var NOOP = () => { +}; +var NO = () => false; +var isOn = (key) => key.charCodeAt(0) === 111 && key.charCodeAt(1) === 110 && // uppercase letter +(key.charCodeAt(2) > 122 || key.charCodeAt(2) < 97); +var isModelListener = (key) => key.startsWith("onUpdate:"); +var extend = Object.assign; +var remove = (arr, el) => { + const i = arr.indexOf(el); + if (i > -1) { + arr.splice(i, 1); + } +}; +var hasOwnProperty = Object.prototype.hasOwnProperty; +var hasOwn = (val, key) => hasOwnProperty.call(val, key); +var isArray = Array.isArray; +var isMap = (val) => toTypeString(val) === "[object Map]"; +var isSet = (val) => toTypeString(val) === "[object Set]"; +var isDate = (val) => toTypeString(val) === "[object Date]"; +var isRegExp = (val) => toTypeString(val) === "[object RegExp]"; +var isFunction = (val) => typeof val === "function"; +var isString = (val) => typeof val === "string"; +var isSymbol = (val) => typeof val === "symbol"; +var isObject = (val) => val !== null && typeof val === "object"; +var isPromise = (val) => { + return (isObject(val) || isFunction(val)) && isFunction(val.then) && isFunction(val.catch); +}; +var objectToString = Object.prototype.toString; +var toTypeString = (value) => objectToString.call(value); +var toRawType = (value) => { + return toTypeString(value).slice(8, -1); +}; +var isPlainObject = (val) => toTypeString(val) === "[object Object]"; +var isIntegerKey = (key) => isString(key) && key !== "NaN" && key[0] !== "-" && "" + parseInt(key, 10) === key; +var isReservedProp = makeMap( + // the leading comma is intentional so empty string "" is also included + ",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted" +); +var isBuiltInDirective = makeMap( + "bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo" +); +var cacheStringFunction = (fn) => { + const cache = /* @__PURE__ */ Object.create(null); + return (str) => { + const hit = cache[str]; + return hit || (cache[str] = fn(str)); + }; +}; +var camelizeRE = /-(\w)/g; +var camelize = cacheStringFunction((str) => { + return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : ""); +}); +var hyphenateRE = /\B([A-Z])/g; +var hyphenate = cacheStringFunction( + (str) => str.replace(hyphenateRE, "-$1").toLowerCase() +); +var capitalize = cacheStringFunction((str) => { + return str.charAt(0).toUpperCase() + str.slice(1); +}); +var toHandlerKey = cacheStringFunction((str) => { + const s = str ? `on${capitalize(str)}` : ``; + return s; +}); +var hasChanged = (value, oldValue) => !Object.is(value, oldValue); +var invokeArrayFns = (fns, arg) => { + for (let i = 0; i < fns.length; i++) { + fns[i](arg); + } +}; +var def = (obj, key, value) => { + Object.defineProperty(obj, key, { + configurable: true, + enumerable: false, + value + }); +}; +var looseToNumber = (val) => { + const n = parseFloat(val); + return isNaN(n) ? val : n; +}; +var toNumber = (val) => { + const n = isString(val) ? Number(val) : NaN; + return isNaN(n) ? val : n; +}; +var _globalThis; +var getGlobalThis = () => { + return _globalThis || (_globalThis = typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : {}); +}; +var GLOBALS_ALLOWED = "Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console,Error"; +var isGloballyAllowed = makeMap(GLOBALS_ALLOWED); +function normalizeStyle(value) { + if (isArray(value)) { + const res = {}; + for (let i = 0; i < value.length; i++) { + const item = value[i]; + const normalized = isString(item) ? parseStringStyle(item) : normalizeStyle(item); + if (normalized) { + for (const key in normalized) { + res[key] = normalized[key]; + } + } + } + return res; + } else if (isString(value) || isObject(value)) { + return value; + } +} +var listDelimiterRE = /;(?![^(]*\))/g; +var propertyDelimiterRE = /:([^]+)/; +var styleCommentRE = /\/\*[^]*?\*\//g; +function parseStringStyle(cssText) { + const ret = {}; + cssText.replace(styleCommentRE, "").split(listDelimiterRE).forEach((item) => { + if (item) { + const tmp = item.split(propertyDelimiterRE); + tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim()); + } + }); + return ret; +} +function stringifyStyle(styles) { + let ret = ""; + if (!styles || isString(styles)) { + return ret; + } + for (const key in styles) { + const value = styles[key]; + const normalizedKey = key.startsWith(`--`) ? key : hyphenate(key); + if (isString(value) || typeof value === "number") { + ret += `${normalizedKey}:${value};`; + } + } + return ret; +} +function normalizeClass(value) { + let res = ""; + if (isString(value)) { + res = value; + } else if (isArray(value)) { + for (let i = 0; i < value.length; i++) { + const normalized = normalizeClass(value[i]); + if (normalized) { + res += normalized + " "; + } + } + } else if (isObject(value)) { + for (const name in value) { + if (value[name]) { + res += name + " "; + } + } + } + return res.trim(); +} +function normalizeProps(props) { + if (!props) + return null; + let { class: klass, style } = props; + if (klass && !isString(klass)) { + props.class = normalizeClass(klass); + } + if (style) { + props.style = normalizeStyle(style); + } + return props; +} +var HTML_TAGS = "html,body,base,head,link,meta,style,title,address,article,aside,footer,header,hgroup,h1,h2,h3,h4,h5,h6,nav,section,div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,summary,template,blockquote,iframe,tfoot"; +var SVG_TAGS = "svg,animate,animateMotion,animateTransform,circle,clipPath,color-profile,defs,desc,discard,ellipse,feBlend,feColorMatrix,feComponentTransfer,feComposite,feConvolveMatrix,feDiffuseLighting,feDisplacementMap,feDistantLight,feDropShadow,feFlood,feFuncA,feFuncB,feFuncG,feFuncR,feGaussianBlur,feImage,feMerge,feMergeNode,feMorphology,feOffset,fePointLight,feSpecularLighting,feSpotLight,feTile,feTurbulence,filter,foreignObject,g,hatch,hatchpath,image,line,linearGradient,marker,mask,mesh,meshgradient,meshpatch,meshrow,metadata,mpath,path,pattern,polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,text,textPath,title,tspan,unknown,use,view"; +var MATH_TAGS = "annotation,annotation-xml,maction,maligngroup,malignmark,math,menclose,merror,mfenced,mfrac,mfraction,mglyph,mi,mlabeledtr,mlongdiv,mmultiscripts,mn,mo,mover,mpadded,mphantom,mprescripts,mroot,mrow,ms,mscarries,mscarry,msgroup,msline,mspace,msqrt,msrow,mstack,mstyle,msub,msubsup,msup,mtable,mtd,mtext,mtr,munder,munderover,none,semantics"; +var VOID_TAGS = "area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr"; +var isHTMLTag = makeMap(HTML_TAGS); +var isSVGTag = makeMap(SVG_TAGS); +var isMathMLTag = makeMap(MATH_TAGS); +var isVoidTag = makeMap(VOID_TAGS); +var specialBooleanAttrs = `itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly`; +var isSpecialBooleanAttr = makeMap(specialBooleanAttrs); +var isBooleanAttr = makeMap( + specialBooleanAttrs + `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,inert,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected` +); +function includeBooleanAttr(value) { + return !!value || value === ""; +} +var isKnownHtmlAttr = makeMap( + `accept,accept-charset,accesskey,action,align,allow,alt,async,autocapitalize,autocomplete,autofocus,autoplay,background,bgcolor,border,buffered,capture,challenge,charset,checked,cite,class,code,codebase,color,cols,colspan,content,contenteditable,contextmenu,controls,coords,crossorigin,csp,data,datetime,decoding,default,defer,dir,dirname,disabled,download,draggable,dropzone,enctype,enterkeyhint,for,form,formaction,formenctype,formmethod,formnovalidate,formtarget,headers,height,hidden,high,href,hreflang,http-equiv,icon,id,importance,inert,integrity,ismap,itemprop,keytype,kind,label,lang,language,loading,list,loop,low,manifest,max,maxlength,minlength,media,min,multiple,muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,preload,radiogroup,readonly,referrerpolicy,rel,required,reversed,rows,rowspan,sandbox,scope,scoped,selected,shape,size,sizes,slot,span,spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,target,title,translate,type,usemap,value,width,wrap` +); +var isKnownSvgAttr = makeMap( + `xmlns,accent-height,accumulate,additive,alignment-baseline,alphabetic,amplitude,arabic-form,ascent,attributeName,attributeType,azimuth,baseFrequency,baseline-shift,baseProfile,bbox,begin,bias,by,calcMode,cap-height,class,clip,clipPathUnits,clip-path,clip-rule,color,color-interpolation,color-interpolation-filters,color-profile,color-rendering,contentScriptType,contentStyleType,crossorigin,cursor,cx,cy,d,decelerate,descent,diffuseConstant,direction,display,divisor,dominant-baseline,dur,dx,dy,edgeMode,elevation,enable-background,end,exponent,fill,fill-opacity,fill-rule,filter,filterRes,filterUnits,flood-color,flood-opacity,font-family,font-size,font-size-adjust,font-stretch,font-style,font-variant,font-weight,format,from,fr,fx,fy,g1,g2,glyph-name,glyph-orientation-horizontal,glyph-orientation-vertical,glyphRef,gradientTransform,gradientUnits,hanging,height,href,hreflang,horiz-adv-x,horiz-origin-x,id,ideographic,image-rendering,in,in2,intercept,k,k1,k2,k3,k4,kernelMatrix,kernelUnitLength,kerning,keyPoints,keySplines,keyTimes,lang,lengthAdjust,letter-spacing,lighting-color,limitingConeAngle,local,marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mask,maskContentUnits,maskUnits,mathematical,max,media,method,min,mode,name,numOctaves,offset,opacity,operator,order,orient,orientation,origin,overflow,overline-position,overline-thickness,panose-1,paint-order,path,pathLength,patternContentUnits,patternTransform,patternUnits,ping,pointer-events,points,pointsAtX,pointsAtY,pointsAtZ,preserveAlpha,preserveAspectRatio,primitiveUnits,r,radius,referrerPolicy,refX,refY,rel,rendering-intent,repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,result,rotate,rx,ry,scale,seed,shape-rendering,slope,spacing,specularConstant,specularExponent,speed,spreadMethod,startOffset,stdDeviation,stemh,stemv,stitchTiles,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,string,stroke,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,stroke-width,style,surfaceScale,systemLanguage,tabindex,tableValues,target,targetX,targetY,text-anchor,text-decoration,text-rendering,textLength,to,transform,transform-origin,type,u1,u2,underline-position,underline-thickness,unicode,unicode-bidi,unicode-range,units-per-em,v-alphabetic,v-hanging,v-ideographic,v-mathematical,values,vector-effect,version,vert-adv-y,vert-origin-x,vert-origin-y,viewBox,viewTarget,visibility,width,widths,word-spacing,writing-mode,x,x-height,x1,x2,xChannelSelector,xlink:actuate,xlink:arcrole,xlink:href,xlink:role,xlink:show,xlink:title,xlink:type,xmlns:xlink,xml:base,xml:lang,xml:space,y,y1,y2,yChannelSelector,z,zoomAndPan` +); +function looseCompareArrays(a, b) { + if (a.length !== b.length) + return false; + let equal = true; + for (let i = 0; equal && i < a.length; i++) { + equal = looseEqual(a[i], b[i]); + } + return equal; +} +function looseEqual(a, b) { + if (a === b) + return true; + let aValidType = isDate(a); + let bValidType = isDate(b); + if (aValidType || bValidType) { + return aValidType && bValidType ? a.getTime() === b.getTime() : false; + } + aValidType = isSymbol(a); + bValidType = isSymbol(b); + if (aValidType || bValidType) { + return a === b; + } + aValidType = isArray(a); + bValidType = isArray(b); + if (aValidType || bValidType) { + return aValidType && bValidType ? looseCompareArrays(a, b) : false; + } + aValidType = isObject(a); + bValidType = isObject(b); + if (aValidType || bValidType) { + if (!aValidType || !bValidType) { + return false; + } + const aKeysCount = Object.keys(a).length; + const bKeysCount = Object.keys(b).length; + if (aKeysCount !== bKeysCount) { + return false; + } + for (const key in a) { + const aHasKey = a.hasOwnProperty(key); + const bHasKey = b.hasOwnProperty(key); + if (aHasKey && !bHasKey || !aHasKey && bHasKey || !looseEqual(a[key], b[key])) { + return false; + } + } + } + return String(a) === String(b); +} +function looseIndexOf(arr, val) { + return arr.findIndex((item) => looseEqual(item, val)); +} +var toDisplayString = (val) => { + return isString(val) ? val : val == null ? "" : isArray(val) || isObject(val) && (val.toString === objectToString || !isFunction(val.toString)) ? JSON.stringify(val, replacer, 2) : String(val); +}; +var replacer = (_key, val) => { + if (val && val.__v_isRef) { + return replacer(_key, val.value); + } else if (isMap(val)) { + return { + [`Map(${val.size})`]: [...val.entries()].reduce( + (entries, [key, val2], i) => { + entries[stringifySymbol(key, i) + " =>"] = val2; + return entries; + }, + {} + ) + }; + } else if (isSet(val)) { + return { + [`Set(${val.size})`]: [...val.values()].map((v) => stringifySymbol(v)) + }; + } else if (isSymbol(val)) { + return stringifySymbol(val); + } else if (isObject(val) && !isArray(val) && !isPlainObject(val)) { + return String(val); + } + return val; +}; +var stringifySymbol = (v, i = "") => { + var _a; + return isSymbol(v) ? `Symbol(${(_a = v.description) != null ? _a : i})` : v; +}; + +// node_modules/.pnpm/@vue+reactivity@3.4.3/node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js +function warn(msg, ...args) { + console.warn(`[Vue warn] ${msg}`, ...args); +} +var activeEffectScope; +var EffectScope = class { + constructor(detached = false) { + this.detached = detached; + this._active = true; + this.effects = []; + this.cleanups = []; + this.parent = activeEffectScope; + if (!detached && activeEffectScope) { + this.index = (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push( + this + ) - 1; + } + } + get active() { + return this._active; + } + run(fn) { + if (this._active) { + const currentEffectScope = activeEffectScope; + try { + activeEffectScope = this; + return fn(); + } finally { + activeEffectScope = currentEffectScope; + } + } else if (true) { + warn(`cannot run an inactive effect scope.`); + } + } + /** + * This should only be called on non-detached scopes + * @internal + */ + on() { + activeEffectScope = this; + } + /** + * This should only be called on non-detached scopes + * @internal + */ + off() { + activeEffectScope = this.parent; + } + stop(fromParent) { + if (this._active) { + let i, l; + for (i = 0, l = this.effects.length; i < l; i++) { + this.effects[i].stop(); + } + for (i = 0, l = this.cleanups.length; i < l; i++) { + this.cleanups[i](); + } + if (this.scopes) { + for (i = 0, l = this.scopes.length; i < l; i++) { + this.scopes[i].stop(true); + } + } + if (!this.detached && this.parent && !fromParent) { + const last = this.parent.scopes.pop(); + if (last && last !== this) { + this.parent.scopes[this.index] = last; + last.index = this.index; + } + } + this.parent = void 0; + this._active = false; + } + } +}; +function effectScope(detached) { + return new EffectScope(detached); +} +function recordEffectScope(effect2, scope = activeEffectScope) { + if (scope && scope.active) { + scope.effects.push(effect2); + } +} +function getCurrentScope() { + return activeEffectScope; +} +function onScopeDispose(fn) { + if (activeEffectScope) { + activeEffectScope.cleanups.push(fn); + } else if (true) { + warn( + `onScopeDispose() is called when there is no active effect scope to be associated with.` + ); + } +} +var activeEffect; +var ReactiveEffect = class { + constructor(fn, trigger2, scheduler, scope) { + this.fn = fn; + this.trigger = trigger2; + this.scheduler = scheduler; + this.active = true; + this.deps = []; + this._dirtyLevel = 3; + this._trackId = 0; + this._runnings = 0; + this._queryings = 0; + this._depsLength = 0; + recordEffectScope(this, scope); + } + get dirty() { + if (this._dirtyLevel === 1) { + this._dirtyLevel = 0; + this._queryings++; + pauseTracking(); + for (const dep of this.deps) { + if (dep.computed) { + triggerComputed(dep.computed); + if (this._dirtyLevel >= 2) { + break; + } + } + } + resetTracking(); + this._queryings--; + } + return this._dirtyLevel >= 2; + } + set dirty(v) { + this._dirtyLevel = v ? 3 : 0; + } + run() { + this._dirtyLevel = 0; + if (!this.active) { + return this.fn(); + } + let lastShouldTrack = shouldTrack; + let lastEffect = activeEffect; + try { + shouldTrack = true; + activeEffect = this; + this._runnings++; + preCleanupEffect(this); + return this.fn(); + } finally { + postCleanupEffect(this); + this._runnings--; + activeEffect = lastEffect; + shouldTrack = lastShouldTrack; + } + } + stop() { + var _a; + if (this.active) { + preCleanupEffect(this); + postCleanupEffect(this); + (_a = this.onStop) == null ? void 0 : _a.call(this); + this.active = false; + } + } +}; +function triggerComputed(computed3) { + return computed3.value; +} +function preCleanupEffect(effect2) { + effect2._trackId++; + effect2._depsLength = 0; +} +function postCleanupEffect(effect2) { + if (effect2.deps && effect2.deps.length > effect2._depsLength) { + for (let i = effect2._depsLength; i < effect2.deps.length; i++) { + cleanupDepEffect(effect2.deps[i], effect2); + } + effect2.deps.length = effect2._depsLength; + } +} +function cleanupDepEffect(dep, effect2) { + const trackId = dep.get(effect2); + if (trackId !== void 0 && effect2._trackId !== trackId) { + dep.delete(effect2); + if (dep.size === 0) { + dep.cleanup(); + } + } +} +function effect(fn, options) { + if (fn.effect instanceof ReactiveEffect) { + fn = fn.effect.fn; + } + const _effect = new ReactiveEffect(fn, NOOP, () => { + if (_effect.dirty) { + _effect.run(); + } + }); + if (options) { + extend(_effect, options); + if (options.scope) + recordEffectScope(_effect, options.scope); + } + if (!options || !options.lazy) { + _effect.run(); + } + const runner = _effect.run.bind(_effect); + runner.effect = _effect; + return runner; +} +function stop(runner) { + runner.effect.stop(); +} +var shouldTrack = true; +var pauseScheduleStack = 0; +var trackStack = []; +function pauseTracking() { + trackStack.push(shouldTrack); + shouldTrack = false; +} +function resetTracking() { + const last = trackStack.pop(); + shouldTrack = last === void 0 ? true : last; +} +function pauseScheduling() { + pauseScheduleStack++; +} +function resetScheduling() { + pauseScheduleStack--; + while (!pauseScheduleStack && queueEffectSchedulers.length) { + queueEffectSchedulers.shift()(); + } +} +function trackEffect(effect2, dep, debuggerEventExtraInfo) { + var _a; + if (dep.get(effect2) !== effect2._trackId) { + dep.set(effect2, effect2._trackId); + const oldDep = effect2.deps[effect2._depsLength]; + if (oldDep !== dep) { + if (oldDep) { + cleanupDepEffect(oldDep, effect2); + } + effect2.deps[effect2._depsLength++] = dep; + } else { + effect2._depsLength++; + } + if (true) { + (_a = effect2.onTrack) == null ? void 0 : _a.call(effect2, extend({ effect: effect2 }, debuggerEventExtraInfo)); + } + } +} +var queueEffectSchedulers = []; +function triggerEffects(dep, dirtyLevel, debuggerEventExtraInfo) { + var _a; + pauseScheduling(); + for (const effect2 of dep.keys()) { + if (!effect2.allowRecurse && effect2._runnings) { + continue; + } + if (effect2._dirtyLevel < dirtyLevel && (!effect2._runnings || dirtyLevel !== 2)) { + const lastDirtyLevel = effect2._dirtyLevel; + effect2._dirtyLevel = dirtyLevel; + if (lastDirtyLevel === 0 && (!effect2._queryings || dirtyLevel !== 2)) { + if (true) { + (_a = effect2.onTrigger) == null ? void 0 : _a.call(effect2, extend({ effect: effect2 }, debuggerEventExtraInfo)); + } + effect2.trigger(); + if (effect2.scheduler) { + queueEffectSchedulers.push(effect2.scheduler); + } + } + } + } + resetScheduling(); +} +var createDep = (cleanup, computed3) => { + const dep = /* @__PURE__ */ new Map(); + dep.cleanup = cleanup; + dep.computed = computed3; + return dep; +}; +var targetMap = /* @__PURE__ */ new WeakMap(); +var ITERATE_KEY = Symbol(true ? "iterate" : ""); +var MAP_KEY_ITERATE_KEY = Symbol(true ? "Map key iterate" : ""); +function track(target, type, key) { + if (shouldTrack && activeEffect) { + let depsMap = targetMap.get(target); + if (!depsMap) { + targetMap.set(target, depsMap = /* @__PURE__ */ new Map()); + } + let dep = depsMap.get(key); + if (!dep) { + depsMap.set(key, dep = createDep(() => depsMap.delete(key))); + } + trackEffect( + activeEffect, + dep, + true ? { + target, + type, + key + } : void 0 + ); + } +} +function trigger(target, type, key, newValue, oldValue, oldTarget) { + const depsMap = targetMap.get(target); + if (!depsMap) { + return; + } + let deps = []; + if (type === "clear") { + deps = [...depsMap.values()]; + } else if (key === "length" && isArray(target)) { + const newLength = Number(newValue); + depsMap.forEach((dep, key2) => { + if (key2 === "length" || !isSymbol(key2) && key2 >= newLength) { + deps.push(dep); + } + }); + } else { + if (key !== void 0) { + deps.push(depsMap.get(key)); + } + switch (type) { + case "add": + if (!isArray(target)) { + deps.push(depsMap.get(ITERATE_KEY)); + if (isMap(target)) { + deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)); + } + } else if (isIntegerKey(key)) { + deps.push(depsMap.get("length")); + } + break; + case "delete": + if (!isArray(target)) { + deps.push(depsMap.get(ITERATE_KEY)); + if (isMap(target)) { + deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)); + } + } + break; + case "set": + if (isMap(target)) { + deps.push(depsMap.get(ITERATE_KEY)); + } + break; + } + } + pauseScheduling(); + for (const dep of deps) { + if (dep) { + triggerEffects( + dep, + 3, + true ? { + target, + type, + key, + newValue, + oldValue, + oldTarget + } : void 0 + ); + } + } + resetScheduling(); +} +function getDepFromReactive(object, key) { + var _a; + return (_a = targetMap.get(object)) == null ? void 0 : _a.get(key); +} +var isNonTrackableKeys = makeMap(`__proto__,__v_isRef,__isVue`); +var builtInSymbols = new Set( + Object.getOwnPropertyNames(Symbol).filter((key) => key !== "arguments" && key !== "caller").map((key) => Symbol[key]).filter(isSymbol) +); +var arrayInstrumentations = createArrayInstrumentations(); +function createArrayInstrumentations() { + const instrumentations = {}; + ["includes", "indexOf", "lastIndexOf"].forEach((key) => { + instrumentations[key] = function(...args) { + const arr = toRaw(this); + for (let i = 0, l = this.length; i < l; i++) { + track(arr, "get", i + ""); + } + const res = arr[key](...args); + if (res === -1 || res === false) { + return arr[key](...args.map(toRaw)); + } else { + return res; + } + }; + }); + ["push", "pop", "shift", "unshift", "splice"].forEach((key) => { + instrumentations[key] = function(...args) { + pauseTracking(); + pauseScheduling(); + const res = toRaw(this)[key].apply(this, args); + resetScheduling(); + resetTracking(); + return res; + }; + }); + return instrumentations; +} +function hasOwnProperty2(key) { + const obj = toRaw(this); + track(obj, "has", key); + return obj.hasOwnProperty(key); +} +var BaseReactiveHandler = class { + constructor(_isReadonly = false, _shallow = false) { + this._isReadonly = _isReadonly; + this._shallow = _shallow; + } + get(target, key, receiver) { + const isReadonly2 = this._isReadonly, shallow = this._shallow; + if (key === "__v_isReactive") { + return !isReadonly2; + } else if (key === "__v_isReadonly") { + return isReadonly2; + } else if (key === "__v_isShallow") { + return shallow; + } else if (key === "__v_raw") { + if (receiver === (isReadonly2 ? shallow ? shallowReadonlyMap : readonlyMap : shallow ? shallowReactiveMap : reactiveMap).get(target) || // receiver is not the reactive proxy, but has the same prototype + // this means the reciever is a user proxy of the reactive proxy + Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver)) { + return target; + } + return; + } + const targetIsArray = isArray(target); + if (!isReadonly2) { + if (targetIsArray && hasOwn(arrayInstrumentations, key)) { + return Reflect.get(arrayInstrumentations, key, receiver); + } + if (key === "hasOwnProperty") { + return hasOwnProperty2; + } + } + const res = Reflect.get(target, key, receiver); + if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) { + return res; + } + if (!isReadonly2) { + track(target, "get", key); + } + if (shallow) { + return res; + } + if (isRef(res)) { + return targetIsArray && isIntegerKey(key) ? res : res.value; + } + if (isObject(res)) { + return isReadonly2 ? readonly(res) : reactive(res); + } + return res; + } +}; +var MutableReactiveHandler = class extends BaseReactiveHandler { + constructor(shallow = false) { + super(false, shallow); + } + set(target, key, value, receiver) { + let oldValue = target[key]; + if (!this._shallow) { + const isOldValueReadonly = isReadonly(oldValue); + if (!isShallow(value) && !isReadonly(value)) { + oldValue = toRaw(oldValue); + value = toRaw(value); + } + if (!isArray(target) && isRef(oldValue) && !isRef(value)) { + if (isOldValueReadonly) { + return false; + } else { + oldValue.value = value; + return true; + } + } + } + const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key); + const result = Reflect.set(target, key, value, receiver); + if (target === toRaw(receiver)) { + if (!hadKey) { + trigger(target, "add", key, value); + } else if (hasChanged(value, oldValue)) { + trigger(target, "set", key, value, oldValue); + } + } + return result; + } + deleteProperty(target, key) { + const hadKey = hasOwn(target, key); + const oldValue = target[key]; + const result = Reflect.deleteProperty(target, key); + if (result && hadKey) { + trigger(target, "delete", key, void 0, oldValue); + } + return result; + } + has(target, key) { + const result = Reflect.has(target, key); + if (!isSymbol(key) || !builtInSymbols.has(key)) { + track(target, "has", key); + } + return result; + } + ownKeys(target) { + track( + target, + "iterate", + isArray(target) ? "length" : ITERATE_KEY + ); + return Reflect.ownKeys(target); + } +}; +var ReadonlyReactiveHandler = class extends BaseReactiveHandler { + constructor(shallow = false) { + super(true, shallow); + } + set(target, key) { + if (true) { + warn( + `Set operation on key "${String(key)}" failed: target is readonly.`, + target + ); + } + return true; + } + deleteProperty(target, key) { + if (true) { + warn( + `Delete operation on key "${String(key)}" failed: target is readonly.`, + target + ); + } + return true; + } +}; +var mutableHandlers = new MutableReactiveHandler(); +var readonlyHandlers = new ReadonlyReactiveHandler(); +var shallowReactiveHandlers = new MutableReactiveHandler( + true +); +var shallowReadonlyHandlers = new ReadonlyReactiveHandler(true); +var toShallow = (value) => value; +var getProto = (v) => Reflect.getPrototypeOf(v); +function get(target, key, isReadonly2 = false, isShallow3 = false) { + target = target["__v_raw"]; + const rawTarget = toRaw(target); + const rawKey = toRaw(key); + if (!isReadonly2) { + if (hasChanged(key, rawKey)) { + track(rawTarget, "get", key); + } + track(rawTarget, "get", rawKey); + } + const { has: has2 } = getProto(rawTarget); + const wrap = isShallow3 ? toShallow : isReadonly2 ? toReadonly : toReactive; + if (has2.call(rawTarget, key)) { + return wrap(target.get(key)); + } else if (has2.call(rawTarget, rawKey)) { + return wrap(target.get(rawKey)); + } else if (target !== rawTarget) { + target.get(key); + } +} +function has(key, isReadonly2 = false) { + const target = this["__v_raw"]; + const rawTarget = toRaw(target); + const rawKey = toRaw(key); + if (!isReadonly2) { + if (hasChanged(key, rawKey)) { + track(rawTarget, "has", key); + } + track(rawTarget, "has", rawKey); + } + return key === rawKey ? target.has(key) : target.has(key) || target.has(rawKey); +} +function size(target, isReadonly2 = false) { + target = target["__v_raw"]; + !isReadonly2 && track(toRaw(target), "iterate", ITERATE_KEY); + return Reflect.get(target, "size", target); +} +function add(value) { + value = toRaw(value); + const target = toRaw(this); + const proto = getProto(target); + const hadKey = proto.has.call(target, value); + if (!hadKey) { + target.add(value); + trigger(target, "add", value, value); + } + return this; +} +function set(key, value) { + value = toRaw(value); + const target = toRaw(this); + const { has: has2, get: get2 } = getProto(target); + let hadKey = has2.call(target, key); + if (!hadKey) { + key = toRaw(key); + hadKey = has2.call(target, key); + } else if (true) { + checkIdentityKeys(target, has2, key); + } + const oldValue = get2.call(target, key); + target.set(key, value); + if (!hadKey) { + trigger(target, "add", key, value); + } else if (hasChanged(value, oldValue)) { + trigger(target, "set", key, value, oldValue); + } + return this; +} +function deleteEntry(key) { + const target = toRaw(this); + const { has: has2, get: get2 } = getProto(target); + let hadKey = has2.call(target, key); + if (!hadKey) { + key = toRaw(key); + hadKey = has2.call(target, key); + } else if (true) { + checkIdentityKeys(target, has2, key); + } + const oldValue = get2 ? get2.call(target, key) : void 0; + const result = target.delete(key); + if (hadKey) { + trigger(target, "delete", key, void 0, oldValue); + } + return result; +} +function clear() { + const target = toRaw(this); + const hadItems = target.size !== 0; + const oldTarget = true ? isMap(target) ? new Map(target) : new Set(target) : void 0; + const result = target.clear(); + if (hadItems) { + trigger(target, "clear", void 0, void 0, oldTarget); + } + return result; +} +function createForEach(isReadonly2, isShallow3) { + return function forEach(callback, thisArg) { + const observed = this; + const target = observed["__v_raw"]; + const rawTarget = toRaw(target); + const wrap = isShallow3 ? toShallow : isReadonly2 ? toReadonly : toReactive; + !isReadonly2 && track(rawTarget, "iterate", ITERATE_KEY); + return target.forEach((value, key) => { + return callback.call(thisArg, wrap(value), wrap(key), observed); + }); + }; +} +function createIterableMethod(method, isReadonly2, isShallow3) { + return function(...args) { + const target = this["__v_raw"]; + const rawTarget = toRaw(target); + const targetIsMap = isMap(rawTarget); + const isPair = method === "entries" || method === Symbol.iterator && targetIsMap; + const isKeyOnly = method === "keys" && targetIsMap; + const innerIterator = target[method](...args); + const wrap = isShallow3 ? toShallow : isReadonly2 ? toReadonly : toReactive; + !isReadonly2 && track( + rawTarget, + "iterate", + isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY + ); + return { + // iterator protocol + next() { + const { value, done } = innerIterator.next(); + return done ? { value, done } : { + value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value), + done + }; + }, + // iterable protocol + [Symbol.iterator]() { + return this; + } + }; + }; +} +function createReadonlyMethod(type) { + return function(...args) { + if (true) { + const key = args[0] ? `on key "${args[0]}" ` : ``; + console.warn( + `${capitalize(type)} operation ${key}failed: target is readonly.`, + toRaw(this) + ); + } + return type === "delete" ? false : type === "clear" ? void 0 : this; + }; +} +function createInstrumentations() { + const mutableInstrumentations2 = { + get(key) { + return get(this, key); + }, + get size() { + return size(this); + }, + has, + add, + set, + delete: deleteEntry, + clear, + forEach: createForEach(false, false) + }; + const shallowInstrumentations2 = { + get(key) { + return get(this, key, false, true); + }, + get size() { + return size(this); + }, + has, + add, + set, + delete: deleteEntry, + clear, + forEach: createForEach(false, true) + }; + const readonlyInstrumentations2 = { + get(key) { + return get(this, key, true); + }, + get size() { + return size(this, true); + }, + has(key) { + return has.call(this, key, true); + }, + add: createReadonlyMethod("add"), + set: createReadonlyMethod("set"), + delete: createReadonlyMethod("delete"), + clear: createReadonlyMethod("clear"), + forEach: createForEach(true, false) + }; + const shallowReadonlyInstrumentations2 = { + get(key) { + return get(this, key, true, true); + }, + get size() { + return size(this, true); + }, + has(key) { + return has.call(this, key, true); + }, + add: createReadonlyMethod("add"), + set: createReadonlyMethod("set"), + delete: createReadonlyMethod("delete"), + clear: createReadonlyMethod("clear"), + forEach: createForEach(true, true) + }; + const iteratorMethods = ["keys", "values", "entries", Symbol.iterator]; + iteratorMethods.forEach((method) => { + mutableInstrumentations2[method] = createIterableMethod( + method, + false, + false + ); + readonlyInstrumentations2[method] = createIterableMethod( + method, + true, + false + ); + shallowInstrumentations2[method] = createIterableMethod( + method, + false, + true + ); + shallowReadonlyInstrumentations2[method] = createIterableMethod( + method, + true, + true + ); + }); + return [ + mutableInstrumentations2, + readonlyInstrumentations2, + shallowInstrumentations2, + shallowReadonlyInstrumentations2 + ]; +} +var [ + mutableInstrumentations, + readonlyInstrumentations, + shallowInstrumentations, + shallowReadonlyInstrumentations +] = createInstrumentations(); +function createInstrumentationGetter(isReadonly2, shallow) { + const instrumentations = shallow ? isReadonly2 ? shallowReadonlyInstrumentations : shallowInstrumentations : isReadonly2 ? readonlyInstrumentations : mutableInstrumentations; + return (target, key, receiver) => { + if (key === "__v_isReactive") { + return !isReadonly2; + } else if (key === "__v_isReadonly") { + return isReadonly2; + } else if (key === "__v_raw") { + return target; + } + return Reflect.get( + hasOwn(instrumentations, key) && key in target ? instrumentations : target, + key, + receiver + ); + }; +} +var mutableCollectionHandlers = { + get: createInstrumentationGetter(false, false) +}; +var shallowCollectionHandlers = { + get: createInstrumentationGetter(false, true) +}; +var readonlyCollectionHandlers = { + get: createInstrumentationGetter(true, false) +}; +var shallowReadonlyCollectionHandlers = { + get: createInstrumentationGetter(true, true) +}; +function checkIdentityKeys(target, has2, key) { + const rawKey = toRaw(key); + if (rawKey !== key && has2.call(target, rawKey)) { + const type = toRawType(target); + console.warn( + `Reactive ${type} contains both the raw and reactive versions of the same object${type === `Map` ? ` as keys` : ``}, which can lead to inconsistencies. Avoid differentiating between the raw and reactive versions of an object and only use the reactive version if possible.` + ); + } +} +var reactiveMap = /* @__PURE__ */ new WeakMap(); +var shallowReactiveMap = /* @__PURE__ */ new WeakMap(); +var readonlyMap = /* @__PURE__ */ new WeakMap(); +var shallowReadonlyMap = /* @__PURE__ */ new WeakMap(); +function targetTypeMap(rawType) { + switch (rawType) { + case "Object": + case "Array": + return 1; + case "Map": + case "Set": + case "WeakMap": + case "WeakSet": + return 2; + default: + return 0; + } +} +function getTargetType(value) { + return value["__v_skip"] || !Object.isExtensible(value) ? 0 : targetTypeMap(toRawType(value)); +} +function reactive(target) { + if (isReadonly(target)) { + return target; + } + return createReactiveObject( + target, + false, + mutableHandlers, + mutableCollectionHandlers, + reactiveMap + ); +} +function shallowReactive(target) { + return createReactiveObject( + target, + false, + shallowReactiveHandlers, + shallowCollectionHandlers, + shallowReactiveMap + ); +} +function readonly(target) { + return createReactiveObject( + target, + true, + readonlyHandlers, + readonlyCollectionHandlers, + readonlyMap + ); +} +function shallowReadonly(target) { + return createReactiveObject( + target, + true, + shallowReadonlyHandlers, + shallowReadonlyCollectionHandlers, + shallowReadonlyMap + ); +} +function createReactiveObject(target, isReadonly2, baseHandlers, collectionHandlers, proxyMap) { + if (!isObject(target)) { + if (true) { + console.warn(`value cannot be made reactive: ${String(target)}`); + } + return target; + } + if (target["__v_raw"] && !(isReadonly2 && target["__v_isReactive"])) { + return target; + } + const existingProxy = proxyMap.get(target); + if (existingProxy) { + return existingProxy; + } + const targetType = getTargetType(target); + if (targetType === 0) { + return target; + } + const proxy = new Proxy( + target, + targetType === 2 ? collectionHandlers : baseHandlers + ); + proxyMap.set(target, proxy); + return proxy; +} +function isReactive(value) { + if (isReadonly(value)) { + return isReactive(value["__v_raw"]); + } + return !!(value && value["__v_isReactive"]); +} +function isReadonly(value) { + return !!(value && value["__v_isReadonly"]); +} +function isShallow(value) { + return !!(value && value["__v_isShallow"]); +} +function isProxy(value) { + return isReactive(value) || isReadonly(value); +} +function toRaw(observed) { + const raw = observed && observed["__v_raw"]; + return raw ? toRaw(raw) : observed; +} +function markRaw(value) { + def(value, "__v_skip", true); + return value; +} +var toReactive = (value) => isObject(value) ? reactive(value) : value; +var toReadonly = (value) => isObject(value) ? readonly(value) : value; +var ComputedRefImpl = class { + constructor(getter, _setter, isReadonly2, isSSR) { + this._setter = _setter; + this.dep = void 0; + this.__v_isRef = true; + this["__v_isReadonly"] = false; + this.effect = new ReactiveEffect( + () => getter(this._value), + () => triggerRefValue(this, 1) + ); + this.effect.computed = this; + this.effect.active = this._cacheable = !isSSR; + this["__v_isReadonly"] = isReadonly2; + } + get value() { + const self2 = toRaw(this); + trackRefValue(self2); + if (!self2._cacheable || self2.effect.dirty) { + if (hasChanged(self2._value, self2._value = self2.effect.run())) { + triggerRefValue(self2, 2); + } + } + return self2._value; + } + set value(newValue) { + this._setter(newValue); + } + // #region polyfill _dirty for backward compatibility third party code for Vue <= 3.3.x + get _dirty() { + return this.effect.dirty; + } + set _dirty(v) { + this.effect.dirty = v; + } + // #endregion +}; +function computed(getterOrOptions, debugOptions, isSSR = false) { + let getter; + let setter; + const onlyGetter = isFunction(getterOrOptions); + if (onlyGetter) { + getter = getterOrOptions; + setter = true ? () => { + console.warn("Write operation failed: computed value is readonly"); + } : NOOP; + } else { + getter = getterOrOptions.get; + setter = getterOrOptions.set; + } + const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR); + if (debugOptions && !isSSR) { + cRef.effect.onTrack = debugOptions.onTrack; + cRef.effect.onTrigger = debugOptions.onTrigger; + } + return cRef; +} +function trackRefValue(ref2) { + if (shouldTrack && activeEffect) { + ref2 = toRaw(ref2); + trackEffect( + activeEffect, + ref2.dep || (ref2.dep = createDep( + () => ref2.dep = void 0, + ref2 instanceof ComputedRefImpl ? ref2 : void 0 + )), + true ? { + target: ref2, + type: "get", + key: "value" + } : void 0 + ); + } +} +function triggerRefValue(ref2, dirtyLevel = 3, newVal) { + ref2 = toRaw(ref2); + const dep = ref2.dep; + if (dep) { + triggerEffects( + dep, + dirtyLevel, + true ? { + target: ref2, + type: "set", + key: "value", + newValue: newVal + } : void 0 + ); + } +} +function isRef(r) { + return !!(r && r.__v_isRef === true); +} +function ref(value) { + return createRef(value, false); +} +function shallowRef(value) { + return createRef(value, true); +} +function createRef(rawValue, shallow) { + if (isRef(rawValue)) { + return rawValue; + } + return new RefImpl(rawValue, shallow); +} +var RefImpl = class { + constructor(value, __v_isShallow) { + this.__v_isShallow = __v_isShallow; + this.dep = void 0; + this.__v_isRef = true; + this._rawValue = __v_isShallow ? value : toRaw(value); + this._value = __v_isShallow ? value : toReactive(value); + } + get value() { + trackRefValue(this); + return this._value; + } + set value(newVal) { + const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal); + newVal = useDirectValue ? newVal : toRaw(newVal); + if (hasChanged(newVal, this._rawValue)) { + this._rawValue = newVal; + this._value = useDirectValue ? newVal : toReactive(newVal); + triggerRefValue(this, 3, newVal); + } + } +}; +function triggerRef(ref2) { + triggerRefValue(ref2, 3, true ? ref2.value : void 0); +} +function unref(ref2) { + return isRef(ref2) ? ref2.value : ref2; +} +function toValue(source) { + return isFunction(source) ? source() : unref(source); +} +var shallowUnwrapHandlers = { + get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)), + set: (target, key, value, receiver) => { + const oldValue = target[key]; + if (isRef(oldValue) && !isRef(value)) { + oldValue.value = value; + return true; + } else { + return Reflect.set(target, key, value, receiver); + } + } +}; +function proxyRefs(objectWithRefs) { + return isReactive(objectWithRefs) ? objectWithRefs : new Proxy(objectWithRefs, shallowUnwrapHandlers); +} +var CustomRefImpl = class { + constructor(factory) { + this.dep = void 0; + this.__v_isRef = true; + const { get: get2, set: set2 } = factory( + () => trackRefValue(this), + () => triggerRefValue(this) + ); + this._get = get2; + this._set = set2; + } + get value() { + return this._get(); + } + set value(newVal) { + this._set(newVal); + } +}; +function customRef(factory) { + return new CustomRefImpl(factory); +} +function toRefs(object) { + if (!isProxy(object)) { + console.warn(`toRefs() expects a reactive object but received a plain one.`); + } + const ret = isArray(object) ? new Array(object.length) : {}; + for (const key in object) { + ret[key] = propertyToRef(object, key); + } + return ret; +} +var ObjectRefImpl = class { + constructor(_object, _key, _defaultValue) { + this._object = _object; + this._key = _key; + this._defaultValue = _defaultValue; + this.__v_isRef = true; + } + get value() { + const val = this._object[this._key]; + return val === void 0 ? this._defaultValue : val; + } + set value(newVal) { + this._object[this._key] = newVal; + } + get dep() { + return getDepFromReactive(toRaw(this._object), this._key); + } +}; +var GetterRefImpl = class { + constructor(_getter) { + this._getter = _getter; + this.__v_isRef = true; + this.__v_isReadonly = true; + } + get value() { + return this._getter(); + } +}; +function toRef(source, key, defaultValue) { + if (isRef(source)) { + return source; + } else if (isFunction(source)) { + return new GetterRefImpl(source); + } else if (isObject(source) && arguments.length > 1) { + return propertyToRef(source, key, defaultValue); + } else { + return ref(source); + } +} +function propertyToRef(source, key, defaultValue) { + const val = source[key]; + return isRef(val) ? val : new ObjectRefImpl(source, key, defaultValue); +} +var TrackOpTypes = { + "GET": "get", + "HAS": "has", + "ITERATE": "iterate" +}; +var TriggerOpTypes = { + "SET": "set", + "ADD": "add", + "DELETE": "delete", + "CLEAR": "clear" +}; + +// node_modules/.pnpm/@vue+runtime-core@3.4.3/node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js +var stack = []; +function pushWarningContext(vnode) { + stack.push(vnode); +} +function popWarningContext() { + stack.pop(); +} +function warn$1(msg, ...args) { + pauseTracking(); + const instance = stack.length ? stack[stack.length - 1].component : null; + const appWarnHandler = instance && instance.appContext.config.warnHandler; + const trace = getComponentTrace(); + if (appWarnHandler) { + callWithErrorHandling( + appWarnHandler, + instance, + 11, + [ + msg + args.join(""), + instance && instance.proxy, + trace.map( + ({ vnode }) => `at <${formatComponentName(instance, vnode.type)}>` + ).join("\n"), + trace + ] + ); + } else { + const warnArgs = [`[Vue warn]: ${msg}`, ...args]; + if (trace.length && // avoid spamming console during tests + true) { + warnArgs.push(` +`, ...formatTrace(trace)); + } + console.warn(...warnArgs); + } + resetTracking(); +} +function getComponentTrace() { + let currentVNode = stack[stack.length - 1]; + if (!currentVNode) { + return []; + } + const normalizedStack = []; + while (currentVNode) { + const last = normalizedStack[0]; + if (last && last.vnode === currentVNode) { + last.recurseCount++; + } else { + normalizedStack.push({ + vnode: currentVNode, + recurseCount: 0 + }); + } + const parentInstance = currentVNode.component && currentVNode.component.parent; + currentVNode = parentInstance && parentInstance.vnode; + } + return normalizedStack; +} +function formatTrace(trace) { + const logs = []; + trace.forEach((entry, i) => { + logs.push(...i === 0 ? [] : [` +`], ...formatTraceEntry(entry)); + }); + return logs; +} +function formatTraceEntry({ vnode, recurseCount }) { + const postfix = recurseCount > 0 ? `... (${recurseCount} recursive calls)` : ``; + const isRoot = vnode.component ? vnode.component.parent == null : false; + const open = ` at <${formatComponentName( + vnode.component, + vnode.type, + isRoot + )}`; + const close = `>` + postfix; + return vnode.props ? [open, ...formatProps(vnode.props), close] : [open + close]; +} +function formatProps(props) { + const res = []; + const keys = Object.keys(props); + keys.slice(0, 3).forEach((key) => { + res.push(...formatProp(key, props[key])); + }); + if (keys.length > 3) { + res.push(` ...`); + } + return res; +} +function formatProp(key, value, raw) { + if (isString(value)) { + value = JSON.stringify(value); + return raw ? value : [`${key}=${value}`]; + } else if (typeof value === "number" || typeof value === "boolean" || value == null) { + return raw ? value : [`${key}=${value}`]; + } else if (isRef(value)) { + value = formatProp(key, toRaw(value.value), true); + return raw ? value : [`${key}=Ref<`, value, `>`]; + } else if (isFunction(value)) { + return [`${key}=fn${value.name ? `<${value.name}>` : ``}`]; + } else { + value = toRaw(value); + return raw ? value : [`${key}=`, value]; + } +} +function assertNumber(val, type) { + if (false) + return; + if (val === void 0) { + return; + } else if (typeof val !== "number") { + warn$1(`${type} is not a valid number - got ${JSON.stringify(val)}.`); + } else if (isNaN(val)) { + warn$1(`${type} is NaN - the duration expression might be incorrect.`); + } +} +var ErrorCodes = { + "SETUP_FUNCTION": 0, + "0": "SETUP_FUNCTION", + "RENDER_FUNCTION": 1, + "1": "RENDER_FUNCTION", + "WATCH_GETTER": 2, + "2": "WATCH_GETTER", + "WATCH_CALLBACK": 3, + "3": "WATCH_CALLBACK", + "WATCH_CLEANUP": 4, + "4": "WATCH_CLEANUP", + "NATIVE_EVENT_HANDLER": 5, + "5": "NATIVE_EVENT_HANDLER", + "COMPONENT_EVENT_HANDLER": 6, + "6": "COMPONENT_EVENT_HANDLER", + "VNODE_HOOK": 7, + "7": "VNODE_HOOK", + "DIRECTIVE_HOOK": 8, + "8": "DIRECTIVE_HOOK", + "TRANSITION_HOOK": 9, + "9": "TRANSITION_HOOK", + "APP_ERROR_HANDLER": 10, + "10": "APP_ERROR_HANDLER", + "APP_WARN_HANDLER": 11, + "11": "APP_WARN_HANDLER", + "FUNCTION_REF": 12, + "12": "FUNCTION_REF", + "ASYNC_COMPONENT_LOADER": 13, + "13": "ASYNC_COMPONENT_LOADER", + "SCHEDULER": 14, + "14": "SCHEDULER" +}; +var ErrorTypeStrings$1 = { + ["sp"]: "serverPrefetch hook", + ["bc"]: "beforeCreate hook", + ["c"]: "created hook", + ["bm"]: "beforeMount hook", + ["m"]: "mounted hook", + ["bu"]: "beforeUpdate hook", + ["u"]: "updated", + ["bum"]: "beforeUnmount hook", + ["um"]: "unmounted hook", + ["a"]: "activated hook", + ["da"]: "deactivated hook", + ["ec"]: "errorCaptured hook", + ["rtc"]: "renderTracked hook", + ["rtg"]: "renderTriggered hook", + [0]: "setup function", + [1]: "render function", + [2]: "watcher getter", + [3]: "watcher callback", + [4]: "watcher cleanup function", + [5]: "native event handler", + [6]: "component event handler", + [7]: "vnode hook", + [8]: "directive hook", + [9]: "transition hook", + [10]: "app errorHandler", + [11]: "app warnHandler", + [12]: "ref function", + [13]: "async component loader", + [14]: "scheduler flush. This is likely a Vue internals bug. Please open an issue at https://github.com/vuejs/core ." +}; +function callWithErrorHandling(fn, instance, type, args) { + let res; + try { + res = args ? fn(...args) : fn(); + } catch (err) { + handleError(err, instance, type); + } + return res; +} +function callWithAsyncErrorHandling(fn, instance, type, args) { + if (isFunction(fn)) { + const res = callWithErrorHandling(fn, instance, type, args); + if (res && isPromise(res)) { + res.catch((err) => { + handleError(err, instance, type); + }); + } + return res; + } + const values = []; + for (let i = 0; i < fn.length; i++) { + values.push(callWithAsyncErrorHandling(fn[i], instance, type, args)); + } + return values; +} +function handleError(err, instance, type, throwInDev = true) { + const contextVNode = instance ? instance.vnode : null; + if (instance) { + let cur = instance.parent; + const exposedInstance = instance.proxy; + const errorInfo = true ? ErrorTypeStrings$1[type] : `https://vuejs.org/errors/#runtime-${type}`; + while (cur) { + const errorCapturedHooks = cur.ec; + if (errorCapturedHooks) { + for (let i = 0; i < errorCapturedHooks.length; i++) { + if (errorCapturedHooks[i](err, exposedInstance, errorInfo) === false) { + return; + } + } + } + cur = cur.parent; + } + const appErrorHandler = instance.appContext.config.errorHandler; + if (appErrorHandler) { + callWithErrorHandling( + appErrorHandler, + null, + 10, + [err, exposedInstance, errorInfo] + ); + return; + } + } + logError(err, type, contextVNode, throwInDev); +} +function logError(err, type, contextVNode, throwInDev = true) { + if (true) { + const info = ErrorTypeStrings$1[type]; + if (contextVNode) { + pushWarningContext(contextVNode); + } + warn$1(`Unhandled error${info ? ` during execution of ${info}` : ``}`); + if (contextVNode) { + popWarningContext(); + } + if (throwInDev) { + throw err; + } else { + console.error(err); + } + } else { + console.error(err); + } +} +var isFlushing = false; +var isFlushPending = false; +var queue = []; +var flushIndex = 0; +var pendingPostFlushCbs = []; +var activePostFlushCbs = null; +var postFlushIndex = 0; +var resolvedPromise = Promise.resolve(); +var currentFlushPromise = null; +var RECURSION_LIMIT = 100; +function nextTick(fn) { + const p2 = currentFlushPromise || resolvedPromise; + return fn ? p2.then(this ? fn.bind(this) : fn) : p2; +} +function findInsertionIndex(id) { + let start = flushIndex + 1; + let end = queue.length; + while (start < end) { + const middle = start + end >>> 1; + const middleJob = queue[middle]; + const middleJobId = getId(middleJob); + if (middleJobId < id || middleJobId === id && middleJob.pre) { + start = middle + 1; + } else { + end = middle; + } + } + return start; +} +function queueJob(job) { + if (!queue.length || !queue.includes( + job, + isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex + )) { + if (job.id == null) { + queue.push(job); + } else { + queue.splice(findInsertionIndex(job.id), 0, job); + } + queueFlush(); + } +} +function queueFlush() { + if (!isFlushing && !isFlushPending) { + isFlushPending = true; + currentFlushPromise = resolvedPromise.then(flushJobs); + } +} +function invalidateJob(job) { + const i = queue.indexOf(job); + if (i > flushIndex) { + queue.splice(i, 1); + } +} +function queuePostFlushCb(cb) { + if (!isArray(cb)) { + if (!activePostFlushCbs || !activePostFlushCbs.includes( + cb, + cb.allowRecurse ? postFlushIndex + 1 : postFlushIndex + )) { + pendingPostFlushCbs.push(cb); + } + } else { + pendingPostFlushCbs.push(...cb); + } + queueFlush(); +} +function flushPreFlushCbs(instance, seen, i = isFlushing ? flushIndex + 1 : 0) { + if (true) { + seen = seen || /* @__PURE__ */ new Map(); + } + for (; i < queue.length; i++) { + const cb = queue[i]; + if (cb && cb.pre) { + if (instance && cb.id !== instance.uid) { + continue; + } + if (checkRecursiveUpdates(seen, cb)) { + continue; + } + queue.splice(i, 1); + i--; + cb(); + } + } +} +function flushPostFlushCbs(seen) { + if (pendingPostFlushCbs.length) { + const deduped = [...new Set(pendingPostFlushCbs)]; + pendingPostFlushCbs.length = 0; + if (activePostFlushCbs) { + activePostFlushCbs.push(...deduped); + return; + } + activePostFlushCbs = deduped; + if (true) { + seen = seen || /* @__PURE__ */ new Map(); + } + activePostFlushCbs.sort((a, b) => getId(a) - getId(b)); + for (postFlushIndex = 0; postFlushIndex < activePostFlushCbs.length; postFlushIndex++) { + if (checkRecursiveUpdates(seen, activePostFlushCbs[postFlushIndex])) { + continue; + } + activePostFlushCbs[postFlushIndex](); + } + activePostFlushCbs = null; + postFlushIndex = 0; + } +} +var getId = (job) => job.id == null ? Infinity : job.id; +var comparator = (a, b) => { + const diff = getId(a) - getId(b); + if (diff === 0) { + if (a.pre && !b.pre) + return -1; + if (b.pre && !a.pre) + return 1; + } + return diff; +}; +function flushJobs(seen) { + isFlushPending = false; + isFlushing = true; + if (true) { + seen = seen || /* @__PURE__ */ new Map(); + } + queue.sort(comparator); + const check = true ? (job) => checkRecursiveUpdates(seen, job) : NOOP; + try { + for (flushIndex = 0; flushIndex < queue.length; flushIndex++) { + const job = queue[flushIndex]; + if (job && job.active !== false) { + if (check(job)) { + continue; + } + callWithErrorHandling(job, null, 14); + } + } + } finally { + flushIndex = 0; + queue.length = 0; + flushPostFlushCbs(seen); + isFlushing = false; + currentFlushPromise = null; + if (queue.length || pendingPostFlushCbs.length) { + flushJobs(seen); + } + } +} +function checkRecursiveUpdates(seen, fn) { + if (!seen.has(fn)) { + seen.set(fn, 1); + } else { + const count = seen.get(fn); + if (count > RECURSION_LIMIT) { + const instance = fn.ownerInstance; + const componentName = instance && getComponentName(instance.type); + handleError( + `Maximum recursive updates exceeded${componentName ? ` in component <${componentName}>` : ``}. This means you have a reactive effect that is mutating its own dependencies and thus recursively triggering itself. Possible sources include component template, render function, updated hook or watcher source function.`, + null, + 10 + ); + return true; + } else { + seen.set(fn, count + 1); + } + } +} +var isHmrUpdating = false; +var hmrDirtyComponents = /* @__PURE__ */ new Set(); +if (true) { + getGlobalThis().__VUE_HMR_RUNTIME__ = { + createRecord: tryWrap(createRecord), + rerender: tryWrap(rerender), + reload: tryWrap(reload) + }; +} +var map = /* @__PURE__ */ new Map(); +function registerHMR(instance) { + const id = instance.type.__hmrId; + let record = map.get(id); + if (!record) { + createRecord(id, instance.type); + record = map.get(id); + } + record.instances.add(instance); +} +function unregisterHMR(instance) { + map.get(instance.type.__hmrId).instances.delete(instance); +} +function createRecord(id, initialDef) { + if (map.has(id)) { + return false; + } + map.set(id, { + initialDef: normalizeClassComponent(initialDef), + instances: /* @__PURE__ */ new Set() + }); + return true; +} +function normalizeClassComponent(component) { + return isClassComponent(component) ? component.__vccOpts : component; +} +function rerender(id, newRender) { + const record = map.get(id); + if (!record) { + return; + } + record.initialDef.render = newRender; + [...record.instances].forEach((instance) => { + if (newRender) { + instance.render = newRender; + normalizeClassComponent(instance.type).render = newRender; + } + instance.renderCache = []; + isHmrUpdating = true; + instance.effect.dirty = true; + instance.update(); + isHmrUpdating = false; + }); +} +function reload(id, newComp) { + const record = map.get(id); + if (!record) + return; + newComp = normalizeClassComponent(newComp); + updateComponentDef(record.initialDef, newComp); + const instances = [...record.instances]; + for (const instance of instances) { + const oldComp = normalizeClassComponent(instance.type); + if (!hmrDirtyComponents.has(oldComp)) { + if (oldComp !== record.initialDef) { + updateComponentDef(oldComp, newComp); + } + hmrDirtyComponents.add(oldComp); + } + instance.appContext.propsCache.delete(instance.type); + instance.appContext.emitsCache.delete(instance.type); + instance.appContext.optionsCache.delete(instance.type); + if (instance.ceReload) { + hmrDirtyComponents.add(oldComp); + instance.ceReload(newComp.styles); + hmrDirtyComponents.delete(oldComp); + } else if (instance.parent) { + instance.parent.effect.dirty = true; + queueJob(instance.parent.update); + } else if (instance.appContext.reload) { + instance.appContext.reload(); + } else if (typeof window !== "undefined") { + window.location.reload(); + } else { + console.warn( + "[HMR] Root or manually mounted instance modified. Full reload required." + ); + } + } + queuePostFlushCb(() => { + for (const instance of instances) { + hmrDirtyComponents.delete( + normalizeClassComponent(instance.type) + ); + } + }); +} +function updateComponentDef(oldComp, newComp) { + extend(oldComp, newComp); + for (const key in oldComp) { + if (key !== "__file" && !(key in newComp)) { + delete oldComp[key]; + } + } +} +function tryWrap(fn) { + return (id, arg) => { + try { + return fn(id, arg); + } catch (e) { + console.error(e); + console.warn( + `[HMR] Something went wrong during Vue component hot-reload. Full reload required.` + ); + } + }; +} +var devtools$1; +var buffer = []; +var devtoolsNotInstalled = false; +function emit$1(event, ...args) { + if (devtools$1) { + devtools$1.emit(event, ...args); + } else if (!devtoolsNotInstalled) { + buffer.push({ event, args }); + } +} +function setDevtoolsHook$1(hook, target) { + var _a, _b; + devtools$1 = hook; + if (devtools$1) { + devtools$1.enabled = true; + buffer.forEach(({ event, args }) => devtools$1.emit(event, ...args)); + buffer = []; + } else if ( + // handle late devtools injection - only do this if we are in an actual + // browser environment to avoid the timer handle stalling test runner exit + // (#4815) + typeof window !== "undefined" && // some envs mock window but not fully + window.HTMLElement && // also exclude jsdom + !((_b = (_a = window.navigator) == null ? void 0 : _a.userAgent) == null ? void 0 : _b.includes("jsdom")) + ) { + const replay = target.__VUE_DEVTOOLS_HOOK_REPLAY__ = target.__VUE_DEVTOOLS_HOOK_REPLAY__ || []; + replay.push((newHook) => { + setDevtoolsHook$1(newHook, target); + }); + setTimeout(() => { + if (!devtools$1) { + target.__VUE_DEVTOOLS_HOOK_REPLAY__ = null; + devtoolsNotInstalled = true; + buffer = []; + } + }, 3e3); + } else { + devtoolsNotInstalled = true; + buffer = []; + } +} +function devtoolsInitApp(app, version2) { + emit$1("app:init", app, version2, { + Fragment, + Text, + Comment, + Static + }); +} +function devtoolsUnmountApp(app) { + emit$1("app:unmount", app); +} +var devtoolsComponentAdded = createDevtoolsComponentHook( + "component:added" + /* COMPONENT_ADDED */ +); +var devtoolsComponentUpdated = createDevtoolsComponentHook( + "component:updated" + /* COMPONENT_UPDATED */ +); +var _devtoolsComponentRemoved = createDevtoolsComponentHook( + "component:removed" + /* COMPONENT_REMOVED */ +); +var devtoolsComponentRemoved = (component) => { + if (devtools$1 && typeof devtools$1.cleanupBuffer === "function" && // remove the component if it wasn't buffered + !devtools$1.cleanupBuffer(component)) { + _devtoolsComponentRemoved(component); + } +}; +function createDevtoolsComponentHook(hook) { + return (component) => { + emit$1( + hook, + component.appContext.app, + component.uid, + component.parent ? component.parent.uid : void 0, + component + ); + }; +} +var devtoolsPerfStart = createDevtoolsPerformanceHook( + "perf:start" + /* PERFORMANCE_START */ +); +var devtoolsPerfEnd = createDevtoolsPerformanceHook( + "perf:end" + /* PERFORMANCE_END */ +); +function createDevtoolsPerformanceHook(hook) { + return (component, type, time) => { + emit$1(hook, component.appContext.app, component.uid, component, type, time); + }; +} +function devtoolsComponentEmit(component, event, params) { + emit$1( + "component:emit", + component.appContext.app, + component, + event, + params + ); +} +function emit(instance, event, ...rawArgs) { + if (instance.isUnmounted) + return; + const props = instance.vnode.props || EMPTY_OBJ; + if (true) { + const { + emitsOptions, + propsOptions: [propsOptions] + } = instance; + if (emitsOptions) { + if (!(event in emitsOptions) && true) { + if (!propsOptions || !(toHandlerKey(event) in propsOptions)) { + warn$1( + `Component emitted event "${event}" but it is neither declared in the emits option nor as an "${toHandlerKey(event)}" prop.` + ); + } + } else { + const validator = emitsOptions[event]; + if (isFunction(validator)) { + const isValid = validator(...rawArgs); + if (!isValid) { + warn$1( + `Invalid event arguments: event validation failed for event "${event}".` + ); + } + } + } + } + } + let args = rawArgs; + const isModelListener2 = event.startsWith("update:"); + const modelArg = isModelListener2 && event.slice(7); + if (modelArg && modelArg in props) { + const modifiersKey = `${modelArg === "modelValue" ? "model" : modelArg}Modifiers`; + const { number, trim } = props[modifiersKey] || EMPTY_OBJ; + if (trim) { + args = rawArgs.map((a) => isString(a) ? a.trim() : a); + } + if (number) { + args = rawArgs.map(looseToNumber); + } + } + if (true) { + devtoolsComponentEmit(instance, event, args); + } + if (true) { + const lowerCaseEvent = event.toLowerCase(); + if (lowerCaseEvent !== event && props[toHandlerKey(lowerCaseEvent)]) { + warn$1( + `Event "${lowerCaseEvent}" is emitted in component ${formatComponentName( + instance, + instance.type + )} but the handler is registered for "${event}". Note that HTML attributes are case-insensitive and you cannot use v-on to listen to camelCase events when using in-DOM templates. You should probably use "${hyphenate( + event + )}" instead of "${event}".` + ); + } + } + let handlerName; + let handler = props[handlerName = toHandlerKey(event)] || // also try camelCase event handler (#2249) + props[handlerName = toHandlerKey(camelize(event))]; + if (!handler && isModelListener2) { + handler = props[handlerName = toHandlerKey(hyphenate(event))]; + } + if (handler) { + callWithAsyncErrorHandling( + handler, + instance, + 6, + args + ); + } + const onceHandler = props[handlerName + `Once`]; + if (onceHandler) { + if (!instance.emitted) { + instance.emitted = {}; + } else if (instance.emitted[handlerName]) { + return; + } + instance.emitted[handlerName] = true; + callWithAsyncErrorHandling( + onceHandler, + instance, + 6, + args + ); + } +} +function normalizeEmitsOptions(comp, appContext, asMixin = false) { + const cache = appContext.emitsCache; + const cached = cache.get(comp); + if (cached !== void 0) { + return cached; + } + const raw = comp.emits; + let normalized = {}; + let hasExtends = false; + if (__VUE_OPTIONS_API__ && !isFunction(comp)) { + const extendEmits = (raw2) => { + const normalizedFromExtend = normalizeEmitsOptions(raw2, appContext, true); + if (normalizedFromExtend) { + hasExtends = true; + extend(normalized, normalizedFromExtend); + } + }; + if (!asMixin && appContext.mixins.length) { + appContext.mixins.forEach(extendEmits); + } + if (comp.extends) { + extendEmits(comp.extends); + } + if (comp.mixins) { + comp.mixins.forEach(extendEmits); + } + } + if (!raw && !hasExtends) { + if (isObject(comp)) { + cache.set(comp, null); + } + return null; + } + if (isArray(raw)) { + raw.forEach((key) => normalized[key] = null); + } else { + extend(normalized, raw); + } + if (isObject(comp)) { + cache.set(comp, normalized); + } + return normalized; +} +function isEmitListener(options, key) { + if (!options || !isOn(key)) { + return false; + } + key = key.slice(2).replace(/Once$/, ""); + return hasOwn(options, key[0].toLowerCase() + key.slice(1)) || hasOwn(options, hyphenate(key)) || hasOwn(options, key); +} +var currentRenderingInstance = null; +var currentScopeId = null; +function setCurrentRenderingInstance(instance) { + const prev = currentRenderingInstance; + currentRenderingInstance = instance; + currentScopeId = instance && instance.type.__scopeId || null; + return prev; +} +function pushScopeId(id) { + currentScopeId = id; +} +function popScopeId() { + currentScopeId = null; +} +var withScopeId = (_id) => withCtx; +function withCtx(fn, ctx = currentRenderingInstance, isNonScopedSlot) { + if (!ctx) + return fn; + if (fn._n) { + return fn; + } + const renderFnWithContext = (...args) => { + if (renderFnWithContext._d) { + setBlockTracking(-1); + } + const prevInstance = setCurrentRenderingInstance(ctx); + let res; + try { + res = fn(...args); + } finally { + setCurrentRenderingInstance(prevInstance); + if (renderFnWithContext._d) { + setBlockTracking(1); + } + } + if (true) { + devtoolsComponentUpdated(ctx); + } + return res; + }; + renderFnWithContext._n = true; + renderFnWithContext._c = true; + renderFnWithContext._d = true; + return renderFnWithContext; +} +var accessedAttrs = false; +function markAttrsAccessed() { + accessedAttrs = true; +} +function renderComponentRoot(instance) { + const { + type: Component, + vnode, + proxy, + withProxy, + props, + propsOptions: [propsOptions], + slots, + attrs, + emit: emit2, + render: render2, + renderCache, + data, + setupState, + ctx, + inheritAttrs + } = instance; + let result; + let fallthroughAttrs; + const prev = setCurrentRenderingInstance(instance); + if (true) { + accessedAttrs = false; + } + try { + if (vnode.shapeFlag & 4) { + const proxyToUse = withProxy || proxy; + const thisProxy = setupState.__isScriptSetup ? new Proxy(proxyToUse, { + get(target, key, receiver) { + warn$1( + `Property '${String( + key + )}' was accessed via 'this'. Avoid using 'this' in templates.` + ); + return Reflect.get(target, key, receiver); + } + }) : proxyToUse; + result = normalizeVNode( + render2.call( + thisProxy, + proxyToUse, + renderCache, + props, + setupState, + data, + ctx + ) + ); + fallthroughAttrs = attrs; + } else { + const render22 = Component; + if (attrs === props) { + markAttrsAccessed(); + } + result = normalizeVNode( + render22.length > 1 ? render22( + props, + true ? { + get attrs() { + markAttrsAccessed(); + return attrs; + }, + slots, + emit: emit2 + } : { attrs, slots, emit: emit2 } + ) : render22( + props, + null + /* we know it doesn't need it */ + ) + ); + fallthroughAttrs = Component.props ? attrs : getFunctionalFallthrough(attrs); + } + } catch (err) { + blockStack.length = 0; + handleError(err, instance, 1); + result = createVNode(Comment); + } + let root = result; + let setRoot = void 0; + if (result.patchFlag > 0 && result.patchFlag & 2048) { + [root, setRoot] = getChildRoot(result); + } + if (fallthroughAttrs && inheritAttrs !== false) { + const keys = Object.keys(fallthroughAttrs); + const { shapeFlag } = root; + if (keys.length) { + if (shapeFlag & (1 | 6)) { + if (propsOptions && keys.some(isModelListener)) { + fallthroughAttrs = filterModelListeners( + fallthroughAttrs, + propsOptions + ); + } + root = cloneVNode(root, fallthroughAttrs); + } else if (!accessedAttrs && root.type !== Comment) { + const allAttrs = Object.keys(attrs); + const eventAttrs = []; + const extraAttrs = []; + for (let i = 0, l = allAttrs.length; i < l; i++) { + const key = allAttrs[i]; + if (isOn(key)) { + if (!isModelListener(key)) { + eventAttrs.push(key[2].toLowerCase() + key.slice(3)); + } + } else { + extraAttrs.push(key); + } + } + if (extraAttrs.length) { + warn$1( + `Extraneous non-props attributes (${extraAttrs.join(", ")}) were passed to component but could not be automatically inherited because component renders fragment or text root nodes.` + ); + } + if (eventAttrs.length) { + warn$1( + `Extraneous non-emits event listeners (${eventAttrs.join(", ")}) were passed to component but could not be automatically inherited because component renders fragment or text root nodes. If the listener is intended to be a component custom event listener only, declare it using the "emits" option.` + ); + } + } + } + } + if (vnode.dirs) { + if (!isElementRoot(root)) { + warn$1( + `Runtime directive used on component with non-element root node. The directives will not function as intended.` + ); + } + root = cloneVNode(root); + root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs; + } + if (vnode.transition) { + if (!isElementRoot(root)) { + warn$1( + `Component inside renders non-element root node that cannot be animated.` + ); + } + root.transition = vnode.transition; + } + if (setRoot) { + setRoot(root); + } else { + result = root; + } + setCurrentRenderingInstance(prev); + return result; +} +var getChildRoot = (vnode) => { + const rawChildren = vnode.children; + const dynamicChildren = vnode.dynamicChildren; + const childRoot = filterSingleRoot(rawChildren); + if (!childRoot) { + return [vnode, void 0]; + } + const index = rawChildren.indexOf(childRoot); + const dynamicIndex = dynamicChildren ? dynamicChildren.indexOf(childRoot) : -1; + const setRoot = (updatedRoot) => { + rawChildren[index] = updatedRoot; + if (dynamicChildren) { + if (dynamicIndex > -1) { + dynamicChildren[dynamicIndex] = updatedRoot; + } else if (updatedRoot.patchFlag > 0) { + vnode.dynamicChildren = [...dynamicChildren, updatedRoot]; + } + } + }; + return [normalizeVNode(childRoot), setRoot]; +}; +function filterSingleRoot(children) { + let singleRoot; + for (let i = 0; i < children.length; i++) { + const child = children[i]; + if (isVNode(child)) { + if (child.type !== Comment || child.children === "v-if") { + if (singleRoot) { + return; + } else { + singleRoot = child; + } + } + } else { + return; + } + } + return singleRoot; +} +var getFunctionalFallthrough = (attrs) => { + let res; + for (const key in attrs) { + if (key === "class" || key === "style" || isOn(key)) { + (res || (res = {}))[key] = attrs[key]; + } + } + return res; +}; +var filterModelListeners = (attrs, props) => { + const res = {}; + for (const key in attrs) { + if (!isModelListener(key) || !(key.slice(9) in props)) { + res[key] = attrs[key]; + } + } + return res; +}; +var isElementRoot = (vnode) => { + return vnode.shapeFlag & (6 | 1) || vnode.type === Comment; +}; +function shouldUpdateComponent(prevVNode, nextVNode, optimized) { + const { props: prevProps, children: prevChildren, component } = prevVNode; + const { props: nextProps, children: nextChildren, patchFlag } = nextVNode; + const emits = component.emitsOptions; + if ((prevChildren || nextChildren) && isHmrUpdating) { + return true; + } + if (nextVNode.dirs || nextVNode.transition) { + return true; + } + if (optimized && patchFlag >= 0) { + if (patchFlag & 1024) { + return true; + } + if (patchFlag & 16) { + if (!prevProps) { + return !!nextProps; + } + return hasPropsChanged(prevProps, nextProps, emits); + } else if (patchFlag & 8) { + const dynamicProps = nextVNode.dynamicProps; + for (let i = 0; i < dynamicProps.length; i++) { + const key = dynamicProps[i]; + if (nextProps[key] !== prevProps[key] && !isEmitListener(emits, key)) { + return true; + } + } + } + } else { + if (prevChildren || nextChildren) { + if (!nextChildren || !nextChildren.$stable) { + return true; + } + } + if (prevProps === nextProps) { + return false; + } + if (!prevProps) { + return !!nextProps; + } + if (!nextProps) { + return true; + } + return hasPropsChanged(prevProps, nextProps, emits); + } + return false; +} +function hasPropsChanged(prevProps, nextProps, emitsOptions) { + const nextKeys = Object.keys(nextProps); + if (nextKeys.length !== Object.keys(prevProps).length) { + return true; + } + for (let i = 0; i < nextKeys.length; i++) { + const key = nextKeys[i]; + if (nextProps[key] !== prevProps[key] && !isEmitListener(emitsOptions, key)) { + return true; + } + } + return false; +} +function updateHOCHostEl({ vnode, parent }, el) { + if (!el) + return; + while (parent) { + const root = parent.subTree; + if (root.suspense && root.suspense.activeBranch === vnode) { + root.el = vnode.el; + } + if (root === vnode) { + (vnode = parent.vnode).el = el; + parent = parent.parent; + } else { + break; + } + } +} +var COMPONENTS = "components"; +var DIRECTIVES = "directives"; +function resolveComponent(name, maybeSelfReference) { + return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name; +} +var NULL_DYNAMIC_COMPONENT = Symbol.for("v-ndc"); +function resolveDynamicComponent(component) { + if (isString(component)) { + return resolveAsset(COMPONENTS, component, false) || component; + } else { + return component || NULL_DYNAMIC_COMPONENT; + } +} +function resolveDirective(name) { + return resolveAsset(DIRECTIVES, name); +} +function resolveAsset(type, name, warnMissing = true, maybeSelfReference = false) { + const instance = currentRenderingInstance || currentInstance; + if (instance) { + const Component = instance.type; + if (type === COMPONENTS) { + const selfName = getComponentName( + Component, + false + ); + if (selfName && (selfName === name || selfName === camelize(name) || selfName === capitalize(camelize(name)))) { + return Component; + } + } + const res = ( + // local registration + // check instance[type] first which is resolved for options API + resolve(instance[type] || Component[type], name) || // global registration + resolve(instance.appContext[type], name) + ); + if (!res && maybeSelfReference) { + return Component; + } + if (warnMissing && !res) { + const extra = type === COMPONENTS ? ` +If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement.` : ``; + warn$1(`Failed to resolve ${type.slice(0, -1)}: ${name}${extra}`); + } + return res; + } else if (true) { + warn$1( + `resolve${capitalize(type.slice(0, -1))} can only be used in render() or setup().` + ); + } +} +function resolve(registry, name) { + return registry && (registry[name] || registry[camelize(name)] || registry[capitalize(camelize(name))]); +} +var isSuspense = (type) => type.__isSuspense; +var suspenseId = 0; +var SuspenseImpl = { + name: "Suspense", + // In order to make Suspense tree-shakable, we need to avoid importing it + // directly in the renderer. The renderer checks for the __isSuspense flag + // on a vnode's type and calls the `process` method, passing in renderer + // internals. + __isSuspense: true, + process(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized, rendererInternals) { + if (n1 == null) { + mountSuspense( + n2, + container, + anchor, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + optimized, + rendererInternals + ); + } else { + patchSuspense( + n1, + n2, + container, + anchor, + parentComponent, + namespace, + slotScopeIds, + optimized, + rendererInternals + ); + } + }, + hydrate: hydrateSuspense, + create: createSuspenseBoundary, + normalize: normalizeSuspenseChildren +}; +var Suspense = SuspenseImpl; +function triggerEvent(vnode, name) { + const eventListener = vnode.props && vnode.props[name]; + if (isFunction(eventListener)) { + eventListener(); + } +} +function mountSuspense(vnode, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized, rendererInternals) { + const { + p: patch, + o: { createElement } + } = rendererInternals; + const hiddenContainer = createElement("div"); + const suspense = vnode.suspense = createSuspenseBoundary( + vnode, + parentSuspense, + parentComponent, + container, + hiddenContainer, + anchor, + namespace, + slotScopeIds, + optimized, + rendererInternals + ); + patch( + null, + suspense.pendingBranch = vnode.ssContent, + hiddenContainer, + null, + parentComponent, + suspense, + namespace, + slotScopeIds + ); + if (suspense.deps > 0) { + triggerEvent(vnode, "onPending"); + triggerEvent(vnode, "onFallback"); + patch( + null, + vnode.ssFallback, + container, + anchor, + parentComponent, + null, + // fallback tree will not have suspense context + namespace, + slotScopeIds + ); + setActiveBranch(suspense, vnode.ssFallback); + } else { + suspense.resolve(false, true); + } +} +function patchSuspense(n1, n2, container, anchor, parentComponent, namespace, slotScopeIds, optimized, { p: patch, um: unmount, o: { createElement } }) { + const suspense = n2.suspense = n1.suspense; + suspense.vnode = n2; + n2.el = n1.el; + const newBranch = n2.ssContent; + const newFallback = n2.ssFallback; + const { activeBranch, pendingBranch, isInFallback, isHydrating } = suspense; + if (pendingBranch) { + suspense.pendingBranch = newBranch; + if (isSameVNodeType(newBranch, pendingBranch)) { + patch( + pendingBranch, + newBranch, + suspense.hiddenContainer, + null, + parentComponent, + suspense, + namespace, + slotScopeIds, + optimized + ); + if (suspense.deps <= 0) { + suspense.resolve(); + } else if (isInFallback) { + if (!isHydrating) { + patch( + activeBranch, + newFallback, + container, + anchor, + parentComponent, + null, + // fallback tree will not have suspense context + namespace, + slotScopeIds, + optimized + ); + setActiveBranch(suspense, newFallback); + } + } + } else { + suspense.pendingId = suspenseId++; + if (isHydrating) { + suspense.isHydrating = false; + suspense.activeBranch = pendingBranch; + } else { + unmount(pendingBranch, parentComponent, suspense); + } + suspense.deps = 0; + suspense.effects.length = 0; + suspense.hiddenContainer = createElement("div"); + if (isInFallback) { + patch( + null, + newBranch, + suspense.hiddenContainer, + null, + parentComponent, + suspense, + namespace, + slotScopeIds, + optimized + ); + if (suspense.deps <= 0) { + suspense.resolve(); + } else { + patch( + activeBranch, + newFallback, + container, + anchor, + parentComponent, + null, + // fallback tree will not have suspense context + namespace, + slotScopeIds, + optimized + ); + setActiveBranch(suspense, newFallback); + } + } else if (activeBranch && isSameVNodeType(newBranch, activeBranch)) { + patch( + activeBranch, + newBranch, + container, + anchor, + parentComponent, + suspense, + namespace, + slotScopeIds, + optimized + ); + suspense.resolve(true); + } else { + patch( + null, + newBranch, + suspense.hiddenContainer, + null, + parentComponent, + suspense, + namespace, + slotScopeIds, + optimized + ); + if (suspense.deps <= 0) { + suspense.resolve(); + } + } + } + } else { + if (activeBranch && isSameVNodeType(newBranch, activeBranch)) { + patch( + activeBranch, + newBranch, + container, + anchor, + parentComponent, + suspense, + namespace, + slotScopeIds, + optimized + ); + setActiveBranch(suspense, newBranch); + } else { + triggerEvent(n2, "onPending"); + suspense.pendingBranch = newBranch; + if (newBranch.shapeFlag & 512) { + suspense.pendingId = newBranch.component.suspenseId; + } else { + suspense.pendingId = suspenseId++; + } + patch( + null, + newBranch, + suspense.hiddenContainer, + null, + parentComponent, + suspense, + namespace, + slotScopeIds, + optimized + ); + if (suspense.deps <= 0) { + suspense.resolve(); + } else { + const { timeout, pendingId } = suspense; + if (timeout > 0) { + setTimeout(() => { + if (suspense.pendingId === pendingId) { + suspense.fallback(newFallback); + } + }, timeout); + } else if (timeout === 0) { + suspense.fallback(newFallback); + } + } + } + } +} +var hasWarned = false; +function createSuspenseBoundary(vnode, parentSuspense, parentComponent, container, hiddenContainer, anchor, namespace, slotScopeIds, optimized, rendererInternals, isHydrating = false) { + if (!hasWarned) { + hasWarned = true; + console[console.info ? "info" : "log"]( + ` is an experimental feature and its API will likely change.` + ); + } + const { + p: patch, + m: move, + um: unmount, + n: next, + o: { parentNode, remove: remove2 } + } = rendererInternals; + let parentSuspenseId; + const isSuspensible = isVNodeSuspensible(vnode); + if (isSuspensible) { + if (parentSuspense == null ? void 0 : parentSuspense.pendingBranch) { + parentSuspenseId = parentSuspense.pendingId; + parentSuspense.deps++; + } + } + const timeout = vnode.props ? toNumber(vnode.props.timeout) : void 0; + if (true) { + assertNumber(timeout, `Suspense timeout`); + } + const suspense = { + vnode, + parent: parentSuspense, + parentComponent, + namespace, + container, + hiddenContainer, + anchor, + deps: 0, + pendingId: 0, + timeout: typeof timeout === "number" ? timeout : -1, + activeBranch: null, + pendingBranch: null, + isInFallback: !isHydrating, + isHydrating, + isUnmounted: false, + effects: [], + resolve(resume = false, sync = false) { + if (true) { + if (!resume && !suspense.pendingBranch) { + throw new Error( + `suspense.resolve() is called without a pending branch.` + ); + } + if (suspense.isUnmounted) { + throw new Error( + `suspense.resolve() is called on an already unmounted suspense boundary.` + ); + } + } + const { + vnode: vnode2, + activeBranch, + pendingBranch, + pendingId, + effects, + parentComponent: parentComponent2, + container: container2 + } = suspense; + let delayEnter = false; + if (suspense.isHydrating) { + suspense.isHydrating = false; + } else if (!resume) { + delayEnter = activeBranch && pendingBranch.transition && pendingBranch.transition.mode === "out-in"; + if (delayEnter) { + activeBranch.transition.afterLeave = () => { + if (pendingId === suspense.pendingId) { + move( + pendingBranch, + container2, + next(activeBranch), + 0 + ); + queuePostFlushCb(effects); + } + }; + } + let { anchor: anchor2 } = suspense; + if (activeBranch) { + anchor2 = next(activeBranch); + unmount(activeBranch, parentComponent2, suspense, true); + } + if (!delayEnter) { + move(pendingBranch, container2, anchor2, 0); + } + } + setActiveBranch(suspense, pendingBranch); + suspense.pendingBranch = null; + suspense.isInFallback = false; + let parent = suspense.parent; + let hasUnresolvedAncestor = false; + while (parent) { + if (parent.pendingBranch) { + parent.effects.push(...effects); + hasUnresolvedAncestor = true; + break; + } + parent = parent.parent; + } + if (!hasUnresolvedAncestor && !delayEnter) { + queuePostFlushCb(effects); + } + suspense.effects = []; + if (isSuspensible) { + if (parentSuspense && parentSuspense.pendingBranch && parentSuspenseId === parentSuspense.pendingId) { + parentSuspense.deps--; + if (parentSuspense.deps === 0 && !sync) { + parentSuspense.resolve(); + } + } + } + triggerEvent(vnode2, "onResolve"); + }, + fallback(fallbackVNode) { + if (!suspense.pendingBranch) { + return; + } + const { vnode: vnode2, activeBranch, parentComponent: parentComponent2, container: container2, namespace: namespace2 } = suspense; + triggerEvent(vnode2, "onFallback"); + const anchor2 = next(activeBranch); + const mountFallback = () => { + if (!suspense.isInFallback) { + return; + } + patch( + null, + fallbackVNode, + container2, + anchor2, + parentComponent2, + null, + // fallback tree will not have suspense context + namespace2, + slotScopeIds, + optimized + ); + setActiveBranch(suspense, fallbackVNode); + }; + const delayEnter = fallbackVNode.transition && fallbackVNode.transition.mode === "out-in"; + if (delayEnter) { + activeBranch.transition.afterLeave = mountFallback; + } + suspense.isInFallback = true; + unmount( + activeBranch, + parentComponent2, + null, + // no suspense so unmount hooks fire now + true + // shouldRemove + ); + if (!delayEnter) { + mountFallback(); + } + }, + move(container2, anchor2, type) { + suspense.activeBranch && move(suspense.activeBranch, container2, anchor2, type); + suspense.container = container2; + }, + next() { + return suspense.activeBranch && next(suspense.activeBranch); + }, + registerDep(instance, setupRenderEffect) { + const isInPendingSuspense = !!suspense.pendingBranch; + if (isInPendingSuspense) { + suspense.deps++; + } + const hydratedEl = instance.vnode.el; + instance.asyncDep.catch((err) => { + handleError(err, instance, 0); + }).then((asyncSetupResult) => { + if (instance.isUnmounted || suspense.isUnmounted || suspense.pendingId !== instance.suspenseId) { + return; + } + instance.asyncResolved = true; + const { vnode: vnode2 } = instance; + if (true) { + pushWarningContext(vnode2); + } + handleSetupResult(instance, asyncSetupResult, false); + if (hydratedEl) { + vnode2.el = hydratedEl; + } + const placeholder = !hydratedEl && instance.subTree.el; + setupRenderEffect( + instance, + vnode2, + // component may have been moved before resolve. + // if this is not a hydration, instance.subTree will be the comment + // placeholder. + parentNode(hydratedEl || instance.subTree.el), + // anchor will not be used if this is hydration, so only need to + // consider the comment placeholder case. + hydratedEl ? null : next(instance.subTree), + suspense, + namespace, + optimized + ); + if (placeholder) { + remove2(placeholder); + } + updateHOCHostEl(instance, vnode2.el); + if (true) { + popWarningContext(); + } + if (isInPendingSuspense && --suspense.deps === 0) { + suspense.resolve(); + } + }); + }, + unmount(parentSuspense2, doRemove) { + suspense.isUnmounted = true; + if (suspense.activeBranch) { + unmount( + suspense.activeBranch, + parentComponent, + parentSuspense2, + doRemove + ); + } + if (suspense.pendingBranch) { + unmount( + suspense.pendingBranch, + parentComponent, + parentSuspense2, + doRemove + ); + } + } + }; + return suspense; +} +function hydrateSuspense(node, vnode, parentComponent, parentSuspense, namespace, slotScopeIds, optimized, rendererInternals, hydrateNode) { + const suspense = vnode.suspense = createSuspenseBoundary( + vnode, + parentSuspense, + parentComponent, + node.parentNode, + // eslint-disable-next-line no-restricted-globals + document.createElement("div"), + null, + namespace, + slotScopeIds, + optimized, + rendererInternals, + true + ); + const result = hydrateNode( + node, + suspense.pendingBranch = vnode.ssContent, + parentComponent, + suspense, + slotScopeIds, + optimized + ); + if (suspense.deps === 0) { + suspense.resolve(false, true); + } + return result; +} +function normalizeSuspenseChildren(vnode) { + const { shapeFlag, children } = vnode; + const isSlotChildren = shapeFlag & 32; + vnode.ssContent = normalizeSuspenseSlot( + isSlotChildren ? children.default : children + ); + vnode.ssFallback = isSlotChildren ? normalizeSuspenseSlot(children.fallback) : createVNode(Comment); +} +function normalizeSuspenseSlot(s) { + let block; + if (isFunction(s)) { + const trackBlock = isBlockTreeEnabled && s._c; + if (trackBlock) { + s._d = false; + openBlock(); + } + s = s(); + if (trackBlock) { + s._d = true; + block = currentBlock; + closeBlock(); + } + } + if (isArray(s)) { + const singleChild = filterSingleRoot(s); + if (!singleChild && s.filter((child) => child !== NULL_DYNAMIC_COMPONENT).length > 0) { + warn$1(` slots expect a single root node.`); + } + s = singleChild; + } + s = normalizeVNode(s); + if (block && !s.dynamicChildren) { + s.dynamicChildren = block.filter((c) => c !== s); + } + return s; +} +function queueEffectWithSuspense(fn, suspense) { + if (suspense && suspense.pendingBranch) { + if (isArray(fn)) { + suspense.effects.push(...fn); + } else { + suspense.effects.push(fn); + } + } else { + queuePostFlushCb(fn); + } +} +function setActiveBranch(suspense, branch) { + suspense.activeBranch = branch; + const { vnode, parentComponent } = suspense; + const el = vnode.el = branch.el; + if (parentComponent && parentComponent.subTree === vnode) { + parentComponent.vnode.el = el; + updateHOCHostEl(parentComponent, el); + } +} +function isVNodeSuspensible(vnode) { + var _a; + return ((_a = vnode.props) == null ? void 0 : _a.suspensible) != null && vnode.props.suspensible !== false; +} +var ssrContextKey = Symbol.for("v-scx"); +var useSSRContext = () => { + { + const ctx = inject(ssrContextKey); + if (!ctx) { + warn$1( + `Server rendering context not provided. Make sure to only call useSSRContext() conditionally in the server build.` + ); + } + return ctx; + } +}; +function watchEffect(effect2, options) { + return doWatch(effect2, null, options); +} +function watchPostEffect(effect2, options) { + return doWatch( + effect2, + null, + true ? extend({}, options, { flush: "post" }) : { flush: "post" } + ); +} +function watchSyncEffect(effect2, options) { + return doWatch( + effect2, + null, + true ? extend({}, options, { flush: "sync" }) : { flush: "sync" } + ); +} +var INITIAL_WATCHER_VALUE = {}; +function watch(source, cb, options) { + if (!isFunction(cb)) { + warn$1( + `\`watch(fn, options?)\` signature has been moved to a separate API. Use \`watchEffect(fn, options?)\` instead. \`watch\` now only supports \`watch(source, cb, options?) signature.` + ); + } + return doWatch(source, cb, options); +} +function doWatch(source, cb, { + immediate, + deep, + flush, + once, + onTrack, + onTrigger +} = EMPTY_OBJ) { + var _a; + if (cb && once) { + const _cb = cb; + cb = (...args) => { + _cb(...args); + unwatch(); + }; + } + if (!cb) { + if (immediate !== void 0) { + warn$1( + `watch() "immediate" option is only respected when using the watch(source, callback, options?) signature.` + ); + } + if (deep !== void 0) { + warn$1( + `watch() "deep" option is only respected when using the watch(source, callback, options?) signature.` + ); + } + if (once !== void 0) { + warn$1( + `watch() "once" option is only respected when using the watch(source, callback, options?) signature.` + ); + } + } + const warnInvalidSource = (s) => { + warn$1( + `Invalid watch source: `, + s, + `A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types.` + ); + }; + const instance = getCurrentScope() === ((_a = currentInstance) == null ? void 0 : _a.scope) ? currentInstance : null; + let getter; + let forceTrigger = false; + let isMultiSource = false; + if (isRef(source)) { + getter = () => source.value; + forceTrigger = isShallow(source); + } else if (isReactive(source)) { + getter = isShallow(source) || deep === false ? () => traverse(source, 1) : () => traverse(source); + forceTrigger = true; + } else if (isArray(source)) { + isMultiSource = true; + forceTrigger = source.some((s) => isReactive(s) || isShallow(s)); + getter = () => source.map((s) => { + if (isRef(s)) { + return s.value; + } else if (isReactive(s)) { + return traverse(s, isShallow(s) || deep === false ? 1 : void 0); + } else if (isFunction(s)) { + return callWithErrorHandling(s, instance, 2); + } else { + warnInvalidSource(s); + } + }); + } else if (isFunction(source)) { + if (cb) { + getter = () => callWithErrorHandling(source, instance, 2); + } else { + getter = () => { + if (instance && instance.isUnmounted) { + return; + } + if (cleanup) { + cleanup(); + } + return callWithAsyncErrorHandling( + source, + instance, + 3, + [onCleanup] + ); + }; + } + } else { + getter = NOOP; + warnInvalidSource(source); + } + if (cb && deep) { + const baseGetter = getter; + getter = () => traverse(baseGetter()); + } + let cleanup; + let onCleanup = (fn) => { + cleanup = effect2.onStop = () => { + callWithErrorHandling(fn, instance, 4); + cleanup = effect2.onStop = void 0; + }; + }; + let ssrCleanup; + if (isInSSRComponentSetup) { + onCleanup = NOOP; + if (!cb) { + getter(); + } else if (immediate) { + callWithAsyncErrorHandling(cb, instance, 3, [ + getter(), + isMultiSource ? [] : void 0, + onCleanup + ]); + } + if (flush === "sync") { + const ctx = useSSRContext(); + ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = []); + } else { + return NOOP; + } + } + let oldValue = isMultiSource ? new Array(source.length).fill(INITIAL_WATCHER_VALUE) : INITIAL_WATCHER_VALUE; + const job = () => { + if (!effect2.active || !effect2.dirty) { + return; + } + if (cb) { + const newValue = effect2.run(); + if (deep || forceTrigger || (isMultiSource ? newValue.some((v, i) => hasChanged(v, oldValue[i])) : hasChanged(newValue, oldValue)) || false) { + if (cleanup) { + cleanup(); + } + callWithAsyncErrorHandling(cb, instance, 3, [ + newValue, + // pass undefined as the old value when it's changed for the first time + oldValue === INITIAL_WATCHER_VALUE ? void 0 : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE ? [] : oldValue, + onCleanup + ]); + oldValue = newValue; + } + } else { + effect2.run(); + } + }; + job.allowRecurse = !!cb; + let scheduler; + if (flush === "sync") { + scheduler = job; + } else if (flush === "post") { + scheduler = () => queuePostRenderEffect(job, instance && instance.suspense); + } else { + job.pre = true; + if (instance) + job.id = instance.uid; + scheduler = () => queueJob(job); + } + const effect2 = new ReactiveEffect(getter, NOOP, scheduler); + const unwatch = () => { + effect2.stop(); + if (instance && instance.scope) { + remove(instance.scope.effects, effect2); + } + }; + if (true) { + effect2.onTrack = onTrack; + effect2.onTrigger = onTrigger; + } + if (cb) { + if (immediate) { + job(); + } else { + oldValue = effect2.run(); + } + } else if (flush === "post") { + queuePostRenderEffect( + effect2.run.bind(effect2), + instance && instance.suspense + ); + } else { + effect2.run(); + } + if (ssrCleanup) + ssrCleanup.push(unwatch); + return unwatch; +} +function instanceWatch(source, value, options) { + const publicThis = this.proxy; + const getter = isString(source) ? source.includes(".") ? createPathGetter(publicThis, source) : () => publicThis[source] : source.bind(publicThis, publicThis); + let cb; + if (isFunction(value)) { + cb = value; + } else { + cb = value.handler; + options = value; + } + const cur = currentInstance; + setCurrentInstance(this); + const res = doWatch(getter, cb.bind(publicThis), options); + if (cur) { + setCurrentInstance(cur); + } else { + unsetCurrentInstance(); + } + return res; +} +function createPathGetter(ctx, path) { + const segments = path.split("."); + return () => { + let cur = ctx; + for (let i = 0; i < segments.length && cur; i++) { + cur = cur[segments[i]]; + } + return cur; + }; +} +function traverse(value, depth, currentDepth = 0, seen) { + if (!isObject(value) || value["__v_skip"]) { + return value; + } + if (depth && depth > 0) { + if (currentDepth >= depth) { + return value; + } + currentDepth++; + } + seen = seen || /* @__PURE__ */ new Set(); + if (seen.has(value)) { + return value; + } + seen.add(value); + if (isRef(value)) { + traverse(value.value, depth, currentDepth, seen); + } else if (isArray(value)) { + for (let i = 0; i < value.length; i++) { + traverse(value[i], depth, currentDepth, seen); + } + } else if (isSet(value) || isMap(value)) { + value.forEach((v) => { + traverse(v, depth, currentDepth, seen); + }); + } else if (isPlainObject(value)) { + for (const key in value) { + traverse(value[key], depth, currentDepth, seen); + } + } + return value; +} +function validateDirectiveName(name) { + if (isBuiltInDirective(name)) { + warn$1("Do not use built-in directive ids as custom directive id: " + name); + } +} +function withDirectives(vnode, directives) { + const internalInstance = currentRenderingInstance; + if (internalInstance === null) { + warn$1(`withDirectives can only be used inside render functions.`); + return vnode; + } + const instance = getExposeProxy(internalInstance) || internalInstance.proxy; + const bindings = vnode.dirs || (vnode.dirs = []); + for (let i = 0; i < directives.length; i++) { + let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]; + if (dir) { + if (isFunction(dir)) { + dir = { + mounted: dir, + updated: dir + }; + } + if (dir.deep) { + traverse(value); + } + bindings.push({ + dir, + instance, + value, + oldValue: void 0, + arg, + modifiers + }); + } + } + return vnode; +} +function invokeDirectiveHook(vnode, prevVNode, instance, name) { + const bindings = vnode.dirs; + const oldBindings = prevVNode && prevVNode.dirs; + for (let i = 0; i < bindings.length; i++) { + const binding = bindings[i]; + if (oldBindings) { + binding.oldValue = oldBindings[i].value; + } + let hook = binding.dir[name]; + if (hook) { + pauseTracking(); + callWithAsyncErrorHandling(hook, instance, 8, [ + vnode.el, + binding, + vnode, + prevVNode + ]); + resetTracking(); + } + } +} +var leaveCbKey = Symbol("_leaveCb"); +var enterCbKey = Symbol("_enterCb"); +function useTransitionState() { + const state = { + isMounted: false, + isLeaving: false, + isUnmounting: false, + leavingVNodes: /* @__PURE__ */ new Map() + }; + onMounted(() => { + state.isMounted = true; + }); + onBeforeUnmount(() => { + state.isUnmounting = true; + }); + return state; +} +var TransitionHookValidator = [Function, Array]; +var BaseTransitionPropsValidators = { + mode: String, + appear: Boolean, + persisted: Boolean, + // enter + onBeforeEnter: TransitionHookValidator, + onEnter: TransitionHookValidator, + onAfterEnter: TransitionHookValidator, + onEnterCancelled: TransitionHookValidator, + // leave + onBeforeLeave: TransitionHookValidator, + onLeave: TransitionHookValidator, + onAfterLeave: TransitionHookValidator, + onLeaveCancelled: TransitionHookValidator, + // appear + onBeforeAppear: TransitionHookValidator, + onAppear: TransitionHookValidator, + onAfterAppear: TransitionHookValidator, + onAppearCancelled: TransitionHookValidator +}; +var BaseTransitionImpl = { + name: `BaseTransition`, + props: BaseTransitionPropsValidators, + setup(props, { slots }) { + const instance = getCurrentInstance(); + const state = useTransitionState(); + let prevTransitionKey; + return () => { + const children = slots.default && getTransitionRawChildren(slots.default(), true); + if (!children || !children.length) { + return; + } + let child = children[0]; + if (children.length > 1) { + let hasFound = false; + for (const c of children) { + if (c.type !== Comment) { + if (hasFound) { + warn$1( + " can only be used on a single element or component. Use for lists." + ); + break; + } + child = c; + hasFound = true; + if (false) + break; + } + } + } + const rawProps = toRaw(props); + const { mode } = rawProps; + if (mode && mode !== "in-out" && mode !== "out-in" && mode !== "default") { + warn$1(`invalid mode: ${mode}`); + } + if (state.isLeaving) { + return emptyPlaceholder(child); + } + const innerChild = getKeepAliveChild(child); + if (!innerChild) { + return emptyPlaceholder(child); + } + const enterHooks = resolveTransitionHooks( + innerChild, + rawProps, + state, + instance + ); + setTransitionHooks(innerChild, enterHooks); + const oldChild = instance.subTree; + const oldInnerChild = oldChild && getKeepAliveChild(oldChild); + let transitionKeyChanged = false; + const { getTransitionKey } = innerChild.type; + if (getTransitionKey) { + const key = getTransitionKey(); + if (prevTransitionKey === void 0) { + prevTransitionKey = key; + } else if (key !== prevTransitionKey) { + prevTransitionKey = key; + transitionKeyChanged = true; + } + } + if (oldInnerChild && oldInnerChild.type !== Comment && (!isSameVNodeType(innerChild, oldInnerChild) || transitionKeyChanged)) { + const leavingHooks = resolveTransitionHooks( + oldInnerChild, + rawProps, + state, + instance + ); + setTransitionHooks(oldInnerChild, leavingHooks); + if (mode === "out-in") { + state.isLeaving = true; + leavingHooks.afterLeave = () => { + state.isLeaving = false; + if (instance.update.active !== false) { + instance.effect.dirty = true; + instance.update(); + } + }; + return emptyPlaceholder(child); + } else if (mode === "in-out" && innerChild.type !== Comment) { + leavingHooks.delayLeave = (el, earlyRemove, delayedLeave) => { + const leavingVNodesCache = getLeavingNodesForType( + state, + oldInnerChild + ); + leavingVNodesCache[String(oldInnerChild.key)] = oldInnerChild; + el[leaveCbKey] = () => { + earlyRemove(); + el[leaveCbKey] = void 0; + delete enterHooks.delayedLeave; + }; + enterHooks.delayedLeave = delayedLeave; + }; + } + } + return child; + }; + } +}; +var BaseTransition = BaseTransitionImpl; +function getLeavingNodesForType(state, vnode) { + const { leavingVNodes } = state; + let leavingVNodesCache = leavingVNodes.get(vnode.type); + if (!leavingVNodesCache) { + leavingVNodesCache = /* @__PURE__ */ Object.create(null); + leavingVNodes.set(vnode.type, leavingVNodesCache); + } + return leavingVNodesCache; +} +function resolveTransitionHooks(vnode, props, state, instance) { + const { + appear, + mode, + persisted = false, + onBeforeEnter, + onEnter, + onAfterEnter, + onEnterCancelled, + onBeforeLeave, + onLeave, + onAfterLeave, + onLeaveCancelled, + onBeforeAppear, + onAppear, + onAfterAppear, + onAppearCancelled + } = props; + const key = String(vnode.key); + const leavingVNodesCache = getLeavingNodesForType(state, vnode); + const callHook3 = (hook, args) => { + hook && callWithAsyncErrorHandling( + hook, + instance, + 9, + args + ); + }; + const callAsyncHook = (hook, args) => { + const done = args[1]; + callHook3(hook, args); + if (isArray(hook)) { + if (hook.every((hook2) => hook2.length <= 1)) + done(); + } else if (hook.length <= 1) { + done(); + } + }; + const hooks = { + mode, + persisted, + beforeEnter(el) { + let hook = onBeforeEnter; + if (!state.isMounted) { + if (appear) { + hook = onBeforeAppear || onBeforeEnter; + } else { + return; + } + } + if (el[leaveCbKey]) { + el[leaveCbKey]( + true + /* cancelled */ + ); + } + const leavingVNode = leavingVNodesCache[key]; + if (leavingVNode && isSameVNodeType(vnode, leavingVNode) && leavingVNode.el[leaveCbKey]) { + leavingVNode.el[leaveCbKey](); + } + callHook3(hook, [el]); + }, + enter(el) { + let hook = onEnter; + let afterHook = onAfterEnter; + let cancelHook = onEnterCancelled; + if (!state.isMounted) { + if (appear) { + hook = onAppear || onEnter; + afterHook = onAfterAppear || onAfterEnter; + cancelHook = onAppearCancelled || onEnterCancelled; + } else { + return; + } + } + let called = false; + const done = el[enterCbKey] = (cancelled) => { + if (called) + return; + called = true; + if (cancelled) { + callHook3(cancelHook, [el]); + } else { + callHook3(afterHook, [el]); + } + if (hooks.delayedLeave) { + hooks.delayedLeave(); + } + el[enterCbKey] = void 0; + }; + if (hook) { + callAsyncHook(hook, [el, done]); + } else { + done(); + } + }, + leave(el, remove2) { + const key2 = String(vnode.key); + if (el[enterCbKey]) { + el[enterCbKey]( + true + /* cancelled */ + ); + } + if (state.isUnmounting) { + return remove2(); + } + callHook3(onBeforeLeave, [el]); + let called = false; + const done = el[leaveCbKey] = (cancelled) => { + if (called) + return; + called = true; + remove2(); + if (cancelled) { + callHook3(onLeaveCancelled, [el]); + } else { + callHook3(onAfterLeave, [el]); + } + el[leaveCbKey] = void 0; + if (leavingVNodesCache[key2] === vnode) { + delete leavingVNodesCache[key2]; + } + }; + leavingVNodesCache[key2] = vnode; + if (onLeave) { + callAsyncHook(onLeave, [el, done]); + } else { + done(); + } + }, + clone(vnode2) { + return resolveTransitionHooks(vnode2, props, state, instance); + } + }; + return hooks; +} +function emptyPlaceholder(vnode) { + if (isKeepAlive(vnode)) { + vnode = cloneVNode(vnode); + vnode.children = null; + return vnode; + } +} +function getKeepAliveChild(vnode) { + return isKeepAlive(vnode) ? ( + // #7121 ensure get the child component subtree in case + // it's been replaced during HMR + vnode.component ? vnode.component.subTree : vnode.children ? vnode.children[0] : void 0 + ) : vnode; +} +function setTransitionHooks(vnode, hooks) { + if (vnode.shapeFlag & 6 && vnode.component) { + setTransitionHooks(vnode.component.subTree, hooks); + } else if (vnode.shapeFlag & 128) { + vnode.ssContent.transition = hooks.clone(vnode.ssContent); + vnode.ssFallback.transition = hooks.clone(vnode.ssFallback); + } else { + vnode.transition = hooks; + } +} +function getTransitionRawChildren(children, keepComment = false, parentKey) { + let ret = []; + let keyedFragmentCount = 0; + for (let i = 0; i < children.length; i++) { + let child = children[i]; + const key = parentKey == null ? child.key : String(parentKey) + String(child.key != null ? child.key : i); + if (child.type === Fragment) { + if (child.patchFlag & 128) + keyedFragmentCount++; + ret = ret.concat( + getTransitionRawChildren(child.children, keepComment, key) + ); + } else if (keepComment || child.type !== Comment) { + ret.push(key != null ? cloneVNode(child, { key }) : child); + } + } + if (keyedFragmentCount > 1) { + for (let i = 0; i < ret.length; i++) { + ret[i].patchFlag = -2; + } + } + return ret; +} +function defineComponent(options, extraOptions) { + return isFunction(options) ? ( + // #8326: extend call and options.name access are considered side-effects + // by Rollup, so we have to wrap it in a pure-annotated IIFE. + (() => extend({ name: options.name }, extraOptions, { setup: options }))() + ) : options; +} +var isAsyncWrapper = (i) => !!i.type.__asyncLoader; +function defineAsyncComponent(source) { + if (isFunction(source)) { + source = { loader: source }; + } + const { + loader, + loadingComponent, + errorComponent, + delay = 200, + timeout, + // undefined = never times out + suspensible = true, + onError: userOnError + } = source; + let pendingRequest = null; + let resolvedComp; + let retries = 0; + const retry = () => { + retries++; + pendingRequest = null; + return load(); + }; + const load = () => { + let thisRequest; + return pendingRequest || (thisRequest = pendingRequest = loader().catch((err) => { + err = err instanceof Error ? err : new Error(String(err)); + if (userOnError) { + return new Promise((resolve2, reject) => { + const userRetry = () => resolve2(retry()); + const userFail = () => reject(err); + userOnError(err, userRetry, userFail, retries + 1); + }); + } else { + throw err; + } + }).then((comp) => { + if (thisRequest !== pendingRequest && pendingRequest) { + return pendingRequest; + } + if (!comp) { + warn$1( + `Async component loader resolved to undefined. If you are using retry(), make sure to return its return value.` + ); + } + if (comp && (comp.__esModule || comp[Symbol.toStringTag] === "Module")) { + comp = comp.default; + } + if (comp && !isObject(comp) && !isFunction(comp)) { + throw new Error(`Invalid async component load result: ${comp}`); + } + resolvedComp = comp; + return comp; + })); + }; + return defineComponent({ + name: "AsyncComponentWrapper", + __asyncLoader: load, + get __asyncResolved() { + return resolvedComp; + }, + setup() { + const instance = currentInstance; + if (resolvedComp) { + return () => createInnerComp(resolvedComp, instance); + } + const onError = (err) => { + pendingRequest = null; + handleError( + err, + instance, + 13, + !errorComponent + ); + }; + if (suspensible && instance.suspense || isInSSRComponentSetup) { + return load().then((comp) => { + return () => createInnerComp(comp, instance); + }).catch((err) => { + onError(err); + return () => errorComponent ? createVNode(errorComponent, { + error: err + }) : null; + }); + } + const loaded = ref(false); + const error = ref(); + const delayed = ref(!!delay); + if (delay) { + setTimeout(() => { + delayed.value = false; + }, delay); + } + if (timeout != null) { + setTimeout(() => { + if (!loaded.value && !error.value) { + const err = new Error( + `Async component timed out after ${timeout}ms.` + ); + onError(err); + error.value = err; + } + }, timeout); + } + load().then(() => { + loaded.value = true; + if (instance.parent && isKeepAlive(instance.parent.vnode)) { + instance.parent.effect.dirty = true; + queueJob(instance.parent.update); + } + }).catch((err) => { + onError(err); + error.value = err; + }); + return () => { + if (loaded.value && resolvedComp) { + return createInnerComp(resolvedComp, instance); + } else if (error.value && errorComponent) { + return createVNode(errorComponent, { + error: error.value + }); + } else if (loadingComponent && !delayed.value) { + return createVNode(loadingComponent); + } + }; + } + }); +} +function createInnerComp(comp, parent) { + const { ref: ref2, props, children, ce } = parent.vnode; + const vnode = createVNode(comp, props, children); + vnode.ref = ref2; + vnode.ce = ce; + delete parent.vnode.ce; + return vnode; +} +var isKeepAlive = (vnode) => vnode.type.__isKeepAlive; +var KeepAliveImpl = { + name: `KeepAlive`, + // Marker for special handling inside the renderer. We are not using a === + // check directly on KeepAlive in the renderer, because importing it directly + // would prevent it from being tree-shaken. + __isKeepAlive: true, + props: { + include: [String, RegExp, Array], + exclude: [String, RegExp, Array], + max: [String, Number] + }, + setup(props, { slots }) { + const instance = getCurrentInstance(); + const sharedContext = instance.ctx; + if (!sharedContext.renderer) { + return () => { + const children = slots.default && slots.default(); + return children && children.length === 1 ? children[0] : children; + }; + } + const cache = /* @__PURE__ */ new Map(); + const keys = /* @__PURE__ */ new Set(); + let current = null; + if (true) { + instance.__v_cache = cache; + } + const parentSuspense = instance.suspense; + const { + renderer: { + p: patch, + m: move, + um: _unmount, + o: { createElement } + } + } = sharedContext; + const storageContainer = createElement("div"); + sharedContext.activate = (vnode, container, anchor, namespace, optimized) => { + const instance2 = vnode.component; + move(vnode, container, anchor, 0, parentSuspense); + patch( + instance2.vnode, + vnode, + container, + anchor, + instance2, + parentSuspense, + namespace, + vnode.slotScopeIds, + optimized + ); + queuePostRenderEffect(() => { + instance2.isDeactivated = false; + if (instance2.a) { + invokeArrayFns(instance2.a); + } + const vnodeHook = vnode.props && vnode.props.onVnodeMounted; + if (vnodeHook) { + invokeVNodeHook(vnodeHook, instance2.parent, vnode); + } + }, parentSuspense); + if (true) { + devtoolsComponentAdded(instance2); + } + }; + sharedContext.deactivate = (vnode) => { + const instance2 = vnode.component; + move(vnode, storageContainer, null, 1, parentSuspense); + queuePostRenderEffect(() => { + if (instance2.da) { + invokeArrayFns(instance2.da); + } + const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted; + if (vnodeHook) { + invokeVNodeHook(vnodeHook, instance2.parent, vnode); + } + instance2.isDeactivated = true; + }, parentSuspense); + if (true) { + devtoolsComponentAdded(instance2); + } + }; + function unmount(vnode) { + resetShapeFlag(vnode); + _unmount(vnode, instance, parentSuspense, true); + } + function pruneCache(filter) { + cache.forEach((vnode, key) => { + const name = getComponentName(vnode.type); + if (name && (!filter || !filter(name))) { + pruneCacheEntry(key); + } + }); + } + function pruneCacheEntry(key) { + const cached = cache.get(key); + if (!current || !isSameVNodeType(cached, current)) { + unmount(cached); + } else if (current) { + resetShapeFlag(current); + } + cache.delete(key); + keys.delete(key); + } + watch( + () => [props.include, props.exclude], + ([include, exclude]) => { + include && pruneCache((name) => matches(include, name)); + exclude && pruneCache((name) => !matches(exclude, name)); + }, + // prune post-render after `current` has been updated + { flush: "post", deep: true } + ); + let pendingCacheKey = null; + const cacheSubtree = () => { + if (pendingCacheKey != null) { + cache.set(pendingCacheKey, getInnerChild(instance.subTree)); + } + }; + onMounted(cacheSubtree); + onUpdated(cacheSubtree); + onBeforeUnmount(() => { + cache.forEach((cached) => { + const { subTree, suspense } = instance; + const vnode = getInnerChild(subTree); + if (cached.type === vnode.type && cached.key === vnode.key) { + resetShapeFlag(vnode); + const da = vnode.component.da; + da && queuePostRenderEffect(da, suspense); + return; + } + unmount(cached); + }); + }); + return () => { + pendingCacheKey = null; + if (!slots.default) { + return null; + } + const children = slots.default(); + const rawVNode = children[0]; + if (children.length > 1) { + if (true) { + warn$1(`KeepAlive should contain exactly one component child.`); + } + current = null; + return children; + } else if (!isVNode(rawVNode) || !(rawVNode.shapeFlag & 4) && !(rawVNode.shapeFlag & 128)) { + current = null; + return rawVNode; + } + let vnode = getInnerChild(rawVNode); + const comp = vnode.type; + const name = getComponentName( + isAsyncWrapper(vnode) ? vnode.type.__asyncResolved || {} : comp + ); + const { include, exclude, max } = props; + if (include && (!name || !matches(include, name)) || exclude && name && matches(exclude, name)) { + current = vnode; + return rawVNode; + } + const key = vnode.key == null ? comp : vnode.key; + const cachedVNode = cache.get(key); + if (vnode.el) { + vnode = cloneVNode(vnode); + if (rawVNode.shapeFlag & 128) { + rawVNode.ssContent = vnode; + } + } + pendingCacheKey = key; + if (cachedVNode) { + vnode.el = cachedVNode.el; + vnode.component = cachedVNode.component; + if (vnode.transition) { + setTransitionHooks(vnode, vnode.transition); + } + vnode.shapeFlag |= 512; + keys.delete(key); + keys.add(key); + } else { + keys.add(key); + if (max && keys.size > parseInt(max, 10)) { + pruneCacheEntry(keys.values().next().value); + } + } + vnode.shapeFlag |= 256; + current = vnode; + return isSuspense(rawVNode.type) ? rawVNode : vnode; + }; + } +}; +var KeepAlive = KeepAliveImpl; +function matches(pattern, name) { + if (isArray(pattern)) { + return pattern.some((p2) => matches(p2, name)); + } else if (isString(pattern)) { + return pattern.split(",").includes(name); + } else if (isRegExp(pattern)) { + return pattern.test(name); + } + return false; +} +function onActivated(hook, target) { + registerKeepAliveHook(hook, "a", target); +} +function onDeactivated(hook, target) { + registerKeepAliveHook(hook, "da", target); +} +function registerKeepAliveHook(hook, type, target = currentInstance) { + const wrappedHook = hook.__wdc || (hook.__wdc = () => { + let current = target; + while (current) { + if (current.isDeactivated) { + return; + } + current = current.parent; + } + return hook(); + }); + injectHook(type, wrappedHook, target); + if (target) { + let current = target.parent; + while (current && current.parent) { + if (isKeepAlive(current.parent.vnode)) { + injectToKeepAliveRoot(wrappedHook, type, target, current); + } + current = current.parent; + } + } +} +function injectToKeepAliveRoot(hook, type, target, keepAliveRoot) { + const injected = injectHook( + type, + hook, + keepAliveRoot, + true + /* prepend */ + ); + onUnmounted(() => { + remove(keepAliveRoot[type], injected); + }, target); +} +function resetShapeFlag(vnode) { + vnode.shapeFlag &= ~256; + vnode.shapeFlag &= ~512; +} +function getInnerChild(vnode) { + return vnode.shapeFlag & 128 ? vnode.ssContent : vnode; +} +function injectHook(type, hook, target = currentInstance, prepend = false) { + if (target) { + const hooks = target[type] || (target[type] = []); + const wrappedHook = hook.__weh || (hook.__weh = (...args) => { + if (target.isUnmounted) { + return; + } + pauseTracking(); + setCurrentInstance(target); + const res = callWithAsyncErrorHandling(hook, target, type, args); + unsetCurrentInstance(); + resetTracking(); + return res; + }); + if (prepend) { + hooks.unshift(wrappedHook); + } else { + hooks.push(wrappedHook); + } + return wrappedHook; + } else if (true) { + const apiName = toHandlerKey(ErrorTypeStrings$1[type].replace(/ hook$/, "")); + warn$1( + `${apiName} is called when there is no active component instance to be associated with. Lifecycle injection APIs can only be used during execution of setup(). If you are using async setup(), make sure to register lifecycle hooks before the first await statement.` + ); + } +} +var createHook = (lifecycle) => (hook, target = currentInstance) => ( + // post-create lifecycle registrations are noops during SSR (except for serverPrefetch) + (!isInSSRComponentSetup || lifecycle === "sp") && injectHook(lifecycle, (...args) => hook(...args), target) +); +var onBeforeMount = createHook("bm"); +var onMounted = createHook("m"); +var onBeforeUpdate = createHook("bu"); +var onUpdated = createHook("u"); +var onBeforeUnmount = createHook("bum"); +var onUnmounted = createHook("um"); +var onServerPrefetch = createHook("sp"); +var onRenderTriggered = createHook( + "rtg" +); +var onRenderTracked = createHook( + "rtc" +); +function onErrorCaptured(hook, target = currentInstance) { + injectHook("ec", hook, target); +} +function renderList(source, renderItem, cache, index) { + let ret; + const cached = cache && cache[index]; + if (isArray(source) || isString(source)) { + ret = new Array(source.length); + for (let i = 0, l = source.length; i < l; i++) { + ret[i] = renderItem(source[i], i, void 0, cached && cached[i]); + } + } else if (typeof source === "number") { + if (!Number.isInteger(source)) { + warn$1(`The v-for range expect an integer value but got ${source}.`); + } + ret = new Array(source); + for (let i = 0; i < source; i++) { + ret[i] = renderItem(i + 1, i, void 0, cached && cached[i]); + } + } else if (isObject(source)) { + if (source[Symbol.iterator]) { + ret = Array.from( + source, + (item, i) => renderItem(item, i, void 0, cached && cached[i]) + ); + } else { + const keys = Object.keys(source); + ret = new Array(keys.length); + for (let i = 0, l = keys.length; i < l; i++) { + const key = keys[i]; + ret[i] = renderItem(source[key], key, i, cached && cached[i]); + } + } + } else { + ret = []; + } + if (cache) { + cache[index] = ret; + } + return ret; +} +function createSlots(slots, dynamicSlots) { + for (let i = 0; i < dynamicSlots.length; i++) { + const slot = dynamicSlots[i]; + if (isArray(slot)) { + for (let j = 0; j < slot.length; j++) { + slots[slot[j].name] = slot[j].fn; + } + } else if (slot) { + slots[slot.name] = slot.key ? (...args) => { + const res = slot.fn(...args); + if (res) + res.key = slot.key; + return res; + } : slot.fn; + } + } + return slots; +} +function renderSlot(slots, name, props = {}, fallback, noSlotted) { + if (currentRenderingInstance.isCE || currentRenderingInstance.parent && isAsyncWrapper(currentRenderingInstance.parent) && currentRenderingInstance.parent.isCE) { + if (name !== "default") + props.name = name; + return createVNode("slot", props, fallback && fallback()); + } + let slot = slots[name]; + if (slot && slot.length > 1) { + warn$1( + `SSR-optimized slot function detected in a non-SSR-optimized render function. You need to mark this component with $dynamic-slots in the parent template.` + ); + slot = () => []; + } + if (slot && slot._c) { + slot._d = false; + } + openBlock(); + const validSlotContent = slot && ensureValidVNode(slot(props)); + const rendered = createBlock( + Fragment, + { + key: props.key || // slot content array of a dynamic conditional slot may have a branch + // key attached in the `createSlots` helper, respect that + validSlotContent && validSlotContent.key || `_${name}` + }, + validSlotContent || (fallback ? fallback() : []), + validSlotContent && slots._ === 1 ? 64 : -2 + ); + if (!noSlotted && rendered.scopeId) { + rendered.slotScopeIds = [rendered.scopeId + "-s"]; + } + if (slot && slot._c) { + slot._d = true; + } + return rendered; +} +function ensureValidVNode(vnodes) { + return vnodes.some((child) => { + if (!isVNode(child)) + return true; + if (child.type === Comment) + return false; + if (child.type === Fragment && !ensureValidVNode(child.children)) + return false; + return true; + }) ? vnodes : null; +} +function toHandlers(obj, preserveCaseIfNecessary) { + const ret = {}; + if (!isObject(obj)) { + warn$1(`v-on with no argument expects an object value.`); + return ret; + } + for (const key in obj) { + ret[preserveCaseIfNecessary && /[A-Z]/.test(key) ? `on:${key}` : toHandlerKey(key)] = obj[key]; + } + return ret; +} +var getPublicInstance = (i) => { + if (!i) + return null; + if (isStatefulComponent(i)) + return getExposeProxy(i) || i.proxy; + return getPublicInstance(i.parent); +}; +var publicPropertiesMap = ( + // Move PURE marker to new line to workaround compiler discarding it + // due to type annotation + extend(/* @__PURE__ */ Object.create(null), { + $: (i) => i, + $el: (i) => i.vnode.el, + $data: (i) => i.data, + $props: (i) => true ? shallowReadonly(i.props) : i.props, + $attrs: (i) => true ? shallowReadonly(i.attrs) : i.attrs, + $slots: (i) => true ? shallowReadonly(i.slots) : i.slots, + $refs: (i) => true ? shallowReadonly(i.refs) : i.refs, + $parent: (i) => getPublicInstance(i.parent), + $root: (i) => getPublicInstance(i.root), + $emit: (i) => i.emit, + $options: (i) => __VUE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type, + $forceUpdate: (i) => i.f || (i.f = () => { + i.effect.dirty = true; + queueJob(i.update); + }), + $nextTick: (i) => i.n || (i.n = nextTick.bind(i.proxy)), + $watch: (i) => __VUE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP + }) +); +var isReservedPrefix = (key) => key === "_" || key === "$"; +var hasSetupBinding = (state, key) => state !== EMPTY_OBJ && !state.__isScriptSetup && hasOwn(state, key); +var PublicInstanceProxyHandlers = { + get({ _: instance }, key) { + const { ctx, setupState, data, props, accessCache, type, appContext } = instance; + if (key === "__isVue") { + return true; + } + let normalizedProps; + if (key[0] !== "$") { + const n = accessCache[key]; + if (n !== void 0) { + switch (n) { + case 1: + return setupState[key]; + case 2: + return data[key]; + case 4: + return ctx[key]; + case 3: + return props[key]; + } + } else if (hasSetupBinding(setupState, key)) { + accessCache[key] = 1; + return setupState[key]; + } else if (data !== EMPTY_OBJ && hasOwn(data, key)) { + accessCache[key] = 2; + return data[key]; + } else if ( + // only cache other properties when instance has declared (thus stable) + // props + (normalizedProps = instance.propsOptions[0]) && hasOwn(normalizedProps, key) + ) { + accessCache[key] = 3; + return props[key]; + } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) { + accessCache[key] = 4; + return ctx[key]; + } else if (!__VUE_OPTIONS_API__ || shouldCacheAccess) { + accessCache[key] = 0; + } + } + const publicGetter = publicPropertiesMap[key]; + let cssModule, globalProperties; + if (publicGetter) { + if (key === "$attrs") { + track(instance, "get", key); + markAttrsAccessed(); + } else if (key === "$slots") { + track(instance, "get", key); + } + return publicGetter(instance); + } else if ( + // css module (injected by vue-loader) + (cssModule = type.__cssModules) && (cssModule = cssModule[key]) + ) { + return cssModule; + } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) { + accessCache[key] = 4; + return ctx[key]; + } else if ( + // global properties + globalProperties = appContext.config.globalProperties, hasOwn(globalProperties, key) + ) { + { + return globalProperties[key]; + } + } else if (currentRenderingInstance && (!isString(key) || // #1091 avoid internal isRef/isVNode checks on component instance leading + // to infinite warning loop + key.indexOf("__v") !== 0)) { + if (data !== EMPTY_OBJ && isReservedPrefix(key[0]) && hasOwn(data, key)) { + warn$1( + `Property ${JSON.stringify( + key + )} must be accessed via $data because it starts with a reserved character ("$" or "_") and is not proxied on the render context.` + ); + } else if (instance === currentRenderingInstance) { + warn$1( + `Property ${JSON.stringify(key)} was accessed during render but is not defined on instance.` + ); + } + } + }, + set({ _: instance }, key, value) { + const { data, setupState, ctx } = instance; + if (hasSetupBinding(setupState, key)) { + setupState[key] = value; + return true; + } else if (setupState.__isScriptSetup && hasOwn(setupState, key)) { + warn$1(`Cannot mutate + + +``` + +UI 的逻辑代码文件入口则是 `view/src/index.ts`。 + +### UI 逻辑代码 + +在 `view/src/index.ts` 中,我们使用 `Vue` 的 `createApp` 方法创建了一个 `Vue` 应用。在 `createApp` 方法中,我们传入了一个 `AppView` 组件,这个组件是我们的 UI 的根组件。 + +```typescript +import { createApp } from "vue"; +import AppView from "./app.vue"; + +async function initialize() { + const app = createApp(AppView); + ... + app.mount("#app"); +} + +initialize(); +``` + +在上述初始化函数中,最重要的部分是建立本进程的 `RPC` 服务: + +```typescript +import { createApp } from "vue"; + +import AppView from "./app.vue"; +import { RendererRPCService } from "paperlib-api/rpc"; +import { PreviewService } from "./services/preview-service"; + +async function initialize() { + const app = createApp(AppView); + + // ============================================================ + // 1. Initilize the RPC service for current process + const rpcService = new RendererRPCService("paperlib-preview-extension-window"); + // ============================================================ + // 2. Start the port exchange process. + await rpcService.initCommunication(); + + // ============================================================ + // 3. Wait for the main process to expose its APIs (PLMainAPI) + const mainAPIExposed = await rpcService.waitForAPI( + "mainProcess", + "PLMainAPI", + 5000 + ); + + if (!mainAPIExposed) { + throw new Error("Main process API is not exposed"); + } else { + console.log("Main process API is exposed"); + } + + // 4. Wait for the renderer process to expose its APIs (PLRendererAPI) + const rendererAPIExposed = await rpcService.waitForAPI( + "rendererProcess", + "PLAPI", + 5000 + ); + + if (!rendererAPIExposed) { + throw new Error("Renderer process API is not exposed"); + } else { + console.log("Renderer process API is exposed"); + } + + app.mount("#app"); +} + +initialize(); +``` + +在上述代码中,我们首先创建了一个 `RendererRPCService` 的实例。这会在当前进程中创建一个 `RPC` 服务。之后,我们调用了 `rpcService.initCommunication()` 方法,该方法会通知其他进程,建立相应的 `MessagePort` 通信通道,暴露相应的 `API`。具体实现方法请参考 Paperlib 中的 RPC 服务。 + +第一个参数,必须与创建新窗口时传入的 ID 相同。 + +在 `rpcService.initCommunication()` 方法执行完毕后,我们就可以通过 `rpcService.waitForAPI` 方法来等待其他进程暴露相应的 `API`。在这里,我们等待了主进程和渲染进程暴露了 `PLMainAPI` 和 `PLAPI`。如果你也需要访问插件进程的相应服务,比如 `PLExtAPI.extensionPreferenceService`,你也可以在这里等待插件进程暴露 `PLExtAPI`: + +```typescript +const extAPIExposed = await rpcService.waitForAPI( + "extensionProcess", // 进程 ID + "PLExtAPI", // API 名称 + 5000 // 等待时间 +); + +if (!extAPIExposed) { + throw new Error("Ext process API is not exposed"); +} else { + console.log("Ext process API is exposed"); +} +``` + +至此,你可以在新窗口的进程中,通过 `PLAPI, PLMainAPI, PLExtAPI` 访问其他进程暴露的服务了。 + + +### UI 内功能 + +实现怎样的功能,完全由开发者决定。在这里几乎没有任何限制,就像开发任何 WebAPP 一样。 + +在我们的示例插件中,我们取得了用户选择的论文,得到了 PDF 文档的路径,之后将其渲染到了新窗口中。你可以在 `services/preview-service.ts` 中找到这部分代码。 \ No newline at end of file diff --git a/src/cn/extension-doc/ext-types/simple-ext.md b/src/cn/extension-doc/ext-types/simple-ext.md new file mode 100644 index 0000000..81c3feb --- /dev/null +++ b/src/cn/extension-doc/ext-types/simple-ext.md @@ -0,0 +1,55 @@ +# Simple 插件 + +这种类型的插件是最简单的,它只需要创建一个插件类,继承自 `PLExtension`,实现 `initialize`, `dispose`,插件入口文件的 export 包含一个叫做 `initialize` 的函数即可: + +```typescript + +import { PLAPI, PLExtAPI, PLExtension, PLMainAPI } from "paperlib-api/api"; + +class PaperlibHelloworldExtension extends PLExtension { + disposeCallbacks: (() => void)[]; + + constructor() { + super({ + id: "...", + defaultPreference: { + ... + }, + }); + + this.disposeCallbacks = []; + } + + async initialize() { + await PLExtAPI.extensionPreferenceService.register( + this.id, + this.defaultPreference, + ); + + this.printSomething(); + + } + + async dispose() { + PLExtAPI.extensionPreferenceService.unregister(this.id); + + for (const disposeCallback of this.disposeCallbacks) { + disposeCallback(); + } + } + + printSomething() { + console.log("Hello world from extension!"); + } +} + +async function initialize() { + const extension = new PaperlibHelloworldExtension(); + await extension.initialize(); + + return extension; +} + +export { initialize }; + +``` \ No newline at end of file diff --git a/src/cn/extension-doc/ext-types/ui-ext.md b/src/cn/extension-doc/ext-types/ui-ext.md new file mode 100644 index 0000000..6ef438e --- /dev/null +++ b/src/cn/extension-doc/ext-types/ui-ext.md @@ -0,0 +1,37 @@ +# UI 插件 + +这种类型的插件主要特点是可以在 Paperlib 的一些面板中显示额外信息。比如在论文详情面板中,显示论文的引用量。 + +## 插件类结构 + +在这里我们给出一个示例结构,当然你可以根据自己的需要进行修改。 + +```typescript +class UIExtension extends PLExtension { + constructor() { + // 你可以在这里设置插件的 id,默认设置 + super(...) + } + + async initialize() { + // 在这里监听事件,修改 UI + this.disposeCallbacks.push( + PLAPI.uiStateService.onChanged("...", (newValues) => { + PLAPI.uiSlotService.updateSlot(...); + }), + ); + ... + } + + async dispose() { + // 在这里取消事件监听 + for (const disposeCallback of this.disposeCallbacks) { + disposeCallback(); + } + } +} +``` + +## 额外 UI 插槽 + +如果目前的插槽无法满足您插件的开发需求,请前往 [GitHub Discussions](https://github.com/Future-Scholars/paperlib/discussions) 提出您的需求,我们会考虑在未来的版本中添加新的插槽。 \ No newline at end of file diff --git a/src/cn/extension-doc/index.md b/src/cn/extension-doc/index.md new file mode 100644 index 0000000..d97550f --- /dev/null +++ b/src/cn/extension-doc/index.md @@ -0,0 +1,72 @@ +# 插件系统 + +我们很高兴地宣布,在 Paperlib 3.0 中,我引入插件系统,这意味着你可以为 Paperlib 中开发插件进行功能扩展了!🎉🎉🎉 + +## 前言 + +本文介绍插件系统的整体设计,便于理解开发过程中可能涉及的每个部分。如果你只是想开发一个简易插件,可以跳过此文。如果你在开发一个相对复杂的插件,强烈推荐阅读本文。 + +## 进程与插件运行环境 + +为了妥协多平台开发,Paperlib 选择基于 Electron。因此在整个程序中,我们有三个主要的进程: + +- 主进程 (Main Process):负责管理整个程序的生命周期,以及与系统进行交互,如上下文菜单等。 +- 渲染进程 (Renderer Process):负责渲染界面,以及大部分的程序逻辑。 +- 插件进程 (Extension Process):负责插件的管理和运行。 + +每个插件都运行在插件进程中,且代码在单独的 [node VM](https://nodejs.org/api/vm.html) 中执行,因此,插件的崩溃并不会导致 Paperlib 的崩溃,各个插件之间也互不干扰。因此每个插件运行的环境都是 `node` 环境,没有 `html` 等相关环境(New Window 插件例外)。 + +## 进程间通信 + +插件进程除了运行插件的代码之外,还负责插件的下载更新、加载、卸载,以及偏好设置。而大部分的逻辑代码都运行在主或者渲染进程里。因此 Paperlib 通过扩展 `MessagePort`,实现了 `Remote Procedure Calling (RPC)` 来暴露主/渲染进程中的各个服务(service)的方法来供插件调用。 + +每一个跨进程方法调用,都会在内部被转换为一个 `json` 字符串并发给对应进程。这里面包含了想要调用的方法,传入的参数等必须信息。对应进程收到消息之后,会解析 `json`,运行相应方法,得到结果并返回给调用者所在的进程。除此之外,我们还实现了跨进程事件监听,让开发更为方便。具体的实现细节请浏览 [Github](https://github.com/Future-Scholars/paperlib/tree/dev-3.0.0/app/base/rpc)。 + +## 服务 Service + +在每个进程中,都有许多服务提供各种各样的方法,来完成 Paperlib 的功能。比如主进程的 `ContextMenuService` 负责右键菜单相关的功能,渲染进程里面的 `PaperService` 负责最主要的论文相关的增删改查。相关实现可以在如 [Github](https://github.com/Future-Scholars/paperlib/tree/dev-3.0.0/app/renderer/services) 找到。 + +Paperlib 中的服务的几乎所有方法都暴露给了插件进程以供调用。在插件进程中几乎可以实现操作 Paperlib 的全部部分。 + +## 服务事件 Service Event + +几乎所有的服务都是 `Eventable` 的。这意味着每个服务都会在不同时机触发一些事件。在其他代码位置、进程可以监听对应的事件触发,来执行自己的代码。比如你可以监听用户选择的论文变化了,然后运行你的方法。 + +## 跨进程调用 API + +为了方便开发,我们对插件进程暴露了主进程、渲染进程,以及插件进程的服务,并把他们集中在了三个对应的 API 中。你在接下来的文档中将会时常遇到这三个 API 组: + +- `PLMainAPI`: 包含了所有的主进程服务。 +- `PLAPI`: 包含了所有的渲染进程服务。 +- `PLExtAPI`:包含了所有的插件进程服务。 + +当你想调用一个进程对应服务的方法的时候,只需要在你的插件代码中编写如下: + +```ts +// const result = await APIGroup.serviceName.methodName(...) +const papers = await PLAPI.paperService.load(...) +``` + +因为是跨进程调用,所以几乎任何调用我们都需要使用 `await` 来等待异步结果的返回。如果你是在调用 `PLExtAPI`,因为是同一个进程,所以同步或异步视情况而定,请参考开发时的类型提示。 + +## 插件类型 + +我们设计了五种不同形式的插件: + +- Simple Extension:简单的功能,自己运行一些东西。 +- Command Extension:用户通过 3.0 版本引入的 command bar 触发命令,然后运行相应功能。 +- Hook Extension:通过在 Paperlib 生命流程中的不同钩子点注册钩子,来截断、修改流程中的内容。 +- UI Extension:修改 UI 界面的部分内容。 +- New Window Extension:创建一个全新的窗口,实现完全自定义的复杂的功能。 + +这五种插件涵盖了大部分插件场景,并可以相互混合。我们提供了对应的例子,会在之后详细介绍。 + +## 插件发布与下载 + +每一个插件,都是一个 `npm` 包,发布时与你发布一个 `npm` 包类似。其余的约定和限制我们会在之后解释。 + +在 Paperlib 设置界面的插件页面中,插件可以通过本地路径加载,或者搜索插件市场来下载。我们的插件市场依托于 `npmjs.com`。 + +## 总结 + +总的来说。插件是运行在一个单独进程中,通过我们暴露的 API 进行操作 Paperlib 的 `npm` 包形式的一些代码。接下来我们推荐阅读[开发环境设置](./env),来开始开发你的插件吧! \ No newline at end of file diff --git a/src/cn/extension-doc/manifest-version.md b/src/cn/extension-doc/manifest-version.md new file mode 100644 index 0000000..4e7b475 --- /dev/null +++ b/src/cn/extension-doc/manifest-version.md @@ -0,0 +1,32 @@ +--- +title: "Manifest Version" +--- + + + +# Manifest Version + +**当前 manifest 版本为 `{{ version }}`。** + +--- + +在 `package.json` 中,`manifest_version` 字段用于指定插件使用的 API 的版本号,请与你实际安装的 `paperlib-api` 包的版本保持一致。我们推荐始终使用最新的 API 版本。 + +```json +// package.json +{ + "manifest_version": "x.x.x" +} +`` \ No newline at end of file diff --git a/src/cn/extension-doc/plapi/cache-service.md b/src/cn/extension-doc/plapi/cache-service.md new file mode 100644 index 0000000..f45e3fd --- /dev/null +++ b/src/cn/extension-doc/plapi/cache-service.md @@ -0,0 +1,74 @@ +# CacheService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.cacheService.methodname(...); +``` + +## Avaliable Methods + +### `fullTextFilter` + +```typescript +/** + * Filter the fulltext cache of the provided papers by the given query. + * @param query - The query to filter the fulltext cache by. + * @param paperEntities - The paper entities to filter. + * @returns The filtered paper entities. */ +fullTextFilter(query: string, paperEntities: IPaperEntityCollection): Promise>; +``` + +### `loadThumbnail` + +```typescript +/** + * Get the thumbnail of the paper entity. + * @param paperEntity - The paper entity to get the thumbnail of. + * @returns The thumbnail of the paper entity. */ +loadThumbnail(paperEntity: PaperEntity): Promise; +``` + +### `updateFullTextCache` + +```typescript +/** + * Update the fulltext cache of the provided paper entities. + * @param paperEntities - The paper entities to update the fulltext cache of. + */ +updateFullTextCache(paperEntities: IPaperEntityCollection): Promise; +``` + +### `updateThumbnailCache` + +```typescript +/** + * Update the thumbnail cache + * @param paperEntity - PaperEntity + * @param thumbnailCache - Cache of thumbnail + */ +updateThumbnailCache(paperEntity: PaperEntity, thumbnailCache: ThumbnailCache): Promise; +``` + +### `updateCache` + +```typescript +/** + * Update the cache of the provided paper entities. + * @param paperEntities - The paper entities. + * @returns + */ +updateCache(paperEntities: IPaperEntityCollection): Promise; +``` + +### `delete` + +```typescript +/** + * Delete the cache of the provided paper entity ids. + * @param ids - The ids of the paper entities to delete the cache of. + */ +delete(ids: OID[]): Promise; +``` \ No newline at end of file diff --git a/src/cn/extension-doc/plapi/categorizer-service.md b/src/cn/extension-doc/plapi/categorizer-service.md new file mode 100644 index 0000000..9383974 --- /dev/null +++ b/src/cn/extension-doc/plapi/categorizer-service.md @@ -0,0 +1,112 @@ +# CategorizerService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.categorizerService.methodname(...); +``` + +## Avaliable Methods + +### `load` + +```typescript +/** + * Load categorizers. + * @param type - The type of the categorizer. + * @param sortBy - Sort by + * @param sortOrder - Sort order + * @returns + */ +load(type: CategorizerType, sortBy: string, sortOrder: string): Promise; +``` + +### `loadByIds` + +```typescript +/** + * Load categorizers by ids. + * @param type - The type of the categorizer. + * @param ids - The ids of the categorizers. + * @returns + */ +loadByIds(type: CategorizerType, ids: OID[]): Promise; +``` + +### `create` + +```typescript +/** + * Update a categorizer. + * @param type - The type of categorizer. + * @param categorizer - The categorizer. + * @param parent - The parent categorizer. + * @returns + */ +create(type: CategorizerType, categorizer: Categorizer, parent?: Categorizer): Promise; +``` + +### `delete` + +```typescript +/** + * Delete a categorizer. + * @param type - The type of categorizer. + * @param name - The name of categorizer. + * @param categorizer - The categorizer. + * @returns + */ +delete(type: CategorizerType, ids?: OID[], categorizers?: ICategorizerCollection): Promise; +``` + +### `colorize` + +```typescript +/** + * Colorize a categorizer. + * @param id - The id of the categorizer. + * @param color - The color. + * @param type - The type of the categorizer. + * @returns + */ +colorize(id: OID, color: Colors, type: CategorizerType): Promise; +``` + +### `rename` + +```typescript +/** + * Rename a categorizer. + * @param id - The id of the categorizer. + * @param name - The new name of the categorizer. + * @param type - The type of the categorizer. + * @returns + */ +rename(id: OID, name: string, type: CategorizerType): Promise; +``` + +### `update` + +```typescript +/** + * Update/Insert a categorizer. + * @param type - The type of the categorizer. + * @param categorizer - The categorizer. + * @param parentCategorizer - The parent categorizer to insert. + * @returns + */ +update( + type: CategorizerType, + categorizer: Categorizer, + parentCategorizer?: Categorizer +): Promise; +``` + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| `tagsUpdated` | `{key: 'tagsUpdated'}` | When Tags database are updated | +| `foldersUpdated` | `{key: 'foldersUpdated'}` | When Folders database are updated | \ No newline at end of file diff --git a/src/cn/extension-doc/plapi/command-service.md b/src/cn/extension-doc/plapi/command-service.md new file mode 100644 index 0000000..954a207 --- /dev/null +++ b/src/cn/extension-doc/plapi/command-service.md @@ -0,0 +1,50 @@ +# CommandService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.commandService.methodname(...); +``` + +## Avaliable Methods + +### `getRegisteredCommands` + +```typescript +/** +* Get registered commands. +* @param filter - Filter string +* @returns - Sorted array of filtered commands +*/ +getRegisteredCommands(filter?: string): Promise; +``` + +### `run` + +```typescript +/** +* Run command. +* @param id - Command ID +* @param args - Command arguments +*/ +run(id: string, ...args: any[]): Promise; +``` + +### `registerExternel` + +```typescript +/** +* Register externel command. +* @param command - Externel command +*/ +registerExternel(command: IExternelCommand): () => Promise; +``` + + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| `your-registed-command-event` | `{key: 'your-registed-command-event', value: someargs}` | When Tags database are updated | \ No newline at end of file diff --git a/src/cn/extension-doc/plapi/database-service.md b/src/cn/extension-doc/plapi/database-service.md new file mode 100644 index 0000000..e50db66 --- /dev/null +++ b/src/cn/extension-doc/plapi/database-service.md @@ -0,0 +1,43 @@ +# DatabaseService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.databaseService.methodname(...); +``` + +## Avaliable Methods + +### `initialize` + +```typescript +/** +* Initialize the database. +* @param reinit - Whether to reinitialize the database. */ +initialize(reinit?: boolean): Promise; +``` + +### `pauseSync` + +```typescript +/** +* Pause the synchronization of the cloud database. */ +pauseSync(): Promise; +``` + +### `resumeSync` + +```typescript +/** +* Resume the synchronization of the cloud database. */ +resumeSync(): Promise; +``` + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| `dbInitializing` | `{key: 'dbInitializing'}` | When database is initilizing | +| `dbInitialized` | `{key: 'dbInitialized'}` | When database is initialized | \ No newline at end of file diff --git a/src/cn/extension-doc/plapi/feed-service.md b/src/cn/extension-doc/plapi/feed-service.md new file mode 100644 index 0000000..145d26b --- /dev/null +++ b/src/cn/extension-doc/plapi/feed-service.md @@ -0,0 +1,123 @@ +# FeedService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.feedService.methodname(...); +``` + +## Avaliable Methods + +### `load` + +```typescript +/** + * Load feeds. + * @param sortBy - Sort by. + * @param sortOrder - Sort order. + * @returns Feeds. + */ +load(sortBy: string, sortOrder: string): Promise; +``` + +### `loadEntities` + +```typescript +/** + * Load feed entities from the database. + * @param filter - Filter. + * @param sortBy - Sort by. + * @param sortOrder - Sort order. + * @returns Feed entities. + */ +loadEntities(filter: FeedEntityFilterOptions, sortBy: string, sortOrder: "asce" | "desc"): Promise; +``` + +### `update` + +```typescript +/** + * Update feeds. + * @param feeds - Feeds. + * @returns Updated feeds. + */ +update(feeds: IFeedCollection): Promise; +``` + +### `updateEntities` + +```typescript +/** + * Update feed entities. + * @param feedEntities - Feed entities + * @param ignoreReadState - Ignore read state. Default: false. + * @returns Updated feed entities. + */ +updateEntities(feedEntities: IFeedEntityCollection, ignoreReadState?: boolean): Promise; +``` + +### `create` + +```typescript +/** + * Create feeds. + * @param feeds - Feeds + */ +create(feeds: Feed[]): Promise; +``` + +### `refresh` + +```typescript +/** + * Refresh feeds. + * @param ids - Feed ids + * @param feeds - Feeds + * @returns + */ +refresh(ids?: OID[], feeds?: IFeedCollection): Promise; +``` + +### `colorize` + +```typescript +/** + * Colorize a feed. + * @param color - Color + * @param id - Feed ID + * @param feed - Feed + */ +colorize(color: Colors, id?: OID, feed?: IFeedObject): Promise; +``` + +### `delete` + +```typescript +/** + * Delete a feed. + * @param ids - Feed IDs + * @param feeds - Feeds + */ +delete(ids?: OID[], feeds?: IFeedCollection): Promise; +``` + +### `addToLib` + +```typescript +/** + * Add feed entities to library. + * @param feedEntities - Feed entities + */ +addToLib(feedEntities: IFeedEntityCollection): Promise; +``` + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| `updated` | `{key: 'updated'}` | When Feed database is updated | +| `entitiesUpdated` | `{key: 'entitiesUpdated'}` | When FeedEntity database is initialized | +| `entitiesCount` | `{key: 'entitiesCount', value: count}` | When FeedEntity database count is changed | + diff --git a/src/cn/extension-doc/plapi/file-service.md b/src/cn/extension-doc/plapi/file-service.md new file mode 100644 index 0000000..606e2ad --- /dev/null +++ b/src/cn/extension-doc/plapi/file-service.md @@ -0,0 +1,183 @@ +# FileService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.fileService.methodname(...); +``` + +## Avaliable Methods + + +### `initialize` + +```typescript +/** +* Initialize the file backend. +*/ +initialize(): Promise; +``` + +### `startWatch` + +```typescript +/** + * Start watching file changes. (Only for WebDAV file backend) + */ +startWatch(): Promise; +``` + +### `stopWatch` + +```typescript +/** + * Stop watching file changes. (Only for WebDAV file backend) + */ +stopWatch(): Promise; +``` + +### `check` + +```typescript +/** + * Check if the file backend is available. + * @returns Whether the file backend is available. + */ +check(): Promise; +``` + +### `move` + +``` typescript +/** + * Move files of a paper entity to the library folder + * @param paperEntity - Paper entity to move + * @param moveMain - Move the main file + * @param moveSups - Move the supplementary files + * @returns + */ +move(paperEntity: PaperEntity, moveMain?: boolean, moveSups?: boolean): Promise; +``` + +### `moveFile` + +``` typescript +/** + * Move a file + * @param sourceURL - Source file URL + * @param targetURL - Target file URL + * @returns The target file URL + */ +moveFile(sourceURL: string, targetURL: string): Promise; +``` + +### `remove` + +```typescript +/** + * Remove files of a paper entity + * @param paperEntity - Paper entity to remove + */ +remove(paperEntity: PaperEntity): Promise; +``` + +### `removeFile` + +```typescript +/** + * Remove a file + * @param url - Url of the file to remove + */ +removeFile(url: string): Promise; +``` + +### `listAllFiles` + +```typescript +/** + * List all files in a folder + * @param folderURL - Url of the folder + * @returns List of file names + */ +listAllFiles(folderURL: string): Promise; +``` + +### `locateFileOnWeb` + +```typescript +/** + * Locate the paper files, such as the PDF, of paper entities. + * @param paperEntities - The paper entities. + * @returns The paper entities with the located file URLs. + */ +locateFileOnWeb(paperEntities: PaperEntity[]): Promise; +``` + +### `access` + +```typescript +/** + * Return the real and accessable path of the URL. + * If the URL is a local file, return the path of the file. + * If the URL is a remote file and `download` is `true`, download the file and return the path of the downloaded file. + * If the URL is a web URL, return the URL. + * @param url + * @param download + * @returns The real and accessable path of the URL. + */ +access(url: string, download: boolean): Promise; +``` + +### `open` + +```typescript +/** + * Open the URL. + * @param url - URL to open + */ +open(url: string): Promise; +``` + +### `showInFinder` + +```typescript +/** + * Show the URL in Finder / Explorer. + * @param url - URL to show + */ +showInFinder(url: string): Promise; +``` + +### `preview` + +```typescript +/** + * Preview the URL only for MacOS. + * Other platforms should install an extension. + * @param url - URL to preview + */ +preview(url: string): Promise; + +``` + +### `inferRelativeFileName` +```typescript +/** + * Infer the relative path of a paper entity. + * @param paperEntity - Paper entity to infer the relative path + */ +inferRelativeFileName(paperEntity: PaperEntity): Promise; +``` + + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| `backend` | `{key: 'backend', value: backendName}` | When file backend is changed | +| `available` | `{key: 'available', value: available}` | When file backend available status is changed | +| `backendInitializing` | `{key: 'backendInitializing'}` | When file backend is initializing | +| `backendInitialized` | `{key: 'backendInitialized'}` | When file backend is initialized | + diff --git a/src/cn/extension-doc/plapi/hook-service.md b/src/cn/extension-doc/plapi/hook-service.md new file mode 100644 index 0000000..87473e2 --- /dev/null +++ b/src/cn/extension-doc/plapi/hook-service.md @@ -0,0 +1,49 @@ +# HookService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.hookService.methodname(...); +``` + +## Avaliable Methods + +### `hasHook` + +```typescript +/** + * Check if a hook point exists. + * @param hookName - Name of the hook point + * @returns Whether the hook point exists + */ +hasHook(hookName: string): false | "modify" | "transform"; +``` + +### `hookModify` + +```typescript +/** + * Hook a modify hook point. + * @param hookName - Name of the hook point + * @param extensionID - ID of the extension + * @param callbackName - Name of the callback function + * @returns A function to dispose the hook + */ +hookModify(hookName: string, extensionID: string, callbackName: string): () => void; +``` + +### `hookTransform` + +```typescript +/** + * Hook a transform hook point. + * @param hookName - Name of the hook point + * @param extensionID - ID of the extension + * @param callbackName - Name of the callback function + * @returns A function to dispose the hook + */ +hookTransform(hookName: string, extensionID: string, callbackName: string): () => void; +``` + diff --git a/src/cn/extension-doc/plapi/index.md b/src/cn/extension-doc/plapi/index.md new file mode 100644 index 0000000..85b9982 --- /dev/null +++ b/src/cn/extension-doc/plapi/index.md @@ -0,0 +1,30 @@ +# PLAPI + +这组 API 中,包含了大部分在渲染进程里面的服务和他们的方法。 + +在插件中,可以直接使用: + +```typescript +import { PLAPI } from "paperlib-api/api"; + +const results = await PLAPI.serviceName.methodName(...) +``` + +## 可用的服务 + +- `logService`:日志服务,用于记录信息、警告、日志。可以在左下角通知中心弹出通知告知用户。 +- `cacheService`:缓存服务,用于缓存一些数据,例如论文的全文、缩略图等。 +- `categorizerService`:标签和组的服务,用于管理标签和组。 +- `commandService`:命令服务,用于注册和执行命令。 +- `databaseService`:数据库服务,用于初始化数据库等。 +- `feedService`:RSS 服务,用于操作 RSS 相关的内容。 +- `fileService`:文件服务,用于操作文件。 +- `hookService`:钩子服务,用于注册和执行钩子。 +- `paperService`:论文服务,用于操作论文。 +- `referenceService`:引用服务,用于导出引用等。 +- `renderService`:渲染服务,用于渲染 PDF,markdown 等。 +- `scrapeService`:搜寻服务,用于转换数据源到`PaperEntity`,搜寻论文元数据。 +- `smartFilterService`:智能过滤器服务,用于操作智能过滤器。 +- `uiStateService`:UI 状态服务,用于操作 UI 状态。 +- `preferenceService`:设置服务,用于操作设置。 +- `uiSlotService`:UI 插槽服务,用于操作 UI 插槽。 diff --git a/src/cn/extension-doc/plapi/log-service.md b/src/cn/extension-doc/plapi/log-service.md new file mode 100644 index 0000000..a8663b6 --- /dev/null +++ b/src/cn/extension-doc/plapi/log-service.md @@ -0,0 +1,90 @@ +# LogService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.logService.methodname(...); +``` + +## Avaliable Methods + +### `log` + +```typescript +/** + * Log info to the console and the log file. + * @param {string} level - Log level + * @param {string} msg - Message to log + * @param {string?} additional - Additional message to log + * @param {boolean?} notify - Show notification in the notification bar, default: false + * @param {string?} id - ID of the log, usually indicates who log this info */ +log(level: "info" | "warn" | "error", msg: string, additional?: string, notify?: boolean, id?: string): Promise; +``` + +### `info` + +```typescript +/** + * Log info to the console and the log file. + * @param {string} msg - Message to log + * @param {string?} additional - Additional message to log + * @param {boolean?} notify - Show notification, default: false + * @param {string?} id - ID of the log */ +info(msg: string, additional?: string, notify?: boolean, id?: string): Promise; +``` + +### `warn` + +```typescript +/** + * Log warning to the console and the log file. + * @param {string} msg - Message to log + * @param {string?} additional - Additional message to log + * @param {boolean?} notify - Show notification, default: false + * @param {string?} id - ID of the log */ +warn(msg: string, additional?: string, notify?: boolean, id?: string): Promise; +``` + +### `error` +```typescript +/** + * Log error to the console and the log file. + * @param {string} msg - Message to log + * @param {string?} additional - Additional message to log + * @param {boolean?} notify - Show notification, default: false + * @param {string?} id - ID of the log */ +error(msg: string, additional?: string | Error, notify?: boolean, id?: string): Promise; +``` + +### `progress` + +```typescript +/** + * Log progress to the console and the log file. + * @param {string} msg - Message to log + * @param {number?} value - Progress value + * @param {boolean?} notify - Show notification, default: false + * @param {string?} id - ID of the log */ +progress(msg: string, value: number, notify?: boolean, id?: string, progressId?: string): Promise; +``` + +### `getLogFilePath` + +```typescript +/** + * Get log file path. + * @returns {string} Log file path */ +getLogFilePath(): Promise; +``` + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| `infoLogMessage` | `{key: 'infoLogMessage', value: msg}` | When a new info message is logged | +| `warnLogMessage` | `{key: 'warnLogMessage', value: msg}` | When a new warning message is logged | +| `errorLogMessage` | `{key: 'errorLogMessage', value: msg}` | When a new error message is logged | +| `progressLogMessage` | `{key: 'progressLogMessage', value: percent}` | When a new progress is logged | + diff --git a/src/cn/extension-doc/plapi/paper-service.md b/src/cn/extension-doc/plapi/paper-service.md new file mode 100644 index 0000000..1dca8f6 --- /dev/null +++ b/src/cn/extension-doc/plapi/paper-service.md @@ -0,0 +1,168 @@ +# PaperService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.paperService.methodname(...); +``` + +## Avaliable Methods + +### `load` + +```typescript +/** + * Load paper entities with filter and sort. + * @param querySentence - Query sentence, string or PaperFilterOptions + * @param sortBy - Sort by + * @param sortOrder - Sort order + * @returns Paper entities + */ +load(querySentence: string, sortBy: string | undefined, sortOrder: "asce" | "desc", fulltextQuerySentence?: string): Promise; +``` + +### `loadByIds` + +```typescript +/** + * Load paper entities by IDs. + * @param ids - Paper entity ids + * @returns Paper entities + */ +loadByIds(ids: OID[]): Promise>>; +``` + +### `update` + +```typescript +/** + * Update paper entities. + * @param paperEntityDrafts - paper entity drafts + * @param updateCache - Update cache, default is true + * @param isUpdate - Is update, default is false, if false, it is insert. This is for preventing insert duplicated papers. + * @returns Updated paper entities + */ +update(paperEntityDrafts: IPaperEntityCollection, updateCache?: boolean, isUpdate?: boolean): Promise; +``` + +### `updateWithCategorizer` + +```typescript +/** + * Update paper entities with a categorizer. + * @param ids - The list of paper IDs. + * @param categorizer - The categorizer. + * @param type - The type of the categorizer. + */ +updateWithCategorizer(ids: OID[], categorizer: Categorizer, type: CategorizerType): Promise; +``` + +### `updateMainURL` + +```typescript +/** + * Update the main file of a paper entity. + * @param paperEntity - The paper entity. + * @param url - The URL of the main file. + * @returns The updated paper entity. + */ +updateMainURL(paperEntity: PaperEntity, url: string): Promise; +``` + +### `updateSupURLs` + +```typescript +/** + * Update the supplementary files of a paper entity. + * @param paperEntity - The paper entity. + * @param urls - The URLs of the supplementary files. + */ +updateSupURLs(paperEntity: PaperEntity, urls: string[]): Promise; + +``` + +### `delete` + +```typescript +/** + * Delete paper entities. + * @param ids - Paper entity ids + * @param paperEntity - Paper entities + */ +delete(ids?: OID[], paperEntities?: PaperEntity[]): Promise; +``` + +### `deleteSup` + +```typescript +/** + * Delete a suplementary file. + * @param paperEntity - The paper entity. + * @param url - The URL of the supplementary file. + */ +deleteSup(paperEntity: PaperEntity, url: string): Promise; +``` + +### `create` + +```typescript +/** + * Create paper entity from file URLs. + * @param urlList - The list of URLs. + * @returns The list of paper entity drafts. + */ +create(urlList: string[]): Promise; +``` + +### `createIntoCategorizer` + +```typescript +/** + * Create paper entity from a URL with a given categorizer. + * @param urlList - The list of URLs. + * @param categorizer - The categorizer. + * @param type - The type of categorizer. + * @returns The list of paper entity drafts. + */ +createIntoCategorizer(urlList: string[], categorizer: Categorizer, type: CategorizerType): Promise; +``` + +### `scrape` + +```typescript +/** + * Scrape paper entities. + * @param paperEntities - The list of paper entities. + * @param specificScrapers - The list of specific scrapers. + */ +scrape(paperEntities: IPaperEntityCollection, specificScrapers?: string[]): Promise; +``` + +### `scrapePreprint` + +```typescript +/** + * Scrape preprint paper entities. + */ +scrapePreprint(): Promise; +``` + +### `renameAll` + +```typescript +/** + * Rename all paper entities. + */ +renameAll(): Promise; +``` + + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| `updated` | `{key: 'updated'}` | When PaperEntity database is updated | +| `count` | `{key: 'count', value: count | When FeedEntity database count is changed | + diff --git a/src/cn/extension-doc/plapi/preference-service.md b/src/cn/extension-doc/plapi/preference-service.md new file mode 100644 index 0000000..e18fb04 --- /dev/null +++ b/src/cn/extension-doc/plapi/preference-service.md @@ -0,0 +1,133 @@ +# PreferenceService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.preferenceService.methodname(...); +``` + +## Avaliable Methods + +### `get` + +```typescript +/** + * Get the value of the preference + * @param key - Key of the preference + * @returns Value of the preference + */ +get(key: keyof IPreferenceStore): Promise; +``` + +### `set` + +```typescript +/** + * Set the value of the preference + * @param patch - Patch object + */ +set(patch: Partial): Promise; +``` + +### `getPassword` + +```typescript +/** + * Get the password + * @param key - Key of the password + * @returns Password + */ +getPassword(key: string): Promise; +``` + +### `setPassword` + +```typescript +/** + * Set the password + * @param key - Key of the password + * @param pwd - Password + */ +setPassword(key: string, pwd: string): Promise; +``` + + +## Avaliable Preferences + +```typescript +declare interface IPreferenceStore { + preferenceVersion: number; + windowSize: { + height: number; + width: number; + }; + appLibFolder: string; + sourceFileOperation: "cut" | "copy" | "link"; + showSidebarCount: boolean; + isSidebarCompact: boolean; + mainTableFields: IDataViewField[]; + feedFields: IDataViewField[]; + preferedTheme: "light" | "dark" | "system"; + invertColor: boolean; + sidebarSortBy: "name" | "count" | "color"; + sidebarSortOrder: "asce" | "desc"; + renamingFormat: "full" | "short" | "authortitle" | "custom"; + customRenamingFormat: string; + language: string; + enableExportReplacement: boolean; + exportReplacement: Array<{ + from: string; + to: string; + }>; + useSync: boolean; + syncCloudBackend: string; + isFlexibleSync: boolean; + syncAPPID: ""; + syncAPIKey: string; + syncEmail: string; + syncFileStorage: string; + webdavURL: string; + webdavUsername: string; + webdavPassword: string; + allowRoutineMatch: boolean; + lastRematchTime: number; + lastFeedRefreshTime: number; + allowproxy: boolean; + httpproxy: string; + httpsproxy: string; + lastVersion: string; + lastDBVersion: number; + shortcutPlugin: string; + shortcutPreview: string; + shortcutOpen: string; + shortcutCopy: string; + shortcutScrape: string; + shortcutEdit: string; + shortcutFlag: string; + shortcutCopyKey: string; + shortcutDelete: string; + sidebarWidth: number; + detailPanelWidth: number; + mainviewSortBy: string; + mainviewSortOrder: "desc" | "asce"; + mainviewType: string; + mainviewShortAuthor: boolean; + pluginLinkedFolder: string; + selectedPDFViewer: string; + selectedPDFViewerPath: string; + selectedCSLStyle: string; + importedCSLStylesPath: string; + showPresetting: boolean; + fontsize: "normal" | "large" | "larger"; +} + +``` + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| any preference key listed above | `{key: prefKey, value: newValue}` | When preference is changed | + diff --git a/src/cn/extension-doc/plapi/reference-service.md b/src/cn/extension-doc/plapi/reference-service.md new file mode 100644 index 0000000..6d3b758 --- /dev/null +++ b/src/cn/extension-doc/plapi/reference-service.md @@ -0,0 +1,116 @@ +# ReferenceService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.referenceService.methodname(...); +``` + +## Avaliable Methods + +### `replacePublication` + +```typescript +/** + * Abbreviate the publication name according to the abbreviation list set in the preference interface. + * @param source - The source paper entity. + * @returns The paper entity with publication name abbreviated. + */ +replacePublication(source: PaperEntity): PaperEntity; +``` + +### `toCite` + +```typescript +/** + * Convert paper entity to citationjs object. + * @param source - The source paper entity. + * @returns The cite object. + */ +toCite(source: PaperEntity | PaperEntity[] | string): any; +``` + +### `exportBibTexKey` + +```typescript +/** + * Export BibTex key. + * @param paperEntities - The paper entities. + * @returns The BibTex key. + */ +exportBibTexKey(paperEntities: PaperEntity[]): string; +``` + +### `exportBibTexBody` + +```typescript +/** + * Export BibTex body string. + * @param paperEntities - The paper entities. + * @returns The BibTex body string. + */ +exportBibTexBody(paperEntities: PaperEntity[]): string; +``` + +### `exportBibTex` + +```typescript +/** + * Export plain text. + * @param paperEntities - The paper entities. + * @returns The plain text. + */ +exportPlainText(paperEntities: PaperEntity[]): Promise; +``` + +### `exportCSV` +```typescript +/** + * Export papers as csv string. + * @param paperEntities - The paper entities. + * @returns The CSV string. + */ +exportCSV(paperEntities: PaperEntity[]): Promise; +``` + +### `exportBibTexKeyInFolder` +```typescript +/** + * Export BibTex body string in folder. + * @param folderName - The folder name. + */ +exportBibTexBodyInFolder(folderName: string): Promise; +``` + +### `exportBibTexBodyInFolder` +```typescript +/** + * Export plain text in folder. + * @param folderName - The folder name. + */ +exportPlainTextInFolder(folderName: string): Promise; +``` + +### `exportBibItem` +```typescript +/** + * Export BibItem. + * @param paperEntities - The paper entities. + * @returns The BibItem. + */ +exportBibItem(paperEntities: PaperEntity[]): Promise; +``` + + +### `export` + +```typescript +/** + * Export paper entities. + * @param paperEntities - The paper entities. + * @param format - The export format: "BibTex" | "BibTex-Key" | "PlainText" + */ +export(paperEntities: PaperEntity[], format: string): Promise; +``` \ No newline at end of file diff --git a/src/cn/extension-doc/plapi/render-service.md b/src/cn/extension-doc/plapi/render-service.md new file mode 100644 index 0000000..08b6100 --- /dev/null +++ b/src/cn/extension-doc/plapi/render-service.md @@ -0,0 +1,80 @@ +# RenderService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.renderService.methodname(...); +``` + +## Avaliable Methods + + +### `renderPDF` + +```typescript +/** + * Render PDF file to canvas + * @param fileURL - File url + * @param canvasId - Canvas id + * @returns Renderer blob: {blob: ArrayBuffer | null, width: number, height: number} + */ +renderPDF(fileURL: string, canvasId: string): Promise<{ + blob: ArrayBuffer | null; + width: number; + height: number; +}>; +``` + +### `renderPDFCache` + +```typescript +/** + * Render PDF cache to canvas + * @param cachedThumbnail - Cached thumbnail + * @param canvasId - Canvas id + */ +renderPDFCache(cachedThumbnail: ThumbnailCache, canvasId: string): Promise; +``` + +### `renderMarkdown` + +```typescript +/** + * Render Markdown to HTML + * @param content - Markdown content + * @param renderFull - Render full content or not, default is false. If false, only render first 10 lines. + * @returns Rendered string: {renderedStr: string, overflow: boolean} + */ +renderMarkdown(content: string, renderFull?: boolean): Promise<{ + renderedStr: string; + overflow: boolean; +}>; +``` + +### `renderMarkdownFile` + +```typescript +/** + * Render Markdown file to HTML + * @param url - File url + * @param renderFull - Render full content or not, default is false. If false, only render first 10 lines. + * @returns Rendered string: {renderedStr: string, overflow: boolean} + */ +renderMarkdownFile(url: string, renderFull?: boolean): Promise<{ + renderedStr: string; + overflow: boolean; +}>; +``` + +### `renderMath` + +```typescript +/** + * Render Math to HTML + * @param content - Math content + * @returns Rendered HTML string + */ +renderMath(content: string): Promise; +``` \ No newline at end of file diff --git a/src/cn/extension-doc/plapi/scrape-service.md b/src/cn/extension-doc/plapi/scrape-service.md new file mode 100644 index 0000000..46bbef2 --- /dev/null +++ b/src/cn/extension-doc/plapi/scrape-service.md @@ -0,0 +1,45 @@ +# ScrapeService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.scrapeService.methodname(...); +``` + +## Avaliable Methods + +### `scrape` + +```typescript +/** + * Scrape a data source's metadata. + * @param payloads - data source payloads. + * @param specificScrapers - list of metadata scrapers. + * @param force - force scraping metadata. + * @returns List of paper entities. */ +scrape(payloads: any[], specificScrapers: string[], force?: boolean): Promise; +``` + +### `scrapeEntry` + +```typescript +/** + * Scrape all entry scrapers to transform data source payloads into a PaperEntity list. + * @param payloads - data source payloads. + * @returns List of paper entities. */ +scrapeEntry(payloads: any[]): Promise; +``` + +### `scrapeMetadata` + +```typescript +/** + * Scrape all metadata scrapers to complete the metadata of PaperEntitys. + * @param paperEntityDrafts - list of paper entities. + * @param scrapers - list of metadata scrapers. + * @param force - force scraping metadata. + * @returns List of paper entities. */ +scrapeMetadata(paperEntityDrafts: PaperEntity[], scrapers: string[], force?: boolean): Promise; +``` \ No newline at end of file diff --git a/src/cn/extension-doc/plapi/smartfilter-service.md b/src/cn/extension-doc/plapi/smartfilter-service.md new file mode 100644 index 0000000..8c487a5 --- /dev/null +++ b/src/cn/extension-doc/plapi/smartfilter-service.md @@ -0,0 +1,92 @@ +# SmartFilterService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.smartFilterService.methodname(...); +``` + +## Avaliable Methods + +### `load` + +```typescript +/** + * Load smartfilters. + * @param type - The type of the smartfilter + * @param sortBy - Sort by + * @param sortOrder - Sort order + * @returns + */ +load(type: PaperSmartFilterType, sortBy: string, sortOrder: string): Promise; +``` + +### `loadByIds` + +```typescript +/** + * Load smartfilters by ids. + * @param ids - The ids of the smartfilters + * @returns + */ +loadByIds(ids: OID[]): Promise; +``` + +### `delete` + +```typescript +/** + * Delete a smartfilter. + * @param type - The type of the smartfilter + * @param ids - The ids of the smartfilters + * @param smartfilters - The smartfilters + */ +delete(type: PaperSmartFilterType, ids?: OID[], smartfilters?: IPaperSmartFilterCollection): Promise; +``` + +### `colorize` + +```typescript +/** + * Colorize a smartfilter. + * @param id - The id of the smartfilter. + * @param color - The color. + * @param type - The type of the smartfilter. + * @returns + */ +colorize(id: OID, color: Colors, type: PaperSmartFilterType): Promise; +``` + +### `rename` +```typescript + /** + * Rename a smartfilter. + * @param id - The id of the smartfilter. + * @param name - The new name of the smartfilter. + * @param type - The type of the smartfilter. + * @returns + */ +rename(id: OID, name: string, type: PaperSmartFilterType): Promise; +``` + +### `update` +```typescript +/** + * Update/Insert a smartfilter. + * @param type - The type of the smartfilter + * @param smartfilter - The smartfilter + * @param parentSmartfilter - The parent smartfilter + * @returns + */ +update(type: PaperSmartFilterType, smartfilter: PaperSmartFilter, parentSmartfilter?: PaperSmartFilter): Promise; +``` + + + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| `updated` | `{key: updated}` | When PaperSmartFilter database is updated | diff --git a/src/cn/extension-doc/plapi/uislot-service.md b/src/cn/extension-doc/plapi/uislot-service.md new file mode 100644 index 0000000..2d4b957 --- /dev/null +++ b/src/cn/extension-doc/plapi/uislot-service.md @@ -0,0 +1,56 @@ +# UISlotService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.uiSlotService.methodname(...); +``` + +## Avaliable Methods + +```typescript +/** + * Update a slot with the given patch + * @param slotID - The slot to update + * @param patch - The patch to apply to the slot + * @returns + */ +updateSlot(slotID: keyof IUISlotState, patch: { + [id: string]: any; +}): Promise; +``` + +## Avaliable Slots + +```typescript + +interface IUISlotState { + paperDetailsPanelSlot1: { + [id: string]: { title: string; content: string }; + }; + paperDetailsPanelSlot2: { + [id: string]: { title: string; content: string }; + }; + paperDetailsPanelSlot3: { + [id: string]: { title: string; content: string }; + }; + overlayNotifications: { + [id: string]: { title: string; content: string }; + }; +} + +All support HTML string. + +``` + + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| `paperDetailsPanelSlot1` | `{key: paperDetailsPanelSlot1, value: newSlotState}` | When slot `paperDetailsPanelSlot1` is updated | +| `paperDetailsPanelSlot2` | `{key: paperDetailsPanelSlot2, value: newSlotState}` | When slot `paperDetailsPanelSlot2` is updated | +| `paperDetailsPanelSlot3` | `{key: paperDetailsPanelSlot3, value: newSlotState}` | When slot `paperDetailsPanelSlot3` is updated | +| `overlayNotifications` | `{key: overlayNotifications, value: newSlotState}` | When slot `overlayNotifications` is updated | \ No newline at end of file diff --git a/src/cn/extension-doc/plapi/uistate-service.md b/src/cn/extension-doc/plapi/uistate-service.md new file mode 100644 index 0000000..37b515f --- /dev/null +++ b/src/cn/extension-doc/plapi/uistate-service.md @@ -0,0 +1,103 @@ +# UIStateService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.uiStateService.methodname(...); +``` + +## Avaliable Methods + +### `setState` + +```typescript +/** + * Set the state of the UI service. Many UI components are controlled by the UI states. + * @param patch - patch to the state. It can be a single state, a partial state or a full state. + */ +setState(patch: Partial): Promise; +``` + +### `getState` + +```typescript +/** + * Get the UI state. + * @param stateKey - key of the state + * @returns The state + */ +getState(stateKey: keyof IUIStateServiceState): Promise; +``` + +### `getStates` + +```typescript +/** + * Get all UI states. + * @returns The state + */ +getStates(): Promise>; +``` + +### `resetState` + +```typescript +/** + * Reset all UI states to default. + */ +resetStates(): Promise; +``` + +## Avaliable States + +```typescript + +interface IUIStateServiceState { + // ========================================= + // Main Paper/Feed panel + contentType: string; // "library" | "feed" + mainViewFocused: boolean; + editViewShown: boolean; + feedEditViewShown: boolean; + paperSmartFilterEditViewShown: boolean; + preferenceViewShown: boolean; + deleteConfirmShown: boolean; + overlayNoticationShown: boolean; + renderRequired: number; // When assign a new value to this state, the rendering of some components, such as the PDF preview, will be triggered. + + entitiesReloaded: number; + + // selectedIndex: contains the index of the selected papers in the dataview. + // It should be the only state that is used to control the selection. + selectedIndex: Array; + // selectedIds: contains the ids of the selected papers in the current dataview. + // It can be accessed in any component. But it is read-only. It can be only changed by the event listener of selectedIndex in the dataview. + selectedIds: Array; + // selectedPaperEntities/selectedFeedEntities: contains the selected paper/feed entities in the current dataview. + // It can be accessed in any component. But it is read-only. It can be only changed by the event listener of selectedIndex in the dataview. + selectedPaperEntities: Array; + selectedFeedEntities: Array; + selectedQuerySentenceId: string; + selectedFeed: string; + editingPaperSmartFilter: PaperSmartFilter; + querySentenceSidebar: string; + querySentenceCommandbar: string; + + dragingIds: Array; + + // ========================================= + // Command / Search Bar + commandBarText: string; + commandBarSearchMode: string; // "general" | "advanced" | "fulltext" +} + +``` + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| any state key listed above | `{key: stateKey, value: newValue}` | When state is changed | + diff --git a/src/cn/extension-doc/plextapi/extensionmanagement-service.md b/src/cn/extension-doc/plextapi/extensionmanagement-service.md new file mode 100644 index 0000000..4d943d6 --- /dev/null +++ b/src/cn/extension-doc/plextapi/extensionmanagement-service.md @@ -0,0 +1,106 @@ +# ExtensionManagementService + +## Call + +```typescript +import { PLExtAPI } from "paperlib-api/api"; + +PLExtAPI.extensionManagementService.methodname(...); +``` + +## Avaliable Methods + +### `loadInstalledExtensions` +```typescript +/** + * Load all installed extensions. + */ +loadInstalledExtensions(): Promise; +``` + +### `install` +```typescript +/** + * Install an extension from the given path or extensionID. + * @param extensionIDorPath - extensionID or path to the extension + * @param notify - whether to show notification, default to true + */ +install(extensionIDorPath: string, notify?: boolean): Promise; +``` + +### `uninstall` +```typescript +/** + * Uninstall an extension. + * @param extensionID - extensionID to uninstall + */ +uninstall(extensionID: string): Promise; +``` + +### `clean` +```typescript +/** + * Clean the extension related files, preference, etc. + * @param extensionIDorPath - extensionID or path to the extension + */ +clean(extensionIDorPath: string): Promise; +``` + +### `reload` +```typescript +/** + * Reload an extension. + * @param extensionID - extensionID to reload + */ +reload(extensionID: string): Promise; +``` + +### `reloadAll` +```typescript +/** + * Reload all installed extensions. + */ +reloadAll(): Promise; +``` + +### `installedExtensions` +```typescript +/** + * Get all installed extensions. + */ +installedExtensions(): Promise<{ + [key: string]: IExtensionInfo; +}>; +``` + +### `listExtensionMarketplace` + +```typescript +/** + * Get extensions from marketplace. + * @param query - Query string + * @returns A map of extensionID to extension info. + */ +listExtensionMarketplace(query: string): Promise<{ + [id: string]: { + id: string; + name: string; + version: string; + author: string; + verified: boolean; + description: string; + }; +}>; +``` + +### `callExtensionMethod` +```typescript +/** + * Call a method of an extension class. + * @param extensionID - extensionID to call method + * @param methodName - method name to call + * @param args - arguments to pass to the method + * @returns + */ +callExtensionMethod(extensionID: string, methodName: string, ...args: any): Promise; +``` \ No newline at end of file diff --git a/src/cn/extension-doc/plextapi/extensionpreference-service.md b/src/cn/extension-doc/plextapi/extensionpreference-service.md new file mode 100644 index 0000000..fc685ec --- /dev/null +++ b/src/cn/extension-doc/plextapi/extensionpreference-service.md @@ -0,0 +1,133 @@ +# ExtensionPreferenceService + +## Call + +```typescript +import { PLExtAPI } from "paperlib-api/api"; + +PLExtAPI.extensionPreferenceService.methodname(...); +``` + +## Avaliable Methods + +### `register` + +```typescript +/** + * Register a preference store. + * @param extensionID - extension ID + * @param defaultPreference - default preference + */ +register(extensionID: string, defaultPreference: T): Promise; +``` + +### `unregister` + +```typescript +/** + * Unregister a preference store. + * @param extensionID - extension ID + */ +unregister(extensionID: string): Promise; +``` + +### `get` + +```typescript +/** + * Get the value of the preference + * @param extensionID - extension ID + * @param key - key of the preference + * @returns value of the preference or null + */ +get(extensionID: string, key: any): Promise; +``` + +### `getAll` + +```typescript +/** + * Get the value of all preferences + * @param extensionID - extension ID + * @returns value of all preferences + */ +getAll(extensionID: string): Promise>; +``` + +### `getMetadata` + +```typescript +/** + * Get the metadata of the preference + * @param extensionID - extension ID + * @param key - key of the preference + * @returns metadata of the preference or null + */ +getMetadata(extensionID: string, key: any): Promise; +``` + +### `getAllMetadata` + +```typescript +/** + * Get the metadata of all preferences + * @param extensionID - extension ID + * @returns metadata of all preferences + */ +getAllMetadata(extensionID: string): Promise>; +``` + +### `set` + +```typescript +/** + * Set the value of the preference + * @param extensionID - extension ID + * @param patch - patch object + * @returns + */ +set(extensionID: string, patch: any): Promise; +``` + +### `getPassword` + +```typescript +/** + * Get the password + * @param extensionID - extension ID + * @param key - key of the password + * @returns - password + */ +getPassword(extensionID: string, key: string): Promise; +``` + +### `setPassword` + +```typescript +/** + * Set the password + * @param extensionID - extension ID + * @param key - key of the password + * @param pwd - password + */ +setPassword(extensionID: string, key: string, pwd: string): Promise; +``` + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| `extensionID:prefKey` | `{key: 'extensionID:prefKey', value: prefValue}` | When the preference is changed | + +## Example + +```typescript +import { PLExtAPI } from "paperlib-api/api"; + +PLExtAPI.extensionPreferenceService.onChanged( + 'extensionID:prefKey', + (newValue: {key: string, value: any}) => { + console.log(newValue.value); +}); \ No newline at end of file diff --git a/src/cn/extension-doc/plextapi/index.md b/src/cn/extension-doc/plextapi/index.md new file mode 100644 index 0000000..aef040e --- /dev/null +++ b/src/cn/extension-doc/plextapi/index.md @@ -0,0 +1,20 @@ +# PLExtAPI + +这组 API 中,包含了大部分在插件进程里面的服务和他们的方法。 + +因为插件和 `PLExtAPI` 中的服务运行在同一个进程中,所以有的方法是同步的,有的是异步的。具体请在使用时参考类型提示。 + +在插件中,可以直接使用: + +```typescript +import { PLExtAPI } from "paperlib-api/api"; + +const syncResults = PLExtAPI.serviceName.methodName(...) +const asyncResults = await PLExtAPI.serviceName.methodName(...) +``` + +## 可用的服务 + +- `extensionManagementService`:插件管理服务,负责插件安装、载入、卸载等。 +- `extensionPreferenceService`:插件偏好设置服务,负责插件偏好设置的读取和写入。 +- `networkTool`:网络工具,提供 `get`,`post`,`download` 等方法。 \ No newline at end of file diff --git a/src/cn/extension-doc/plextapi/network-tool.md b/src/cn/extension-doc/plextapi/network-tool.md new file mode 100644 index 0000000..8a4db9f --- /dev/null +++ b/src/cn/extension-doc/plextapi/network-tool.md @@ -0,0 +1,129 @@ +# NetworkTool + +## Call + +```typescript +import { PLExtAPI } from "paperlib-api/api"; + +PLExtAPI.networkTool.methodname(...); +``` + +## Avaliable Methods + +### `setProxyAgent` + +```typescript +/** + * Set proxy agent + * @param httpproxy - HTTP proxy + * @param httpsproxy - HTTPS proxy + */ +setProxyAgent(httpproxy?: string, httpsproxy?: string): void; +``` + +### `checkSystemProxy` + +```typescript +/** + * Check system proxy, if exists, set it as proxy agent + */ +checkProxy(): Promise; +``` + +### `get` + +```typescript +/** + * HTTP GET + * @param url - URL + * @param headers - Headers + * @param retry - Retry times + * @param timeout - Timeout + * @param cache - Use cache + * @param parse - Try to parse response body + * @returns Response + */ +get(url: string, headers?: Record, retry?: number, timeout?: number, cache?: boolean, parse?: boolean): Promise<{ + body: any; + status: number; + statusText: string; + headers: Record; +}>; +``` + +### `post` + +```typescript +/** + * HTTP POST + * @param url - URL + * @param data - Data + * @param headers - Headers + * @param retry - Retry times + * @param timeout - Timeout + * @param compress - Compress data + * @param parse - Try to parse response body + * @returns Response + */ +post(url: string, data: Record | string, headers?: Record, retry?: number, timeout?: number, compress?: boolean, parse?: boolean): Promise<{ + body: any; + status: number; + statusText: string; + headers: Record; +}>; +``` + +### `postForm` + +```typescript +/** + * HTTP POST with form data + * @param url - URL + * @param data - Data + * @param headers - Headers + * @param retry - Retry times + * @param timeout - Timeout + * @returns Response + */ +postForm(url: string, data: FormData, headers?: Record, retry?: number, timeout?: number, parse?: boolean): Promise<{ + body: any; + status: number; + statusText: string; + headers: Record; +}>; +``` + +### `download` + +```typescript +/** + * Download + * @param url - URL + * @param targetPath - Target path + * @param cookies - Cookies + * @returns Target path + */ +download(url: string, targetPath: string, cookies?: CookieJar | ICookieObject[]): Promise; +``` + +### `downloadPDFs` + +```typescript +/** + * Download PDFs + * @param urlList - URL list + * @param cookies - Cookies + * @returns Target paths + */ +downloadPDFs(urlList: string[], cookies?: CookieJar | ICookieObject[]): Promise; +``` + +### `connected` + +```typescript +/** + * Check if the network is connected + * @returns Whether the network is connected + */ +connected(): Promise; +``` \ No newline at end of file diff --git a/src/cn/extension-doc/plmainapi/contextmenu-service.md b/src/cn/extension-doc/plmainapi/contextmenu-service.md new file mode 100644 index 0000000..743665d --- /dev/null +++ b/src/cn/extension-doc/plmainapi/contextmenu-service.md @@ -0,0 +1,82 @@ +# ContextMenuService + +## Call + +```typescript +import { PLMainAPI } from "paperlib-api/api"; + +PLMainAPI.contextMenuService.methodname(...); +``` + +## Avaliable Methods + +### `registerScraperExtension` +```typescript +/** + * Registers a scraper extension. It will be shown in the context menu. + */ +registerScraperExtension(extID: string, scrapers: { + [id: string]: string; +}): Promise; +``` + +### `unregisterScraperExtension` +```typescript +/** + * Unregisters a scraper extension. + * @param {string} extID - The ID of the extension. + */ +unregisterScraperExtension(extID: string): Promise; +``` + +### `registerContextMenu` +```typescript +/** + * Registers context menus form extensions. + * @param extID - The id of the extension to register menus + * @param items - The menu items to be registered + */ +registerContextMenu(extID: string, items: { + id: string; + label: string; +}[]): void; +``` + +### `unregisterContextMenu` +```typescript +/** + * Registers context menus form extensions. + * @param extID - The id of the extension to unregister menu items + */ +unregisterContextMenu(extID: string): void; +``` + + + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| `dataContextMenuScrapeFromClicked` | `{key: 'dataContextMenuScrapeFromClicked', value: scraperID}` | When `Scrape From` is clicked in the context menu of a paper in the library | +| `dataContextMenuOpenClicked` | `{key: 'dataContextMenuOpenClicked'}` | When `Open` is clicked in the context menu of a paper in the library | +| `dataContextMenuShowInFinderClicked` | `{key: 'dataContextMenuShowInFinderClicked'}` | When `Show in Finder` is clicked in the context menu of a paper in the library | +| `dataContextMenuEditClicked` | `{key: 'dataContextMenuEditClicked'}` | When `Edit` is clicked in the context menu of a paper in the library | +| `dataContextMenuScrapeClicked` | `{key: 'dataContextMenuScrapeClicked'}` | When `Scrape` is clicked in the context menu of a paper in the library | +| `dataContextMenuDeleteClicked` | `{key: 'dataContextMenuDeleteClicked'}` | When `Delete` is clicked in the context menu of a paper in the library | +| `dataContextMenuFlagClicked` | `{key: 'dataContextMenuFlagClicked'}` | When `Flag` is clicked in the context menu of a paper in the library | +| `dataContextMenuExportBibTexClicked` | `{key: 'dataContextMenuExportBibTexClicked'}` | When `Export BibTex` is clicked in the context menu of a paper in the library | +| `dataContextMenuExportBibTexKeyClicked` | `{key: 'dataContextMenuExportBibTexKeyClicked'}` | When `Export BibTex Key` is clicked in the context menu of a paper in the library | +| `dataContextMenuExportPlainTextClicked` | `{key: 'dataContextMenuExportPlainTextClicked'}` | When `Export Plain Text` is clicked in the context menu of a paper in the library | +| `dataContextMenuExportCSVClicked` | `{key: 'dataContextMenuExportCSVClicked'}` | When `Export CSV` is clicked in the context menu of a paper in the library | +| `feedContextMenuAddToLibraryClicked` | `{key: 'feedContextMenuAddToLibraryClicked'}` | When `Add to Library` is clicked in the context menu of a feed in the library | +| `feedContextMenuToggleReadClicked` | `{key: 'feedContextMenuToggleReadClicked'}` | When `Toggle Read` is clicked in the context menu of a feed in the library | +| `sidebarContextMenuFeedRefreshClicked` | `{key: 'sidebarContextMenuFeedRefreshClicked', value: {data: feedID}}` | When `Refresh` is clicked in the context menu of a feed in the sidebar | +| `sidebarContextMenuEditClicked` | `{key: 'sidebarContextMenuEditClicked', value: {data: id, type: Categorizer or Feed}}` | When `Edit` is clicked in the context menu of a feed in the sidebar | +| `sidebarContextMenuColorClicked` | `{key: 'sidebarContextMenuColorClicked', value: {data: id, type: Categorizer or Feed, color: color}}` | When `Color` is clicked in the context menu of a feed in the sidebar | +| `sidebarContextMenuDeleteClicked` | `{key: 'sidebarContextMenuDeleteClicked', value: {data: id, type: Categorizer or Feed}}` | When `Delete` is clicked in the context menu of a feed in the sidebar | +| `supContextMenuDeleteClicked` | `{key: 'supContextMenuDeleteClicked'}` | When `Delete` is clicked in the context menu of a supplementary file in the library | +| `thumbnailContextMenuReplaceClicked` | `{key: 'thumbnailContextMenuReplaceClicked'}` | When `Replace` is clicked in the context menu of a thumbnail in the library | +| `thumbnailContextMenuRefreshClicked` | `{key: 'thumbnailContextMenuRefreshClicked'}` | When `Refresh` is clicked in the context menu of a thumbnail in the library | +| `linkToFolderClicked` | `{key: 'linkToFolderClicked'}` | When `Link to Folder` is clicked in Quickpaste UI | +| `dataContextMenuFromExtensionsClicked` | `{ extID: string; itemID: string}` | When a context menu item from an extension is clicked | +| `dataContextMenuExportBibItemClicked` | `{key: 'dataContextMenuExportBibItemClicked'}` | When `Export BibItem` is clicked in the context menu of a paper in the library | \ No newline at end of file diff --git a/src/cn/extension-doc/plmainapi/filesystem-service.md b/src/cn/extension-doc/plmainapi/filesystem-service.md new file mode 100644 index 0000000..b80abde --- /dev/null +++ b/src/cn/extension-doc/plmainapi/filesystem-service.md @@ -0,0 +1,63 @@ +# FileSystemService + +## Call + +```typescript +import { PLMainAPI } from "paperlib-api/api"; + +PLMainAPI.fileSystemService.methodname(...); +``` + +## Avaliable Methods + +### `getSystemPath` + +```typescript +/** + * Get the path of the given key. + * @param {string} key - The key to get the path of. + * @returns {string} - The path of the given key. + */ +getSystemPath(key: "home" | "appData" | "userData" | "sessionData" | "temp" | "exe" | "module" | "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos" | "recent" | "logs" | "crashDumps", windowId: string): Promise; +``` + +### `showFilePicker` + +```typescript +/** + * Show a file picker. + * @returns {Promise} The result of the file picker. + */ +showFilePicker(): Promise; +``` + +### `showFolderPicker` + +```typescript +/** + * Show a folder picker. + * @returns {Promise} The result of the folder picker. + */ +showFolderPicker(): Promise; +``` + +### `showSaveDialog` + +```typescript +/** + * Preview a file. + * @param {string} fileURL - The URL of the file to preview. + */ +preview(fileURL: string): Promise; +``` + +### `writeToFile` +```typescript +/** + * Write some text to a file. + * @param {string} filePath The path of the file to write to. + * @param {string} text The text to write to the file. + * @returns {void} Nothing. + */ +writeToFile(filePath: string, text: string): void; +``` \ No newline at end of file diff --git a/src/cn/extension-doc/plmainapi/index.md b/src/cn/extension-doc/plmainapi/index.md new file mode 100644 index 0000000..5cab902 --- /dev/null +++ b/src/cn/extension-doc/plmainapi/index.md @@ -0,0 +1,18 @@ +# PLMainAPI + +这组 API 中,包含了大部分在主进程里面的服务和他们的方法。 + +在插件中,可以直接使用: + +```typescript +import { PLMainAPI } from "paperlib-api/api"; + +const results = await PLMainAPI.serviceName.methodName(...) +``` + +## 可用的服务 + +- `contextMenuService`:右键菜单服务。 +- `fileSystemService`:文件系统服务,获取一些默认的文件夹路径,以及控制路径选择框,预览文件等。 +- `menuService`:菜单服务。包含大部分快捷键。 +- `windowProcessManagementService`:窗口管理服务,用于管理窗口。 \ No newline at end of file diff --git a/src/cn/extension-doc/plmainapi/menu-service.md b/src/cn/extension-doc/plmainapi/menu-service.md new file mode 100644 index 0000000..1d03331 --- /dev/null +++ b/src/cn/extension-doc/plmainapi/menu-service.md @@ -0,0 +1,57 @@ +# MenuService + +## Call + +```typescript +import { PLMainAPI } from "paperlib-api/api"; + +PLMainAPI.menuService.methodname(...); +``` + +## Avaliable Methods + +### `enableGlobalShortcuts` + +```typescript +/** + * Enable global shortcuts. + */ +enableGlobalShortcuts(): void; +``` + +### `disableGlobalShortcuts` + +```typescript +/** + * Disable global shortcuts. + */ +disableGlobalShortcuts(): void; +``` + +### `click` + +```typescript +/** + * Click menu item in a programmatic way. + * @param key + */ +click(key: keyof IMenuServiceState): void; +``` + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| `preference` | `{key: 'preference'}` | When Preference is clicked | +| `File-enter` | `{key: 'File-enter'}` | When `File-Open` is clicked in the menu bar | +| `File-copyBibTex` | `{key: 'File-copyBibTex'}` | When `File-Copy BibTex` is clicked in the menu bar | +| `File-copyBibTexKey` | `{key: 'File-copyBibTexKey'}` | When `File-Copy BibTex Key` is clicked in the menu bar | +| `Edit-rescrape` | `{key: 'Edit-rescrape'}` | When `Edit-Rescrape` is clicked in the menu bar | +| `Edit-edit` | `{key: 'Edit-edit'}` | When `Edit-Edit` is clicked in the menu bar | +| `Edit-flag` | `{key: 'Edit-flag'}` | When `Edit-Flag` is clicked in the menu bar | +| `View-preview` | `{key: 'View-preview'}` | When `View-Preview` is clicked in the menu bar | +| `View-next` | `{key: 'View-next'}` | When `View-Next` is clicked in the menu bar | +| `View-previous` | `{key: 'View-previous'}` | When `View-Previous` is clicked in the menu bar | + + + diff --git a/src/cn/extension-doc/plmainapi/windowprocessmanagement-service.md b/src/cn/extension-doc/plmainapi/windowprocessmanagement-service.md new file mode 100644 index 0000000..5b1704b --- /dev/null +++ b/src/cn/extension-doc/plmainapi/windowprocessmanagement-service.md @@ -0,0 +1,243 @@ +# WindowProcessManagementService + +## Call + +```typescript +import { PLMainAPI } from "paperlib-api/api"; + +PLMainAPI.windowProcessManagementService.methodname(...); +``` + +## Avaliable Methods + +### `create` + +```typescript +/** + * Create Process with a BrowserWindow + * @param id - window id + * @param options - window options + * @param eventCallbacks - callbacks for events + * @param additionalHeaders - additional response headers for the window + */ +create( + id: string, + options: WindowOptions, + eventCallbacks?: Record void>, + additionalHeaders?: Record +): void; +``` + +### `destroy` + +```typescript +/** + * Destroy the window with the given id. + * @param windowId - The id of the window to be destroyed + */ +destroy(windowId: string): Promise; +``` + +### `fireServiceReady` + +```typescript +/** + * Fire the serviceReady event. This event is fired when the service of the window is ready to be used by other processes. + * @param windowId - The id of the window that fires the event + */ +fireServiceReady(windowId: string): Promise; +``` + +### `show` + +```typescript +/** + * Show the window with the given id. + * @param windowId - The id of the window to be shown + */ +show(windowId: string): Promise; +``` + +### `hide` + +```typescript +/** + * Hide the window with the given id. + * @param windowId - The id of the window to be hidden + */ +hide(windowId: string, restoreFocus?: boolean): Promise; +``` + +### `minimize` + +```typescript +/** + * Minimize the window with the given id. + * @param windowId - The id of the window to be minimized + */ +minimize(windowId: string): Promise; +``` + +### `maximize` + +```typescript +/** + * Maximize the window with the given id. + * @param windowId - The id of the window to be maximized + */ +maximize(windowId: string): Promise; +``` + +### `close` + +```typescript +/** + * Close the window with the given id. + * @param windowId - The id of the window to be closed + */ +close(windowId: string): Promise; +``` + +### `forceClose` + +```typescript +/** + * Force close the window with the given id. + * @param windowId - The id of the window to be force closed + */ +forceClose(windowId: string): Promise; +``` + +### `changeTheme` + +```typescript +/** + * Change the theme of the app. + * @param theme - The theme to be changed to + */ +changeTheme(theme: APPTheme): Promise; +``` + +### `isDarkMode` + +```typescript +/** + * Check if the app is in dark mode. + * @returns Whether the app is in dark mode + */ +isDarkMode(): Promise; +``` + +### `resize` + +```typescript +/** + * Resize the window with the given id. + * @param windowId - The id of the window to be resized + * @param width - The width of the window + * @param height - The height of the window + */ +resize(windowId: string, width: number, height: number): Promise; +``` + +### `getScreenSize` + +```typescript +/** + * Get the size of the screen. + * @returns The size of the screen + */ +getScreenSize(): Promise<{ + width: number; + height: number; +}>; + +``` + +### `isFocused` + +```typescript +/** + * Whether the window is focused. + * @param windowId - The id of the window to be checked + */ +isFocused(windowId: string): Promise; +``` + +### `setParentWindow` +```typescript +/** + * Set parent as current window's parent window. + * @param parentId - The id of the parent window + * @param currentId - The id of the current window + */ +setParentWindow(parentId: string | null, currentId: string): void; +``` + +### `getBounds` +```typescript +/** + * Return the window's current bounds. + * @param windowId - The id of the window to be checked + */ +getBounds(windowId: string): Electron.Rectangle | undefined; +``` + +### `setBounds` +```typescript +/** + * Set the window's current bounds. + * @param windowId - The id of the window to be set + * @param bounds - The bounds of the window to be set + */ +setBounds(windowId: string, bounds: Partial): void; +``` + +### `hasParentWindow` +```typescript +/** + * Return whether the window has a parent. + * @param windowId - The id of the window to be checked + */ +hasParentWindow(windowId: string): boolean; +``` + +### `setAlwaysOnTop` +```typescript +/** + * Set whether the window should show always on top of other windows. + * @param windowId - The id of the window to be set + * @param flag - Whether the window should show always on top of other windows + */ +setAlwaysOnTop(windowId: string, flag: boolean): void; +``` + +### `center` +```typescript +/** + * Move the window to the center of the screen. + * @param windowId - The id of the window to be set + */ +center(windowId: string): void; +``` + +## Events + +| Event ID | Callback Value | Description | +| -------------- | ---------------------------------------------- | --------------------------------------------------------------------- | +| `serviceReady` | `{key: 'serviceReady', value: windowId}` | When the service of the window is ready to be used by other processes | +| `requestPort` | `{key: 'requestPort', value: senderProcessId}` | When a process is requesting MessagePort | +| `destroyed` | `{key: 'destroyed', value: windowId}` | When the window is destroyed | +| any window ID | `{key: windowId, value: event}` | event: `ready-to-show`, `blur`, `focus`, `close`, `show`, `created` | + +The main renderer window's ID is `rendererProcess`. If you want to listen to the `blur` event of the main renderer window, you can do this: + +```typescript +import { PLMainAPI } from "paperlib-api"; + +PLMainAPI.windowProcessManagementService.on("rendererProcess", (event) => { + if (event.value === "blur") { + // do something + } +}); +``` diff --git a/src/cn/extension-doc/preference.md b/src/cn/extension-doc/preference.md new file mode 100644 index 0000000..2f81979 --- /dev/null +++ b/src/cn/extension-doc/preference.md @@ -0,0 +1,134 @@ +# 用户设置 + +## 默认插件设置 + +在创建插件类时,我们可以提供默认的插件设置,这些设置将在用户第一次安装插件时被使用。 + +```typescript +class PaperlibHelloworldExtension extends PLExtension { + constructor() { + super({ + id: "...", + defaultPreference: { + [id: string]: { + type: "string" | "boolean" | "options" | "pathpicker", + name: string, + description: string, + value: string | boolean, + order?: number, + options?: { [key: string]: string }, // only for options type + }, + ... + }, + }); + } +} +``` + +每一个设置项都是一个键值对,键是设置项的 ID,值是一个对象,包含了设置项的各种信息。 + +其中: + +- `id` 字段是必须的,它指定了设置项的 ID。这被用来后续访问设置项的值。 +- `type` 字段是必须的,它指定了设置项的类型。 +- `name` 字段是必须的,它指定了设置项显示在 UI 的名称。 +- `description` 字段是必须的,它指定了设置项显示在 UI 的描述。 +- `value` 字段是必须的,它指定了设置项的默认值。 +- `order` 字段是可选的,它指定了设置项的显示顺序。 +- `options` 字段是可选的,它只有在设置项类型为 `options` 时才需要,它指定了选项类型的设置项的选项。 + + +## 设置项类型 + +如上所示,我们可以定义四种类型的设置项: + +- `string`:字符串类型的设置项,用户可以输入任意字符串。 +- `boolean`:布尔类型的设置项,用户可以选择 `true` 或 `false`。 +- `options`:选项类型的设置项,用户可以从预定义的选项中选择一个。必须提供 `options` 字段,其中的每个键值对,都是一个选项。键是选项的值,值是选项的显示名称。 +- `pathpicker`:路径选择器类型的设置项,用户可以选择一个文件或文件夹。 + +不同的设置项类型,Paperlib 会显示不同的组件用于方便用户设置。 + +## 访问设置项 + +在插件中访问设置项的值,可以通过 `PLExtAPI.extensionPreferenceService.get` 来获取。例如: + +```typescript +PLExtAPI.extensionPreferenceService.get(this.id, "lang") +``` + +这样,我们就可以获取到 `lang` 这个设置项的值。注意,我们需要提供插件的 ID (`this.id`) 以让 `ExtensionPreferenceService` 知道我们要访问哪个插件的设置项。 + +## 修改设置项 + +除了用户在设置界面中修改设置项的值,插件也可以通过 `PLExtAPI.extensionPreferenceService.set` 来修改设置项的值。例如: + +```typescript +PLExtAPI.extensionPreferenceService.set(this.id, {"lang": "en"}) +``` + +在这里我们提供一个 `patch object` 来修改设置。`patch object` 是一个键值对,其中的每个键值对,都是一个设置项的修改。键是设置项的 ID,值是设置项的新值。你可以同时传入多个设置项的修改。 + +## 监听设置项的修改 + +在插件中,我们可以通过 `PLExtAPI.extensionPreferenceService.onChanged / on` 来监听设置项的修改。例如: + +```typescript +PLExtAPI.extensionPreferenceService.onChanged( + `${this.id}:lang`, + (changes: {key: string, value: string}) => { + console.log(changes) // {key: "lang", value: "en"} + } +) +``` + +> **⚠️ 请注意,监听插件某个设置,必须以 `extID:preferenceKey` 的形式,来组成监听的事件的 ID。以便区分不同插件的设置项。** + +这样,我们就可以监听到 `lang` 这个设置项的修改。当用户在设置界面中修改了 `lang` 这个设置项的值时,我们就会运行传入的回调函数。该函数收到的参数是一个 Object,其中的 `key` 字段是设置项的 ID,`value` 字段是设置项的新值。 + +> ⚠️ 请注意,所有的监听注册,必须在插件的 `dispose` 函数中取消,来避免内存泄漏。 + +## 取消监听设置项的修改 + +在插件卸载或者重载时,Paperlib 会调用插件的 `dispose` 函数。在这个函数中,我们需要取消所有的监听注册,来避免内存泄漏。 + +当你在监听设置项的修改时,`PLExtAPI.extensionPreferenceService.onChanged / on` 会返回一个函数。运行此函数你可以取消监听。 + +例如: + +```typescript +// 监听 +const disposeCallback = PLExtAPI.extensionPreferenceService.onChanged( + `${this.id}:lang`, + (changes: {key: string, value: string}) => { + console.log(changes) // {key: "lang", value: "en"} + } +) + +// 取消监听 +disposeCallback() +``` + +## 存取密码 + +Paperlib 还提供密码项的存取。普通设置都存在插件目录附近的 `.json` 文件中,密码项则根据不同平台存在不同的钥匙串中。如 macOS 上的钥匙串,Windows 上的凭据管理器等。 + +```typescript +await PLExtAPI.extensionPreferenceService.setPassword( + this.id, "password-key", "your-password" +) + +const password = await PLExtAPI.extensionPreferenceService.getPassword( + this.id, "password-key" +) + +``` + +## 设置存储文件路径 + +在 Paperlib 中,插件的设置存储在插件目录附近的 `.json` 文件中。 + +- macOS: `~/Library/Application Support/paperlib/extensions/.json` +- Windows: `%APPDATA%/paperlib/extensions/.json` +- Linux: `~/.config/paperlib/extensions/.json` + diff --git a/src/cn/extension-doc/process-hook.md b/src/cn/extension-doc/process-hook.md new file mode 100644 index 0000000..59efae9 --- /dev/null +++ b/src/cn/extension-doc/process-hook.md @@ -0,0 +1,248 @@ +# 流程与钩子 + +本文介绍了 Paperlib 中重要的流程,以及在这些流程中,可用的钩子。 + +## 钩子介绍 + +在 Paperlib 中,我们在不同的地方,放置了不同名字的钩子点。一个钩子插件可以注册到对应的钩子点来介入 Paperlib 的运行流程。一共有两种类型的钩子点。 + +### Modify 钩子点 + +- **目的**:这种类型的钩子点,用于修改钩子点传来的参数,或者参数内的变量,而不能改变类型,最后返回修改后的参数。 + +- **回调返回值类型**:该类钩子要求回调函数的返回值必须是一个数组,数组的每个元素,对应着传入的参数数组。比如,钩子点传入的参数为 `(arg1: string, arg2: {value: number})`,在钩子的回调函数中可以修改 `arg1` 为别的字符串,修改 `arg2.value` 为别的数字,但是不能修改 `arg1` 的类型为数字,或者修改 `arg2` 的类型为别的类型。回调函数的返回值必须为修改后的参数组成的数组:`[arg1, arg2]`。 + + **注意,即使传入参数仅为一个,也需要返回元素数量为1的数组。因为传入参数永远被视作一个数组。在接下来的文章中,我们使用 *参数数组<...>* 来表示。** + + +### Transform 钩子点 + +- **目的**:这种类型的钩子点,可以修改 Paperlib 的运行流程中的数据流。用于把传入参数转换为其他形式的数据,然后返回。 +- **回调返回值类型**:可以为其他类型,但是通常不同钩子点有期望的返回值类型。比如,`scrapeEntry` 钩子点,期望的返回值类型为 `PaperEntity` 数组。 + +关于如何注册钩子,以及如何编写钩子插件,请参考 [钩子插件](./ext-types/hook-ext)。 + +## 论文导入流程 + +无论是从文件拖动导入,还是从浏览器插件导入,在组成相应的 `source payload` 后,都会进入到论文导入流程。论文导入流程的图示如下: + + + +该流程中,主要的钩子点都在 `ScrapeService` 的 `scrapeEntry()` 和 `scrapeMetadata()` 方法中。 + +--- + +### `scrapeEntry()` + +`scrapeEntry()` 的主要任务就是把这些来自不同类型的源的数据,转换为 Paperlib 内部的数据结构 `PaperEntity`,并尽可能填充 `PaperEntity` 的重要字段(例如:`title`, `doi`, `arxivID` 等),以便之后的 `scrapeMetadata()` 方法搜寻补全其论文 metadata。 + +在接受到论文源的导入之后,我们首先调用 `scrapeEntry()`。他的主要参数是 `SourcePayload` 的数组,即论文源的数据。`source payload` 包含指示源的类型的 `type` 字段,以及源的数据: + +```typescript +interface SourcePayload { + type: "file" | "webcontent"; + // 对于 file 类型,value 为文件路径的字符串 + // 对于 webcontent 类型,value 为 WebContentSourcePayload + value: string | WebContentSourcePayload; +} + +interface WebContentSourcePayload { + url: string; // 源网页的 url + document: string; // 源网页的 html + cookies?: string; // 有的网页包含 cookies +} +``` + +`scrapeEntry()` 的返回值类型为 `PaperEntity` 的数组,因为即使传入的 `SourcePayload` 数组只包含一个 `source payload`,它也可能包含多个论文。例如,从文件拖动导入,可能拖入的是一个 BibTex 文件,其中包含多个论文的信息。 + +--- + +### `scrapeMetadata()` +`scrapeEntry()` 的返回值,会被传入到 `scrapeMetadata()` 方法中。`scrapeMetadata()` 的主要任务是从网络各种数据库上搜寻论文的 metadata,并补全 `PaperEntity` 中所有字段。`scrapeMetadata()` 的返回值类型也是 `PaperEntity`的数组。 + +--- + +如图中所示,在这个流程中,有六个钩子点可以,五个 `Modify` 类型的钩子点,以及一个 `Transform` 类型的钩子点。 + +### `beforeScrapeEntry` + +| 参数 | 值 | +| --- | --- | +| 类型 | `Modify` | +| 位置 | `scrapeEntry()` 方法的最开始,还未转换为 `PaperEntity` 之前 | +| Callback 参数 | `SourcePayload[]` | +| Callback 返回值 | 参数数组<`SourcePayload[]`> | + +### `scrapeEntry` + +| 参数 | 值 | +| --- | --- | +| 类型 | `Transform` | +| 位置 | `scrapeEntry()` 的主要钩子点,接受 `SourcePayload` 数组 转换为 `PaperEntity` 数组输出 | +| Callback 参数 | `SourcePayload[]` | +| Callback 返回值 | `PaperEntity[]` | + +### `afterScrapeEntry` + +| 参数 | 值 | +| --- | --- | +| 类型 | `Modify` | +| 位置 | `scrapeEntry()` 方法的最后,已经转换为 `PaperEntity` 之后 | +| Callback 参数 | `PaperEntity[]` | +| Callback 返回值 | 参数数组<`PaperEntity[]`> | + +### `beforeScrapeMetadata` + +| 参数 | 值 | +| --- | --- | +| 类型 | `Modify` | +| 位置 | `scrapeMetadata()` 方法的最开始,还未搜寻 metadata 之前 | +| Callback 参数 | `paperEntities: PaperEntity[]`, `scrapers: string[]`, `force: boolean` | +| Callback 返回值 | 参数数组<`paperEntities: PaperEntity[]`, `scrapers: string[]`, `force: boolean`> | + +其中,`scrapers` 是一个字符串数组,如果不为空,则代表用户选择用特定的搜寻器进行搜寻。`force` 表示是否强制搜寻,如果为 `true`,则会忽略 `PaperEntity` 中已经存在的 metadata,强制搜寻。 + + +### `scrapeMetadata` + +| 参数 | 值 | +| --- | --- | +| 类型 | `Modify` | +| 位置 | `scrapeMetadata()` 的主要钩子点,接受 `PaperEntity` 数组,可修改每一个的属性后返回 | +| Callback 参数 | `paperEntities: PaperEntity[]`, `scrapers: string[]`, `force: boolean` | +| Callback 返回值 | 参数数组<`paperEntities: PaperEntity[]`, `scrapers: string[]`, `force: boolean`> | + +### `afterScrapeMetadata` + +| 参数 | 值 | +| --- | --- | +| 类型 | `Modify` | +| 位置 | `scrapeMetadata()` 方法的最后,已经搜寻 metadata 之后 | +| Callback 参数 | `paperEntities: PaperEntity[]`, `scrapers: string[]`, `force: boolean` | +| Callback 返回值 | 参数数组<`paperEntities: PaperEntity[]`, `scrapers: string[]`, `force: boolean`> | + +## 论文 PDF 搜寻流程 + +当一个论文没有对应的 PDF 文件时,Paperlib 会在详情面板显示一个按钮用于在网络资源搜索可用的 PDF 并下载。这个按钮点击后,会进入到论文 PDF 搜寻流程。论文 PDF 搜寻流程的主要函数是 `FileService` 的 `locateFileOnWeb()` 方法。在这个流程里可用的钩子有一个。 + +### `locateFile` + +| 参数 | 值 | +| --- | --- | +| 类型 | `Modify` | +| 位置 | `locateFileOnWeb()` 方法中 | +| Callback 参数 | `PaperEntity[]` | +| Callback 返回值 | 参数数组<`PaperEntity[]`> | + +## 引用导出流程 + +总的来说,引用导出时,我们首先得到需要导出的 PaperEntity 数组,然后将其转换为 `citation.js` 的 `Cite` 对象。最后,我们将 `Cite` 对象转化为对应格式的字符串,例如 BibTex 字符串。详情可见 GitHub 代码中,`ReferenceService` 的各个函数。 + +### `beforeExportBibItem` + +| 参数 | 值 | +| --- | --- | +| 类型 | `Modify` | +| 位置 | `exportBibItem()` 方法的最开始 | +| Callback 参数 | `PaperEntity[]` | +| Callback 返回值 | 参数数组<`PaperEntity[]`> | + +### `citeObjCreatedInExportBibItem` + +| 参数 | 值 | +| --- | --- | +| 类型 | `Modify` | +| 位置 | `exportBibItem()` 方法中,`Cite` 对象已经创建 | +| Callback 参数 | `cite: Cite, paperEntities: PaperEntity[]` | +| Callback 返回值 | 参数数组<`cite: Cite, paperEntities: PaperEntity[]`> | + +### `afterExportBibItem` + +| 参数 | 值 | +| --- | --- | +| 类型 | `Modify` | +| 位置 | `exportBibItem()` 方法的最后 | +| Callback 参数 | `string` | +| Callback 返回值 | 参数数组<`string`> | + +### `beforeExportBibTexKey` + +| 参数 | 值 | +| --- | --- | +| 类型 | `Modify` | +| 位置 | `exportBibTexKey()` 方法的最开始 | +| Callback 参数 | `PaperEntity[]` | +| Callback 返回值 | 参数数组<`PaperEntity[]`> | + +### `citeObjCreatedInExportBibTexKey` + +| 参数 | 值 | +| --- | --- | +| 类型 | `Modify` | +| 位置 | `exportBibTexKey()` 方法中,`Cite` 对象已经创建 | +| Callback 参数 | `cite: Cite, paperEntities: PaperEntity[]` | +| Callback 返回值 | 参数数组<`cite: Cite, paperEntities: PaperEntity[]`> | + +### `afterExportBibTexKey` + +| 参数 | 值 | +| --- | --- | +| 类型 | `Modify` | +| 位置 | `exportBibTexKey()` 方法的最后 | +| Callback 参数 | `string` | +| Callback 返回值 | 参数数组<`string`> | + +### `beforeExportBibTexBody` + +| 参数 | 值 | +| --- | --- | +| 类型 | `Modify` | +| 位置 | `exportBibTexBody()` 方法的最开始 | +| Callback 参数 | `PaperEntity[]` | +| Callback 返回值 | 参数数组<`PaperEntity[]`> | + +### `citeObjCreatedInExportBibTexBody` + +| 参数 | 值 | +| --- | --- | +| 类型 | `Modify` | +| 位置 | `exportBibTexBody()` 方法中,`Cite` 对象已经创建 | +| Callback 参数 | `cite: Cite, paperEntities: PaperEntity[]` | +| Callback 返回值 | 参数数组<`cite: Cite, paperEntities: PaperEntity[]`> | + +### `afterExportBibTexBody` + +| 参数 | 值 | +| --- | --- | +| 类型 | `Modify` | +| 位置 | `exportBibTexBody()` 方法的最后 | +| Callback 参数 | `string` | +| Callback 返回值 | 参数数组<`string`> | + +### `beforeExportPlainText` + +| 参数 | 值 | +| --- | --- | +| 类型 | `Modify` | +| 位置 | `exportPlainText()` 方法的最开始 | +| Callback 参数 | `PaperEntity[]` | +| Callback 返回值 | 参数数组<`PaperEntity[]`> | + +### `citeObjCreatedInExportPlainText` + +| 参数 | 值 | +| --- | --- | +| 类型 | `Modify` | +| 位置 | `exportPlainText()` 方法中,`Cite` 对象已经创建 | +| Callback 参数 | `cite: Cite, paperEntities: PaperEntity[]` | +| Callback 返回值 | 参数数组<`cite: Cite, paperEntities: PaperEntity[]`> | + +### `afterExportPlainText` + +| 参数 | 值 | +| --- | --- | +| 类型 | `Modify` | +| 位置 | `exportPlainText()` 方法的最后 | +| Callback 参数 | `string` | +| Callback 返回值 | 参数数组<`string`> | + diff --git a/src/cn/extension-doc/service-event.md b/src/cn/extension-doc/service-event.md new file mode 100644 index 0000000..c46b68d --- /dev/null +++ b/src/cn/extension-doc/service-event.md @@ -0,0 +1,67 @@ +# 服务事件 + +API 里的几乎所有的服务都是 `Eventable`` 的。这意味着每个服务都会在不同时机触发一些事件。在其他代码位置、进程可以监听对应的事件触发,来执行自己的代码。比如你可以监听用户选择的论文变化了,然后运行你的方法等等。 + +## 监听事件 + +监听事件的方法是 `on`,它接受两个参数,第一个是事件名,第二个是回调函数。回调函数的参数是事件触发时传递的参数。 + +```typescript +import { PLAPI } from 'paperlib-api/api'; + + +PLAPI.serviceName.on('event-id', (newValue: {key: string, value: any}) => { + ... +}); +``` + +回调函数接收的参数通常是一个对象,包含了 `key` 和 `value` 两个字段。`key` 是事件 ID,`value` 是对应的新值。 + +你可以给多个事件注册同一个回调函数: + +```typescript +PLAPI.serviceName.on( + ['event-id-1', 'event-id-2'], + (newValue: {key: string, value: any}) => { + ... + } +); +``` + +此时,`key` 字段将会有助于你判断是哪个事件触发了。 + +**在监听事件回调中,不要使用 `floating promise`,即,如果你的回调函数中包含 `AsyncFunction`,请务必 `await`,或者 `.catch` 异常。因为 `floating promise` 中的异常无法在 Paperlib 中被捕获,会导致插件崩溃:** + +```typescript +// 永远不要做如下: +PLAPI.serviceName.on('event-id', (newValue: {key: string, value: any}) => { + asyncFunction(); +}); + +async function asyncFunction() { + throw new Error('error'); // 这个错误将无法被 Paperlib 捕获,会导致插件崩溃。 +} +``` + +## 取消监听 + +请注意,你需要保存 `on` 方法返回的函数,以便在不需要监听时取消监听。 + +```typescript +const cancel = PLAPI.serviceName.on('event-id', (newValue: {key: string, value: any}) => { + ... +}); + +// 取消监听 +cancel(); +``` + +## 监听别名 + +对于部分服务,我们提供了一些 `on()` 函数的别名,以便更舒服地编写代码。 + +比如,在 `preferenceService` 中,我们提供 `onChanged()` 方法,它的效果和 `on()` 一样,仅仅只是一个别名。 + +## 事件列表 + +不同的服务有不同的事件,具体的事件列表请参考对应的服务文档。 \ No newline at end of file diff --git a/src/cn/extension-doc/ui-slot.md b/src/cn/extension-doc/ui-slot.md new file mode 100644 index 0000000..59ac745 --- /dev/null +++ b/src/cn/extension-doc/ui-slot.md @@ -0,0 +1,49 @@ +# UI 插槽 + +本文介绍了 Paperlib 中提供的可以使用,修改的 UI 插槽。 + +## 论文详情面板插槽 + +在论文的详情面板,我提供三个插槽: + +- `paperDetailsPanelSlot1`: 在论文发表时间之下。 +- `paperDetailsPanelSlot2`: 在论文的评分之下。 +- `paperDetailsPanelSlot3`: 在论文的补充材料之下。 + +## 通知视图插槽 + +在通知视图中,我提供一个插槽: + +- `overlayNotifications`: 出现在屏幕顶部的通知。 + +要在插槽中显示内容,您需要更新 `overlayNoticationShown = true`: + +```typescript +PLAPI.uiStateService.setState({"overlayNoticationShown": true}); +``` + +### 插槽内容 + +```typescript +{ + title: string, + content: string +} +``` + +### 插槽更新 + +```typescript +PLAPI.uiSlotService.updateSlot( + "paperDetailsPanelSlot1", + { + [id: string]: { + title: string, + content: string + } + } +); + +``` + +`id` 是内容的唯一标识符。如果 `id` 已经在插槽中,内容将被更新。否则,将添加新内容。 \ No newline at end of file diff --git a/src/cn/index.md b/src/cn/index.md new file mode 100644 index 0000000..b55cae7 --- /dev/null +++ b/src/cn/index.md @@ -0,0 +1,126 @@ +--- +layout: home + +hero: + name: Paperlib + text: to organise academic papers decently. + tagline: 一个简单好用的论文管理工具。 + image: + src: /assets/images/ui.png + alt: PaperlibUI + actions: + - theme: brand + text: 使用指南 + link: ./doc/getting-started + - theme: alt + text: 下载 + link: ./download + - theme: alt + text: 捐赠 + link: ./donate + + +features: +- title: 自动检索元数据 + details: 接入各学科数据库用于匹配论文元数据,逐步为每一个学科(例如计算机科学,物理学等)定制化数据库组合提高检索精度。尤其是精准的会议论文元数据检索能力 +- title: 管理你的论文 + details: 为论文评分,分配标签、组,方便进行分类检索。支持 markdown 和纯文本笔记 +- title: 导出引用 + details: 导出论文引用到 BibTex 或者 Plain Text 格式,快速导出插件让你一键复制 BibTex。同时支持 Word 插件 +- title: 插件系统 + details: 开发并发布你自己的插件吧! +- title: RSS 订阅 + details: 订阅 RSS 信息流来获取最新论文 +- title: 现代UI,多平台、云同步 + details: 支持 macOS, Linux 和 Windows。现代化简介美观的 UI。数据库云同步功能,跨平台随处访问您的论文库 +--- + + + +
+
+ 评价 +
+
+ +
+ +
+
用户
+
+
+
  • + {{ user }} +
  • +
    +
    + +
    +
    Sponsors
    +
    +
    + + +
    +
    \ No newline at end of file diff --git a/src/cn/release-note.md b/src/cn/release-note.md new file mode 100644 index 0000000..4d574e4 --- /dev/null +++ b/src/cn/release-note.md @@ -0,0 +1,55 @@ +--- +title: "更新日志" +--- + + + + + + diff --git a/src/en/doc/cloud-sync/setup.md b/src/en/doc/cloud-sync/setup.md new file mode 100644 index 0000000..4e3efe3 --- /dev/null +++ b/src/en/doc/cloud-sync/setup.md @@ -0,0 +1,110 @@ +# Cloud Sync + +--- + +We have two things to sync: 1. metadata of papers, 2. PDF files. + +For metadata, we can use MongoDB Atlas as the cloud database backend. For PDF files, we recommand to use onedrive (or dropbox etc.) or webDAV to sync your PDF files accross devices. + +About why can't you use webdav/dropbox... to sync metadata: + +> 1. Cloud storage services are designed for files, not for databases. They cannot merge conflict and the sync delay usually occurs. For example you modified something on PC A, then you modified something on PC B, but without the network connection. Once you connect PC B with network, how to merge these two databases? +> 2. We need a powerful database for fast searching. +> 3. When an APP is reading a database file, there is a file lock. It means that the cloud storage service such as OneDrive will never sync the database file until you exit the APP. So , even if conflict merging does not become a problem, you must ensure that you remember to exit the software in time on any computer. Imagine you forget to close the software on PC B, but leave PC B and go to PC A. You will never be able to access the changes you made on PC B. + +Paperlib use MongoDB Atlas as the cloud database backend. You can create your own cloud database and use it to sync your Paperlib data across your devices. The free tier of MongoDB Atlas is enough for Paperlib. When everything is set up, you can safely store the metadata of your papers in the cloud without any maintenance operation. + +## Create a MongoDB Atlas DB +1. Open [https://account.mongodb.com/account/login](https://account.mongodb.com/account/login), sign up and login to your account. Close all the guides and click `Create`. + +![](/assets/images/guide/cloud-sync/n1.png) + +2. Choose the free dataset plan. Uncheck the two options in the red box. Select the best server location according to your country. Click `Create Deployment`. + +![](/assets/images/guide/cloud-sync/n2.png) + +3. Close the popup window. + +![](/assets/images/guide/cloud-sync/n3.png) + +4. Go to the App Services page, create an APP. + +![](/assets/images/guide/cloud-sync/n4.png) + +5. Make sure the Data Source is the one you just created. Select the location for deployment. Click `Create App`. + +![](/assets/images/guide/cloud-sync/n5.png) + +6. Close the popup window. + +![](/assets/images/guide/cloud-sync/n6.png) + +7.Since then, the MongoDB Atlas is ready. + +## Create a User + +A User is a person who can access the cloud database. + +1. In `App Services` page, click `App Users`, click `Authentication Providers`. + +![](/assets/images/guide/cloud-sync/user1.png) + +2. Turn on the `Email Password Authentication`. Choose `Automatically confirm users` as the user confirmation method. `Password Reset URL` is not required, just leave a random URL. Click `Save Draft`. + +![](/assets/images/guide/cloud-sync/user2.png) + +3. Click the upper banner `REVIEW DRAFT & DEPLOY` to apply previous settings. + +![](/assets/images/guide/cloud-sync/user3.png) + +4. Back to the User page, Click `Add New User` to create a user. + +![](/assets/images/guide/cloud-sync/user4.png) + +5. Sinc then, you've created a user. + +## Create a Data Table. + +The data schema can be automatically created based on the data sent by the Paperlib APP. + +1. In the `Device Sync` page, click `Start Sync`. It enables the your APP to sync data with the backend database cluster. In the popup dialog, click `No thanks, continue to Sync`. + +![](/assets/images/guide/cloud-sync/n7.png) + +2. Open the `Development Mode` to allow MongoDB Altas automatically create data schema based on the data sent by the Paperlib APP. Other configurations are as shown below. Leave the Queryable Fields setting as default. + +![](/assets/images/guide/cloud-sync/n8.png) + +3. Click `Enable Sync`. + +4. Click the upper banner `REVIEW DRAFT & DEPLOY` again. + +5. In the popup dialog, choose `Users can read and write all data`. + +6. Click the upper banner `REVIEW DRAFT & DEPLOY` again. + +7. Since then, the cloud database is ready. + +## Connect Paperlib to your MongoDB Atlas DB + +1. Open the preference window of Paperlib, click `Cloud` tab, input your MongoDB Atlas APP ID, email and password of the user you created. + +![](/assets/images/guide/cloud-sync/n13.png) + +2. Turn on the `Flexible Sync`. + +![](/assets/images/guide/cloud-sync/n11.png) + +3. Click `Login`. If the sync is successful, you can see some writing logs after importing a paper to Paperlib. + +![](/assets/images/guide/cloud-sync/n19.png) + +--- + +Now, everthing is ready, enjoy Paperlib with cloud sync. + +## Sync PDF Files + +Since PDF storage may cost a lot of cloud space, we recommand to use onedrive (or dropbox etc.) or webDAV to sync your PDF files accross devices. + +For example, on device A, choose `C:/Onedrive/mypaperlib` as your library folder. On the other device B, choose `/user/xxx/Onedrive/mypaperlib` as the library folder. diff --git a/src/en/doc/custom-ca.md b/src/en/doc/custom-ca.md new file mode 100644 index 0000000..e063917 --- /dev/null +++ b/src/en/doc/custom-ca.md @@ -0,0 +1,15 @@ +# Custom Root CA + +In some cases, you may need to use a custom root CA to access the network. You can put the custom root CA file in the following directory: + +- macOS: `~/Library/Application Support/paperlib/` +- Windows: `%APPDATA%/paperlib/extensions/` +- Linux: `~/.config/paperlib/extensions/` + +In this directory, please rename the required files to: + +- `ca.crt` +- `ca.cer` +- `ca.pem` +- `client.key` +- `client.crt` diff --git a/src/en/doc/extensions/browser-extension.md b/src/en/doc/extensions/browser-extension.md new file mode 100644 index 0000000..da971f8 --- /dev/null +++ b/src/en/doc/extensions/browser-extension.md @@ -0,0 +1,18 @@ +# Browser Extension + +## Chrome and Edge + +Install it at [Chrome Web Store](https://chrome.google.com/webstore/detail/paperlib/kgnpnfbjckgddlednhoblgfdfohhapng). + +## Firefox + +Install it at [Firefox Add-ons](https://addons.mozilla.org/firefox/addon/paperlib/). + +## Safari Extension + +> Since I don't have an Apple developer account, I can't publish the extension to the App Store. If you want to update the safari extension, you need to manually re-install it.. + +1. Download [Paperlib Safari Extension](https://objectstorage.uk-london-1.oraclecloud.com/n/lrarf8ozesjn/b/bucket-20220130-2329/o/distribution%2Fsafari_ext%2FPaperlib%20Safari%20Extension.zip) +2. Drag it to the `Applications` folder. +3. Open it, and then close it. +4. Open `Safari - Preference - Extensions`, Enable it. \ No newline at end of file diff --git a/src/en/doc/extensions/msword-extension.md b/src/en/doc/extensions/msword-extension.md new file mode 100644 index 0000000..2c79154 --- /dev/null +++ b/src/en/doc/extensions/msword-extension.md @@ -0,0 +1,73 @@ +# Microsoft Word Add-in + +This article introduces how to use the Paperlib Microsoft Word Add-in. + +## Install + +### Extension + +Open the extension market, and download the `paperlib-msword-extension` extension. + +### Mac OS and Windows + +When installing the extension, it will automatically inject the extension into MS Word. It requires The elevation of administrator privileges on MacOS and Windows. + +Then, restart Microsoft Word. + +### Web Office + +Download and save `https://distribution.paperlib.app/word_addin/manifest.xml` + +Click the `Add-in` button on the toolbar of the web office. + +Then, click the `Upload My Add-in` button to upload the downloaded `manifest.xml` file. + +![](/assets/images/guide/extensions/word/web-install.png) + +## Open Add-in + +### Mac OS + +Click the chevron button on the right side of the `My Add-ins` botton, and select the `Paperlib Add-in`. + +![](/assets/images/guide/extensions/word/macos-open.png) + +After that, click the `Paperlib Add-in` button in the toolbar. + +### Windows + +Click the `My Add-ins` button, then go to the `SHARED FOLDER` tab. + +![](/assets/images/guide/extensions/word/win-open-1.png) + +![](/assets/images/guide/extensions/word/win-open-2.png) + +After that, click the `Paperlib Add-in` button in the toolbar. + +## Search + +You can search the papers in your Paperlib library. + +![](/assets/images/guide/extensions/word/search.png) + + +## Insert a Citation + +Click a paper in the search result list, then click the `Add Citation` button. + +![](/assets/images/guide/extensions/word/add-cite.png) + +## Insert the Reference List + +Click the `Update Bibilography` button. + +![](/assets/images/guide/extensions/word/update-ref.png) + + +## Others + +You can change the citation style and the reference list style. To import new CSL styles, go to the preference view of the extension. More CSL styles are available at [CSL Style Repository](https://github.com/citation-style-language/styles). + +The link icon means that the add-in cannot communicate with the Paperlib app, so you need to click it to retry, restart Paperlib, or reload the add-in. + +![](/assets/images/guide/extensions/word/others.png) \ No newline at end of file diff --git a/src/en/doc/getting-started.md b/src/en/doc/getting-started.md new file mode 100644 index 0000000..ce336cf --- /dev/null +++ b/src/en/doc/getting-started.md @@ -0,0 +1,93 @@ +# Getting Started + +This article introduces how to use Paperlib. + +## Library Folder + +Open the preference window, choose a folder to store all your paper PDF files and the local database files. + +> ⚠️ If you are going to use the cloud sync feature, we recommend you to choose a shared folder such as Onedrive, Dropbox etc. + +> ⚠️ If you use WebDAV to sync your library, this folder is used as the cache folder. + +![](/assets/images/getting-started/library-folder.png) + +## Metadata Scrapers + +We build a data pipeline to scrape the metadata from many databases for each paper. We achieve this via an extension `paperlib-metadata-scrape-extension`. You can select your preferred scrapers in the preference window. + +> ⚠️ Please choose a proper scraper combination for the best retrieval rate. + +![](/assets/images/getting-started/scraper.png) + +## Import New Papers + +Drag and drop PDF files onto the main view of Paperlib. You can also import a paper from a website by clicking the [browser extension](./extensions/browser-extension). + +The `.bib` file and the `.csv` file exported from Zotero are also supported. + +![](/assets/images/getting-started/add.png) + +## Metadata Retrieval + +The metadata of each imported paper will be automaticlly scraped. Some papers may match nothing in the databases, thereby resulting in an empty/wrong paper item. You can manually edit its metadata by pressing `CTRL/CMD + E` or by clicking the edit button. Providing one of the title / DOI / arxivID and click the `Scrape` button to complete the rest is enough for most cases. + +## Open / Preview a Paper + +- Open a paper: double click an item in the main view. +- Preview a paper: press `Space` or switch to the Table and Reader view. +- For Windows / Linux users, please install the `paperlib-preview-extension` from our extension marketplace. + +![](/assets/images/getting-started/preview.png) + +## Tags / Folders +- Add: open the edit view, add or create your preferred tags. You can also drag-drop an item to a tag/folder in the sidebar to add it to the tag/folder. In the left sidebar, you can also create a new tag or folder by clicking the `+` button. +- Colorize: right click a tag or a folder in the sidebar list can change its color. + +> By default, the `tag` is the inherent attributes of a paper such as `computer-vision`. The `folder` indicates the attributes given by users such as `good-writing`. But you can use them in any way you like. In Paperlib 3.0, we provide hierarchical folders. + +![](/assets/images/getting-started/edit.png) + +## Attach Supplementary Files + +Drag and drop some files onto the detail view (right panel) of a paper to attach supplementaries. + +![](/assets/images/getting-started/addsup.png) + +## Locate Available PDF + +For a paper without a PDF file, click the button called 'Locate' in the details panel to download the PDF file from internet. Please install the `paperlib-paper-locate-extension` to use it. + +Paperlib provides some PDF downloaders such as arXiv, Unpaywall etc.. For xxx-hub (that very famous paper download website XD), we cannot directly provide it because of the legal issue. You can manually input the URL of it in the `preference - downloader` to use it. + +![](/assets/images/getting-started/locate.png) + +## Search + +Paperlib provides three search modes: `general`, `fulltext`, and `advanced` mode. You can use `\search`, `\search_fulltext`, and `\search_advanced` in the command bar to switch between them. + +## Quick Copy-paste Plugin + +When you are writing a paper, you can use the Quick Copy-paste Plugin to copy-paste the BibTex of a paper in your library. Just press `CTRL/CMD+SHIFT+I`, search a paper, and press `Enter`. Now, the BibTex is copied to your clipboard. You can also link this plugin to a folder. All papers copied by this plugin will be added to that folder. Thus, you can export all the BibTex once you finish writing your draft paper. + + + +See [Quick Copy-paste Plugin](./quick-copy-paste-plugin/) for more details. + +## Subscribe to RSS Feeds + +You can subscribe to RSS feeds to get the latest papers in your research topic. To add a new RSS feed, switch to the `Feeds` view and click the `+` button in the sidebar. + +> How to use the arXiv RSS? +> +> https://arxiv.org/help/rss +> +> https://arxiv.org/help/api/user-manual#search_query_and_id_list + +Many journals provide RSS feeds. Please refer to their official website to learn how to use RSS. + +![](/assets/images/getting-started/feedadd.png) + +## Cloud Sync + +See [Cloud Sync](./cloud-sync/setup) for more details. \ No newline at end of file diff --git a/src/en/doc/introduction.md b/src/en/doc/introduction.md new file mode 100644 index 0000000..36a9c9f --- /dev/null +++ b/src/en/doc/introduction.md @@ -0,0 +1,63 @@ +--- +title: "Highlights" +--- + +# Paperlib + +
    + + + +
    + + +An open-source academic paper management tool. + +## Introduction + +I'm a computer science PhD student. Conference papers are in major in my research community, which is different from other disciplines. Without DOI, ISBN, metadata of a lot of conference papers are hard to look up (e.g., NIPS, ICLR etc.). When I cite a publication in a draft paper, I need to manually search the publication information of it in Google Scholar or DBLP over and over again. + +**Why not Zotero, Mendely?** + +- A good metadata scraping capability is one of the core functions of a paper management tool. Unfortunately, no software in this world does this well, not even commercial software. + +- A modern UI. No extra useless features. + +What we need may be to: import a paper, scrape the metadata of it as accurately as possible, simply organise the library, and export it to BibTex when we are writing our papers. + +That is Paperlib. + + +## Highlights +- Scrape paper’s metadata with many scrapers. Support writing your metadata scrapers. Tailored for many disciplines (still growing): + - **General** + - `arXiv` + - `doi.org` + - `Semantic Scholar` + - `Crossref` + - `Google Scholar` + - `Springer` + - `Elseivier Scopus` + - **Computer Science and Electronic Engineering** + - `openreview.net` + - `IEEE` + - `DBLP` + - `Paper with Code (scrape available in the code repository)` + - **Earth Science** + - **Physics** + - `NASA Astrophysics Data System` + - `SPIE: Inte. Society for Optics and Photonics` + - **Chemistry** + - `ChemRxiv` + - **Biology** + - `BioRxiv / MedRxiv` + + - ... +- Fulltext and advanced search. +- Rating, flag, tag, folder and markdown/plain text note. +- RSS feed subscription to follow the newest publications on your research topic. +- Locate and download PDF files from the web. +- macOS spotlight-like plugin to copy-paste references easily when writing a draft paper. Also supports MS Word. +- Cloud sync, supports macOS, Linux, and Windows. +- Beautiful and clean UI. +- Extensible \ No newline at end of file diff --git a/src/en/doc/quick-copy-paste-plugin/index.md b/src/en/doc/quick-copy-paste-plugin/index.md new file mode 100644 index 0000000..74d27a7 --- /dev/null +++ b/src/en/doc/quick-copy-paste-plugin/index.md @@ -0,0 +1 @@ +coming soon... \ No newline at end of file diff --git a/src/en/doc/smart-filter/index.md b/src/en/doc/smart-filter/index.md new file mode 100644 index 0000000..0df36b3 --- /dev/null +++ b/src/en/doc/smart-filter/index.md @@ -0,0 +1,49 @@ +# Smart Filter + +## Concept + +Smart filter is a feature that allows you to do advanced filtering. For example, you can filter all papers published in 2020, all papers with some tags, or all papers with a specific author, etc. + +## Create a smart filter + +Simply click the `+` button near the smart filter section in the left sidebar, and you can create a new smart filter. + +We provide two ways to create a smart filter: +1. Create a smart filter via the UI. +2. Create a smart filter from an advanced search query string. + +### Create a smart filter via the UI + +You can create a new rule by clicking the `+` button at the bottom of the smart filter creating window. The `Match` selection box is used to specify the combination matching condition of the rules. For each rule, we have 4 fields: +- `Prefix Ops`: They are useful for collection query and for logical not. +- `Fields`: all query fields. +- `OPs`: operations for the fields. +- `Value`: the value for the rule. + + +### Create a smart filter from an advanced search query string + +For advanced users, you can create a smart filter from an advanced search query string. The query language is from our database library [Realm Query Language](https://www.mongodb.com/docs/realm-sdks/js/latest/tutorial-query-language.html). All available query fields are listed in the `Fields` selection box. + +## Case sensitive + +All string operations, such as `CONTAINS`, are case insensitive. If you want to ignore case, you can use `CONTAINS[c]`. + +## Examples + +### Recently added papers +![](/assets/images/guide/smart-filter/recent.png) + +To create a dynamic date string, you can use `[x DAYS]`. + +### Papers published in CVPR +![](/assets/images/guide/smart-filter/pub.png) + +### Papers published by a specific author +![](/assets/images/guide/smart-filter/author.png) + +### Papers with multiple tags +![](/assets/images/guide/smart-filter/tag.png) + +### Keywords in papers' title +![](/assets/images/guide/smart-filter/title.png) \ No newline at end of file diff --git a/src/en/donate.md b/src/en/donate.md new file mode 100644 index 0000000..ff506b6 --- /dev/null +++ b/src/en/donate.md @@ -0,0 +1,11 @@ +# Donate + +Paperlib thanks you for your donation. Your donation will help us to improve Paperlib and make it better. + +## Buy Me a Coffee + +Buy Me A Coffee + +## Wechat Pay + + \ No newline at end of file diff --git a/src/en/download-linux.md b/src/en/download-linux.md new file mode 100644 index 0000000..58da0a2 --- /dev/null +++ b/src/en/download-linux.md @@ -0,0 +1,48 @@ +# Linux Install + +We use AppImage to distribute Paperlib on Linux rather than .deb. The reason is that AppImage can run on almost any Linux distribution without installing any dependencies and can perform automatic update detection. + +## Requirements + +`GLIBCXX >= 3.4.26` + +For example: Ubuntu 18.04 or later, Debian 11 or later, Deepin 23 or later. + +To check if your system meets the requirements, run the following command: + +``` +strings /usr/lib/x86_64-linux-gnu/libstdc++.so.6 | grep GLIBCXX +``` + +You may also need to install: + +``` +Debian/Ubuntu: sudo apt-get install libsecret-1-dev +Red Hat-based: sudo yum install libsecret-devel +Arch Linux: sudo pacman -S libsecret +``` + + +## Download and Run + +Download the latest AppImage file. + +``` +curl -L https://distribution.paperlib.app/electron-linux/Paperlib_latest.AppImage -o Paperlib.AppImage +``` + +Then give it execute permission. + +``` +chmod +x Paperlib.AppImage +``` + +Finally, run it. + +``` +./Paperlib.AppImage +``` + +## Automatic Update + +AppImage will automatically detect new release, will automatically download and install it. Please make sure that you don't put Paperlib in a folder that your current user doesn't have write permission, such as `/usr/bin/`. diff --git a/src/en/download.md b/src/en/download.md new file mode 100644 index 0000000..af50429 --- /dev/null +++ b/src/en/download.md @@ -0,0 +1,21 @@ +# Download + +## Desktop App + +- [ macOS (Intel)](https://distribution.paperlib.app/electron-mac/latest.dmg) +- [ macOS (silicon)](https://distribution.paperlib.app/electron-mac-arm/latest.dmg) +- [ Windows](https://distribution.paperlib.app/electron-win/latest.exe) +- [ Linux](./download-linux) + +As we don't have funding to buy a code sign. On Windows, the Windows Defender might treat Paperlib as a virus. I hate it. Please go to Windows Defender - Virus & threat protection - Allowed threats - Allow my App - redownload! + +## Browser Extensions + +- [ Chrome / Edge](https://chrome.google.com/webstore/detail/paperlib/kgnpnfbjckgddlednhoblgfdfohhapng) +- [ Firefox](https://addons.mozilla.org/firefox/addon/paperlib/) +- [ Safari](./doc/extensions/browser-extension.html#safari-extension) + +## MS Word Extension + +- [ MS Word](./doc/extensions/msword-extension) + diff --git a/src/en/extension-doc/convention.md b/src/en/extension-doc/convention.md new file mode 100644 index 0000000..97094c0 --- /dev/null +++ b/src/en/extension-doc/convention.md @@ -0,0 +1,18 @@ +# Development Conventions + +A Paperlib extension is essentially an `npm` package. Therefore, the development of a Paperlib extension is similar to the development of an `npm` package. However, there are some restrictions. This section will explain the restrictions and conventions: + +1. In the callback of the event listener, please avoid `floating promise`. That is, if your callback function contains any `AsyncFunction`, please be sure to `await` or `.catch` the error exception. Because the error in the `floating promise` cannot be caught in Paperlib, it will cause the extension to crash. +2. The extension must be able to be packaged as an `npm` package that conforms to the `commonjs` specification. +3. The extension must be able to be packaged as a single `js` file, and it is recommended to `minify` to reduce the download size. (Except for `New Window` extensions) +4. Use the appropriate tools to remove the `import` statements of `PLAPI, PLMainAPI, PLExtAPI`, and keep the usage statements of `PLAPI, PLMainAPI, PLExtAPI` in the code after packaging. This ensures the normal operation of the extension in Paperlib. In our provided development environment, we use the `rollup-extension-modify` extension to achieve this. The reason for this constraint is that the `PLAPI, PLMainAPI, PLExtAPI` provided by the `paperlib-api/api` package are only used for code type autocompletion during development, do not contain any functionality, and should not appear in the final code of a extension. After a extension is loaded, the `PLAPI, PLMainAPI, PLExtAPI` objects will be automatically globally injected into its `VM` for directly using. +5. The `manifest_version` field must be included in `package.json` to indicate the API version used by the extension. Please keep it consistent with the version of the `paperlib-api` package you actually installed. We recommend that you always use the latest API version. +6. The `main` field must be included in `package.json`, pointing to the entry file of the extension. +7. The keywords in `package.json` must contain `paperlib` so that it can be searched in the extension marketplace. +8. The `name` field in `package.json` must be consistent with the `id` field in the main code of the extension. +9. The `version` field in `package.json` must comply with the `semver` specification. +10. The `description` field in `package.json` must contain a brief description of the extension. +11. The `author` field in `package.json` must contain the author information of the extension. +12. The `dependencies` field in `package.json` does not contain dependencies. That is, all dependencies are `devDependencies`. Please package the related functions in the dependencies into the `.js` file of the extension through any bundling tool. +13. It is recommended to correctly set the `files` field in `package.json` to only include the release files. +14. It is highly recommended to use the `homepage` field in `package.json` to provide the official website of the extension for users to learn more about the extension and feedback. diff --git a/src/en/extension-doc/data-structure.md b/src/en/extension-doc/data-structure.md new file mode 100644 index 0000000..0b14766 --- /dev/null +++ b/src/en/extension-doc/data-structure.md @@ -0,0 +1,194 @@ +# Important Data Structures + +In Paperlib, we have some important data structures, which are widely used in Paperlib, so we provide them to developers. You can import them from `paperlib-api/model` package. + +Here we will briefly introduce these data structures. + +## `OID` + +```typescript +type OID = ObjectId | string; +``` + +## `PaperEntity` + +```typescript +interface PaperEntity { + _id: OID; + id: OID; + _partition?: string; // For Realm Cloud Sync. + addTime: Date; + title: string; + authors: string; // Split by comma and a space. + publication: string; + pubTime: string; + pubType: number; + doi: string; + arxiv: string; + mainURL: string; + supURLs: string[]; + rating: number; + tags: Categorizer[]; + folders: Categorizer[]; + flag: boolean; + note: string; + codes: string[]; + pages: string; + volume: string; + number: string; + publisher: string; +} +``` + +This is the most important data structure in Paperlib. Each paper is a `PaperEntity` object. You can use it in your extension like this: + +```typescript + +```typescript +import { PaperEntity } from 'paperlib-api/model'; + +const draft = new PaperEntity({ title: '123' }) +draft.authors = "xxx, yyy" +``` + + +## `Categorizer`: `PaperTag` and `PaperFolder` + +```typescript + +interface Categorizer { + _id: OID; + _partition: string; + name: string; + count: number; + color?: string; + children: Categorizer[]; // Readonly. Cannot be used when create or update. +} + +interface PaperTag { + _id: OID; + _partition: string; + name: string; + count: number; + color?: string; + children: PaperTag[]; // Readonly. Cannot be used when create or update. +} + +interface PaperFolder { + _id: OID; + _partition: string; + name: string; + count: number; + color?: string; + children: PaperFolder[]; // Readonly. Cannot be used when create or update. +} +``` + +This is the categorizer in Paperlib, that is, tags and folders. They are the same in data structure, but have different names in the database. You can use it like `PaperEntity`. + +## `PaperSmartFilter` + +```typescript +interface IPaperSmartFilter { + _id: OID; + _partition: string; + name: string; + filter: string; + color?: string; + children: PaperSmartFilter[]; // Readonly. Cannot be used when create or update. +} +``` + +This is the smart filter in Paperlib. Smart filters are a special kind of tags, which will automatically fill in the content of the search bar. You can use it like we show in the example of `PaperEntity`. + + +## `Feed` + +```typescript +interface Feed { + _id: OID; + id: OID; + _partition: string; + name: string; + count: number; + color?: string; + url: string; +} +``` + +This is the RSS feed in Paperlib. You can use it like we show in the example of `PaperEntity`. + +## `FeedEntity` + +```typescript +interface FeedEntity { + _id: OID; + id: OID; + _partition?: string; + addTime: Date; + feed: Feed; + feedTime: Date; + title: string; + authors: string; + abstract: string; + publication: string; + pubTime: string; + pubType: number; + doi: string; + arxiv: string; + mainURL: string; + pages: string; + volume: string; + number: string; + publisher: string; + read: boolean; +} +``` + +This is the RSS feed item in Paperlib. You can use it like we show in the example of `PaperEntity`. + +## `PaperFilterOptions` + +```typescript +interface IPaperFilterOptions { + search?: string; + searchMode?: "general" | "fulltext" | "advanced"; + flaged?: boolean; + tag?: string; + folder?: string; + limit?: number; +} +``` + +This is the `PaperEntity` filter options in Paperlib. You can filter out different papers by passing in different options. For example: + +```typescript +import { PaperFilterOptions, PLAPI } from 'paperlib-api/api'; + +const options: PaperFilterOptions = { + search: 'LLM', + searchMode: 'general', + flaged: true, + tag: 'tag1', +} + +// We can use this options to filter out the papers we want. +// The results are papers that contain 'LLM' in their title, +// and are flaged, and have tag 'tag1'. +const results = await PLAPI.paperService.load(options.toString(), ...) +``` + +## `FeedEntityFilterOptions` + +```typescript +interface IFeedEntityFilterOptions { + search?: string; + searchMode?: "general" | "fulltext" | "advanced"; + feedNames?: string[]; + unread?: boolean; + title?: string; + authors?: string; +} +``` + +This is the `FeedEntity` filter options in Paperlib. You can filter out different RSS feed items by passing in different options. You can use it like `PaperFilterOptions`. \ No newline at end of file diff --git a/src/en/extension-doc/demo.md b/src/en/extension-doc/demo.md new file mode 100644 index 0000000..8bc3338 --- /dev/null +++ b/src/en/extension-doc/demo.md @@ -0,0 +1,349 @@ +# Example Extension Development + +This article provides a guide for developing an example extension. The source code can be found on [Github](https://github.com/Future-Scholars/paperlib-demo-helloworld-extension). + +For other types of extensions, we also provide corresponding example extensions, which can be found on [Github](https://github.com/orgs/Future-Scholars/repositories). + +## Development Environment + +All the following examples are based on the example extension on [Github](https://github.com/Future-Scholars/paperlib-demo-helloworld-extension). Please clone it to your local first. Setup according to the instructions in [Development Environment](./env). + +## Extension Entry + +The extension entry file is `src/main.ts`, which exports a function `initialize`: + +```typescript +export { initialize } +``` + +The main task of this function is to create an extension class instance, initialize the extension, and return it. + +```typescript +// src/main.ts + +async function initialize() { + const extension = new PaperlibHelloworldExtension(); + await extension.initialize(); + + return extension; +} +``` + +In this example, we created an instance of the `PaperlibHelloworldExtension` class and called its `initialize` function. + +## Extension Class + +The extension class is the core of the extension. It is responsible for the initialization of the extension, lifecycle management, and providing the functionality of the extension. This extension class must inherit from the `PLExtension` class. + + +```typescript +// src/main.ts + +import { PLExtension } from "paperlib-api/api"; + +class PaperlibHelloworldExtension extends PLExtension { + disposeCallbacks: (() => void)[] = []; + + constructor() { + super({ + id: "the-name-in-package.json", + defaultPreference: { ... }, + }); + ... + } + + async initialize() { + // initialize the extension + ... + } + + async dispose() { + // dispose the extension, remove some event listeners, etc. + ... + } + + // other methods + ... +} + +``` + +The `PLExtension` class includes some necessary checks to prevent issues during extension development. For example, an extension must provide an `id`, and the extension's default preferences must follow certain standards. + +In the extension class, we need to define two functions: `initialize` and `dispose`. These two functions are called respectively when the extension is loading and unloading/uninstalling. In the `initialize` function, we can initialize the extension, such as registering event listeners, registering extension preferences, etc. In the `dispose` function, we can release the extension, such as canceling event listeners, etc. Besides, other functions and member variables of the extension can be freely defined. + +Next, we will detail these two functions. + +### `async initialize()` + +```typescript +// src/main.ts + +async initialize() { + await PLExtAPI.extensionPreferenceService.register( + this.id, + this.defaultPreference, + ); + + this.printSomething(); + + // 1. Command Extension Example + this.registerSomeCommands(); + + // 2. UI Extension Example + this.modifyPaperDetailsPanel(); + + // 3. Hook Extension Example + this.hookSomePoints(); +} +``` + +In the `initialize` function, we first registered the extension preferences through `PLExtAPI.extensionPreferenceService.register`. This way, users can see and modify the extension preferences in the Paperlib preferences interface. + +Then, we called the `printSomething` function, which will print some information in the Paperlib console. This is an example, meaning you can do anything you need here. + +After that, we ran three functions respectively for three types of extensions. We will detail the development of these three types of extensions below. Here, we take `registerSomeCommands()` as an example: + + +```typescript +// src/main.ts + +registerSomeCommands() { + // When the user choose to run the command, the PLAPI.commandService will + // emit a "command_echo_event" event. + // we get the message from the preference of this extension by calling PLExtAPI.extensionPreferenceService.get() + // + this.disposeCallbacks.push( + PLAPI.commandService.on("command_echo_event" as any, () => { + let msg = PLExtAPI.extensionPreferenceService.get(this.id, "msg"); + if (PLExtAPI.extensionPreferenceService.get(this.id, "signature")) { + if ( + PLExtAPI.extensionPreferenceService.get(this.id, "lang") === "zh" + ) { + msg += ` - 来自 SimpleCMD 扩展`; + } else { + msg += ` - from SimpleCMD Extension`; + } + } + + PLAPI.logService.info( + "Hello from the extension process", + msg, + true, + this.id, + ); + }), + ); + + // Register a command with event "command_echo_event". + this.disposeCallbacks.push( + PLAPI.commandService.registerExternel({ + id: "command_echo", + description: "Hello from the extension process", + event: "command_echo_event", + }), + ); +} + +``` + +In this example, the functionality we want to achieve is that when a user selects a command to run in the `Command Bar`, we receive this instruction and run some functions. + +1. First, we registered an event listener through `PLAPI.commandService.on`. When a user chooses to run a command, we will receive this event. In this event listener, our response is very simple, that is, we get some information from the extension preferences through `PLExtAPI.extensionPreferenceService.get`, and then print some information through `PLAPI.logService.info`. + +2. Then, we registered a command through `PLAPI.commandService.registerExternel`. This way, this command will appear in the user's `Command Bar`. When the user chooses to run this command, we will receive the event registered above. We need to provide the command's `id`, `description`, and `event`. `id` is the unique identifier of the command, `description` is the description of the command, and `event` is the event that will be emitted when the command is run. As you can see, this event is the one we registered to listen to above. + +3. All event listening, registration, etc., need to be `disposed` when the extension is unloaded to prevent memory leaks. These methods will return a function, calling this function can perform the corresponding `dispose`. We save these functions in `disposeCallbacks` so that they can be called in the `dispose` function. + +4. Notablly, in the callback of the event listener, please avoid `floating promise`. That is, if your callback function contains any `AsyncFunction`, please be sure to `await` or `.catch` the error exception. Because the error in the `floating promise` cannot be caught in Paperlib, it will cause the extension to crash. + +This is the main code of a `Command Extension`. You can call your other methods, do anything you need, etc., at the place where you listen to events. + +### `async dispose()` + +```typescript +// src/main.ts + +async dispose() { + PLExtAPI.extensionPreferenceService.unregister(this.id); + + for (const disposeCallback of this.disposeCallbacks) { + disposeCallback(); + } +} + +``` + +This is a function that must exist. Paperlib will call this function of the extension when reloading, unloading, etc. In the `dispose` function, we first cancel the registration of the extension preferences. Then, we call all the `dispose` functions saved in `disposeCallbacks` to release all resources of the extension, such as canceling event listeners, etc. If your extension has additional resources that need to be released, please release them here. + +### Extension Preferences + +In the above example, we registered the extension preferences through `PLExtAPI.extensionPreferenceService.register`. This way, users can see and modify the extension preferences in the Paperlib preferences interface. + +The default value of this preference is the `defaultPreference` passed in when constructing the `PaperlibHelloworldExtension` class instance. This `defaultPreference` is an instance, each key-value pair in it is a preference item. For example: + + +```typescript +// src/main.ts + +class PaperlibHelloworldExtension extends PLExtension { + constructor() { + super({ + id: "...", + defaultPreference: { + msg: { + type: "string", + name: "Message", + description: "Message to show when echo", + value: "Hello from the extension process", + order: 0, + }, + signature: { + type: "boolean", + name: "Signature", + description: "Show signature in the message", + value: false, + order: 1, + }, + lang: { + type: "options", + name: "Language", + description: "Language of the message", + options: { en: "English", zh: "Chinese" }, + value: "en", + order: 2, + }, + }, + }); + } +} + +``` + +In this example, we defined three preferences: `msg`, `signature`, `lang`. Among them, `msg` is a string type preference, `signature` is a boolean type preference, and `lang` is an option type preference. The default values of these preferences are `Hello from the extension process`, `false`, and `en` respectively. The order of these preferences is `0`, `1`, `2` respectively. In the Paperlib preferences interface, these preferences will be arranged in order and display different components according to different types, making it convenient for users to make changes. For detailed preference types, you can refer to [Extension Preferences](./preference). + +To access the value of a preference in the extension, you can get it through `PLExtAPI.extensionPreferenceService.get`. For example: + + +```typescript +PLExtAPI.extensionPreferenceService.get(this.id, "lang") +``` + +As a result, we can get the value of the `lang` preference. + +--- + +So far, we have completed the development of a `Command Extension`. Next, we will introduce the development of `Hook Extension` and `UI Extension`. + +## Data Structure + +In Paperlib, we have some important data structures, which are widely used in Paperlib, so we provide them to developers. You can import them from `paperlib-api/model` package: + +```typescript +import { + PaperEntity, + PaperTag, + PaperFolder, + Feed, + FeedEntity, + OID, +} from 'paperlib-api/model'; + +``` + +If your extension trying to deal with such data structures, please use we provided above. For more, please refer to [Data Structure](./data-structure)。 + +## Other Examples + +### Hook Extension + +This type of extension mainly targets those that need to intervene in the lifecycle of Paperlib. For example, we want to develop a new metadata scraper that automatically scrapes metadata from the internet when a user imports a paper. In fact, in Paperlib 3.0, all default scrapers already exist in the form of extensions. The code for these extensions can be found on [Github (Entry)](https://github.com/Future-Scholars/paperlib-entry-scrape-extension) [Github (Metadata)](https://github.com/Future-Scholars/paperlib-metadata-scrape-extension). They can serve as demos for `Hook Extensions`. + +For detailed development of `Hook Extensions`, you can refer to [Hook Extensions](./ext-types/hook-ext). Here, we use a simple example to illustrate the development of `Hook Extensions`. + +The main function of this part is that when a user imports a new file, we print some information. Here you can modify this information and return it to Paperlib to modify the data in the subsequent process of Paperlib. + + +```typescript +// src/main.ts + +hookSomePoints() { + this.disposeCallbacks.push( + PLAPI.hookService.hookModify( + "scrapeEntryBefore", + this.id, + "modifyPayloads", + ), + ); +} + +modifyPayloads(payloads: any[]) { + PLAPI.logService.info("modifyPayloads", `${payloads}`, true, this.id); + + // Modify the payloads here + // ... + + // Return the modified payloads + // For modify hook, the return value should be an array of args. + // For example, the original args array is [payloads: SomeType], then the return value should be also [payloads: SomeType] + return [payloads]; +} + +``` + +In this example, we first registered a `modify` type hook through `PLAPI.hookService.hookModify`. This hook point is `scrapeEntryBefore`, which will be triggered before Paperlib performs metadata retrieval. For information about hook types and hook points, please see [Hook Extensions](./ext-types/hook-ext). In this hook, we registered the name of the `modifyPayloads` function, which will be called when this hook is triggered. + +In this `modifyPayloads` function, we first printed some information, then you can modify `payloads` and return the modified `payloads`. + +Now, we have completed the development of a `Hook Extension`. + +### UI Extension + +This type of extension mainly targets those that need to modify the UI interface of Paperlib. For example, we want to add its citation count in the paper details panel of Paperlib, add other information related to the paper, etc. + +For detailed development of `UI Extensions`, you can refer to [UI Extensions](./ext-types/ui-ext). Here, we use a simple example to illustrate the development of `UI Extensions`. + + +```typescript +// src/main.ts + +modifyPaperDetailsPanel() { + this.disposeCallbacks.push( + PLAPI.uiStateService.onChanged("selectedPaperEntities", (newValues) => { + const selectedPaperEntities = newValues.value; + + if (selectedPaperEntities.length === 0) { + return; + } + + if (selectedPaperEntities.length === 1) { + const paperEntity = selectedPaperEntities[0]; + + PLAPI.uiSlotService.updateSlot("paperDetailsPanelSlot1", { + demo_section_id: { + title: "Demo Section", + content: `Any string here - ${Math.random()} - ${ + paperEntity.title + }}`, + }, + }); + } + }), + ); +} + +``` + +In this example, we first listen to whether the user's selected paper has changed. Because only when the user selects one paper, we will display the detail panel. + +At this time, we updated the `paperDetailsPanelSlot1` UI slot through `PLAPI.uiSlotService.updateSlot`. This slot is the first slot of Paperlib's paper details panel. We added a slot item with `demo_section_id` as the ID in this slot, where `title` is the title of the slot item, and `content` is the content of the slot item. In this way, we added a section with a title and content in the paper details panel. For slots provided by Paperlib, please see [UI Extensions](./ext-types/ui-ext). + +### New Window Extension + +This type of extension will create a brand new window to implement some complex functions. For example, we want to develop a new paper reading interface to implement some complex functions, such as reading papers, editing notes, annotating papers, etc. + +We developed a paper preview extension to provide Windows and Linux users with the same paper preview as Mac users. The code for this extension can be found on [Github](https://github.com/Future-Scholars/paperlib-preview-extension). This code can serve as a reference for `New Window Extensions`. + +For the development of `New Window Extensions`, you can refer to [New Window Extensions](./ext-types/new-window-ext). + + diff --git a/src/en/extension-doc/env.md b/src/en/extension-doc/env.md new file mode 100644 index 0000000..400feea --- /dev/null +++ b/src/en/extension-doc/env.md @@ -0,0 +1,78 @@ +# Setting Up Development Environment + +This article explains how to set up a local development environment for Paperlib extensions. + +We provide a basic development environment that supports the development of most extensions. This environment includes tools such as `typescript`, `vue`, `vite`, and more. + +Since a Paperlib extension is essentially an `npm` package, developers who are very familiar with `web/node` development can use their own development environment, build tools, etc., for development. Just follow some [conventions](./convention). + +## Download Development Environment + +We provide a basic development environment that you can use as a starting point for your development. This environment includes tools like `typescript`, `vue`, `vite`, and more. + +You can download it using the following command: + +```shell +git clone https://github.com/Future-Scholars/paperlib-extension-dev-env.git +``` + +## Install Dependencies + +```shell +cd paperlib-extension-dev-env +npm install +``` + +We have a `paperlib-api` package to provide API interface type definitions for autocompletion, along with some commonly used utility functions and the base class for extensions. Ensure it is updated to the latest version with the following command: + +```shell +npm install paperlib-api@latest -D +``` + +## `paperlib-api` Package + +In this package, we provide API type hints, commonly used data structures, some utility functions, and the base class for extensions. They are in different sub-modules of the `paperlib-api` package: + +- `paperlib-api/api`: API type definitions, and the base class `PLExtension` for extensions. +- `paperlib-api/model`: Commonly used data structures. +- `paperlib-api/utils`:Commonly used utility functions. +- `paperlib-api/rpc`: RPC service for communication between New Window extensions. + +## File Structure + +``` +├── dist // The directory where the packaged files are located +├── src // Source code directory +│ ├── main.ts // Extension entry file +│ ├── ... +│── vite.config.ts // vite configuration file +├── tsconfig.json // typescript configuration file +├── package.json // npm package configuration file + +``` + +## Build Extension + +Run the following command to build the extension and output the extension code to the `dist` directory. + +```shell +npm run build +``` + +## Load Extension for Testing + +In the extension tab of the Paperlib preference interface, you can load local extensions for testing. Click the `Load from Local` button and select the extension directory (the folder where `package.json` is located) to load the extension. + +After the extension is loaded, you can see the extension information in the extension tab of Paperlib. Click the `Reload` button of the extension to reload the extension for testing after modifying the extension. Please note that before reloading, you need to build the extension first after modifying the code. + +> For extension developers, you can open the developer mode in the about tab of the Paperlib preference interface to reload the extension directly in the developer tools shown on the main interface. + +## Publish Extension + +Our extension marketplace relies on `npmjs.com`. + +Each extension is an `npm` package, Publishing an extension is similar to publishing an `npm` package. Please make sure your extension follows the conventions listed in [Conventions](./convention). + +## Development Conventions + +Please refer to [Conventions](./convention). diff --git a/src/en/extension-doc/ext-types/command-ext.md b/src/en/extension-doc/ext-types/command-ext.md new file mode 100644 index 0000000..7d56126 --- /dev/null +++ b/src/en/extension-doc/ext-types/command-ext.md @@ -0,0 +1,84 @@ +# Command Extension + +The main feature of this type of extension is that it can register some commands in the command bar of Paperlib. When a user selects a command in Paperlib, the extension will receive a event and then execute the code in the extension. + +## Extension Class Structure + +Here we provide an example structure, of course, you can modify it according to your needs. + +```typescript +class CommandExtension extends PLExtension { + constructor() { + // You can set the id of the extension here, the default preference etc. + super(...) + } + + async initialize() { + // Register event listener here + this.disposeCallbacks.push( + await PLAPI.commandService.on(...) + ); + + // Register commands here, it is recommended to listen to events before registering + this.disposeCallbacks.push( + PLAPI.commandService.registerExternel(...) + ); + + ... + } + + async dispose() { + // Cancel command registration and event listening here + for (const disposeCallback of this.disposeCallbacks) { + disposeCallback(); + } + } +} +``` + +## Command Registration + +We can use the `PLAPI.commandService.registerExternel` method to register external commands used by your extension. + +> ⚠️ Do not use the `PLAPI.commandService.register` method, this method is used to register internal commands of Paperlib. + +The arguments of this method are as follows: + +```typescript +PLAPI.commandService.registerExternel({ + id: string, + description: string, + event: string, +}) +``` + +- `id`: The id of the command, which must be unique. You can use the format `extensionID.commandid`, for example `paperlib-helloworld.print`. +- `description`: The description of the command, used for display in the command panel. +- `event`: The event when the command is triggered. You can use the format `extensionID.eventid`, for example `paperlib-helloworld.print-event`. + +## Command Event + +When a user selects a command in Paperlib, the extension will receive an event. We can use the `PLAPI.commandService.on` method to listen to this event. This event listener also needs to run `disposeCallback` when the extension is destroyed to cancel the registration. + +The arguments of this method are as follows: + +```typescript +PLAPI.commandService.on({ + event: string, + callback: (event: {key: string, value: string[]}) => void, +}) +``` + +- `event`:Event when the command is triggered, the same as the `event` argument when registering the command. +- `callback`:The callback function when the command is triggered, this function receives an `event` argument, the `key` property of this argument is the id of the command, and the `value` property is the argument list of the command. For example, when the corresponding command is selected in the command panel, followed by the argument `test`, then the value of `event` is `{key: "your-event-name", value: ["test"]}`. + +## Cancel Command Registration + +We need to cancel the command registration when the extension is destroyed, otherwise it will cause memory leaks. It is the same as other similar event listening operations. When the extension uses the `PLAPI.commandService.registerExternel` method to register the command, the method will return a `disposeCallback`, we can save it in the extension class, and then call it when the extension is destroyed. + +```typescript +// Register command +const disposeCallback = await PLAPI.commandService.registerExternel(...); +// Call disposeCallback when the extension is destroyed +disposeCallback(); +``` \ No newline at end of file diff --git a/src/en/extension-doc/ext-types/hook-ext.md b/src/en/extension-doc/ext-types/hook-ext.md new file mode 100644 index 0000000..9190f6d --- /dev/null +++ b/src/en/extension-doc/ext-types/hook-ext.md @@ -0,0 +1,104 @@ +# Hook Extension + +This type of extension mainly intervene in the running process of Paperlib, modify the arguments and data flow in the process. For example, when a user drags a paper PDF into Paperlib, Paperlib will search for the metadata of the file and generate a PaperEntity in the database. We can use the Hook extension to intervene in this process, for example, after completing the metadata of a PaperEntity, we modify a part of the metadata. + +In fact, in Paperlib 3.0, all official Metadata Scrapers are implemented in extensions. + +## Extension Class Structure + +Here we provide an example structure, of course, you can modify it according to your needs. + +```typescript +class HookExtension extends PLExtension { + constructor() { + // You can set the id of the extension here, the default preference etc. + super(...) + } + + async initialize() { + // Register hook points here + this.disposeCallbacks.push( + PLAPI.hookService.hookModify( + "scrapeEntryBefore", + this.id, + "modifyPayloads", + ), + ); + + ... + } + + async dispose() { + // Cancel registration and event listening here + for (const disposeCallback of this.disposeCallbacks) { + disposeCallback(); + } + } + + async modifyPayloads(payloads: SourcePayload[]) { + // Modify payloads here + ... + + return [payloads] + } +} +``` + +## Hook Points + +In Paperlib, we have placed hooks with different names in different places. A hook extension can be registered to the corresponding hook point to intervene in the operation flow of Paperlib. There are two types of hook points in total. + +### Modify Hook Points + +- **Purpose**: This type of hook point is used to modify the arguments passed by the hook point, or the variables within the argument objects, but cannot change the type, and finally returns the modified arguments. + +- **Type of Callback Return Value**: Modify Hook requires the return value of the callback function to be an array, each element of which corresponds to the input argument array. For example, if the arguments passed by the hook point are `(arg1: string, arg2: {value: number})`, you can modify `arg1` to another string and `arg2.value` to another number in the callback function of the hook, but you cannot change the type of `arg1` to a number, or change the type of `arg2` to another type. The return value of the callback function must be an array composed of modified arguments: `[arg1, arg2]`. **Note that even if only one argument is passed in, an array with one element needs to be returned. Because the input arguments are always treated as an arguments array** + +### Transform Hook Points + +- **Purpose**: This type of hook point can modify the data flow in the operation process of Paperlib. It is used to transform the input arguments into other forms of data and then return. +- **Callback Return Value Type**: It can be other types, but usually different hook points have expected return value types. For example, the `scrapeEntry` hook point expects the return value type to be an array of `PaperEntity`. + + +## Avaliable hook points: + +The operation flows of Paperlib and the detailed introduction of hook points, please refer to [Operation Flows and Hooks](../process-hook). + +## Hook Registration + +We can use the `PLAPI.hookService.hookModify` method to register to a `Modify` hook point used by the extension, or use the `PLAPI.hookService.hookTransform` method to register to a `Transform` hook point used by the extension: + +```typescript +PLAPI.hookService.hookModify( + hookPoint: string, + extensionId: string, + callbackFuncName: string, +) + +PLAPI.hookService.hookTransform( + hookPoint: string, + extensionId: string, + callbackFuncName: string, +) +``` + +- `hookPoint`:Paperlib 提供的钩子点的 id,Hook point id provided by Paperlib, where you want to set the hook. +- `extensionId`:插件的 id,用于标识插件。Extension id, used to identify the extension. +- `callbackFuncName`:The name of the callback function in the extension, used to be find the callback function when the hook point is triggered. + +## Hook Callback Function + +The name of the hook callback function needs to be consistent with the `callbackFuncName` when registering. The form of the arguments and return values of the hook callback function vary according to the different hook positions and hook types. For details, please refer to [Operation Flows and Hooks](../process-hook). + +In the [Hook Points](#Hook-Points) section, we generally described the forms of the two types of hook callback functions. Here we don't repeat them. + +## Timeout Limitation + +Because hook extensions will intervene in the running process of Paperlib, we need to limit the running time of the hook callback function. If the running time of the hook callback function exceeds the preset time, Paperlib will forcibly bypass the running of the callback function: + +- `modifyHookPoint`:5s +- `transformHookPoint`:15s + +## Others + +Although our hook mechanism supports us to place hooks almost anywhere, because of the intrusiveness of hook extensions, we are very cautious about placing hook points. If the current hook point cannot meet the development needs of your extension, please go to [GitHub Discussions](https://github.com/Future-Scholars/paperlib/discussions) to submit your requirements, we will consider adding new hook points in future versions. \ No newline at end of file diff --git a/src/en/extension-doc/ext-types/new-window-ext.md b/src/en/extension-doc/ext-types/new-window-ext.md new file mode 100644 index 0000000..d5ede0f --- /dev/null +++ b/src/en/extension-doc/ext-types/new-window-ext.md @@ -0,0 +1,282 @@ +# New Window Extension + +This type of extension is the most complex. It creates a new window, within which there will be a brand new rendering process. This requires you to create the corresponding `RPC` service to communicate with the main process, rendering process, and extension process to get the exposed `API`. + +Inside the new window, you can use `React`, `Vue`, or any other framework you like to develop the UI. You just need to use an appropriate packaging tool to build and package your code into `.js` and `.html` files. + +To demonstrate the development of this type of extension, we provide a sample extension, which you can find on [GitHub](https://github.com/Future-Scholars/paperlib-preview-extension). The function of this extension is that in Windows and Linux systems, pressing the space will pop up a new window displaying the preview of the first page of the currently selected PDF. + +In the following sections, we will use this extension as an example. + +## Development Environment + +Similarly, we provide a basic development environment. You can find it on the `new-window` branch on [GitHub](https://github.com/Future-Scholars/paperlib-extension-dev-env). This environment uses `Vue` for UI development and `vite` for packaging. Of course, you can create the environment you like. + +The project structure is as follows: + + +``` +├── dist // packaged files +├── ext // extension source code directory +│ ├── src/main.ts // extension entry file +│ │── vite.config.ts // vite configuration file for extension +│ ├── ... +├── view // UI source code directory +│ ├── src // UI source code directory +│ ├── index.html // UI entry file +│ ├── vite.config.ts // vite configuration file for UI +│ ├── ... +├── tsconfig.json // typescript configuration file +├── package.json // npm configuration file + +``` + +When building, the code in the `ext` and `view` directories will be packaged into `dist/main.js` and `dist/view` respectively. You can see in `ext/main.ts` that we use `./view/index.html` to load the UI file of the new window. + + +## Extension Entry + +New Window extensions still need to create an extension entry file, which contains the extension class. These codes are the same as other types of extensions, and they all run in the extension process. The creation of the new window is also done there. + +### Extension Class Structure + +```typescript +class PaperlibPreviewExtension extends PLExtension { + disposeCallbacks: (() => void)[]; + + constructor() { + super({ + id: "@future-scholars/paperlib-preview-extension", + defaultPreference: {}, + }); + + this.disposeCallbacks = []; + } + + private async _createPreviewWindow() { + ... + } + + async initialize() { + ... + + try { + await this._createPreviewWindow(); + } catch (error) { + PLAPI.logService.error( + "Failed to create preview window", + error as Error, + true, + "Preview", + ); + } + + ... + } + + async dispose() { + ... + } +} +``` + +### Create a New Window + +When the extension is initializing, we call the `_createPreviewWindow` method to create a new window. In this method, we use `PLMainAPI.windowProcessManagementService` to create a new window. This service provides some methods to manage windows. + +```typescript +private async _createPreviewWindow() { + const screenSize = + await PLMainAPI.windowProcessManagementService.getScreenSize(); + await PLMainAPI.windowProcessManagementService.create( + "paperlib-preview-extension-window", + { + entry: path.resolve(__dirname, "./view/index.html"), + title: "Paper Preview", + width: Math.floor(screenSize.height * 0.8 * 0.75), + height: Math.floor(screenSize.height * 0.8), + minWidth: Math.floor(screenSize.height * 0.8 * 0.75), + minHeight: Math.floor(screenSize.height * 0.8), + useContentSize: true, + center: true, + resizable: false, + skipTaskbar: true, + webPreferences: { + webSecurity: false, + nodeIntegration: true, + contextIsolation: false, + }, + frame: false, + show: false, + }, + ); +} +``` + +The first argument of the `PLMainAPI.windowProcessManagementService.create` method is the `id` of the new window. This `id` is used to identify the window, and it is also the ID of the process corresponding to the window. The second argument is an object that contains some configurations of the new window. These configurations are the same as the options of `electron`'s `BrowserWindow`. You can find the detailed description of these options in the [electron document](https://www.electronjs.org/docs/api/browser-window#new-browserwindowoptions). + +In the above example, we get the size of the screen to limit the size of the new window. Note that here, we use `./view/index.html` to specify the UI entry file of the new window. + +### Listen to Window Events + +A window usually emits some events, such as close, blur, etc. We can use `PLMainAPI.windowProcessManagementService.on` to listen to these events. + +```typescript +PLMainAPI.windowProcessManagementService.on( + "paperlib-preview-extension-window" as any, + (newValues: { value: string }) => { + if (newValues.value === "blur") { + PLMainAPI.windowProcessManagementService.hide( + "paperlib-preview-extension-window", + ); + } + }, +) +``` + +The first argument of this method is the `id` of the window. The second argument is a callback function, which will be called when the window emits an event. The argument of the callback function is an object, which contains the type of the event. For example, here, we listen to the `blur` event, when the window loses focus, we hide it. + +For the window events, you can find them in the documentation of `PLMainAPI.windowProcessManagementService`. + +### Window Control + +We provide a series of methods to operate the window, such as hide, minimize, close, etc. You can find these methods in the documentation of `PLMainAPI.windowProcessManagementService`. + +In this example, we listen to the user pressing the Preview button in the menu bar, and then we show our new window: + +```typescript +PLMainAPI.menuService.onClick("View-preview", async () => { + PLMainAPI.windowProcessManagementService.show( + "paperlib-preview-extension-window", + ); +}) +``` + +## New Window UI + +In this example, we use `Vue` as the UI development framework. Please make sure that you are familiar with the basic `Vue` development knowledge before continuing to read. + +### UI Entry File + +In `view/index.html`, we set a `div` tag with an `id` of `app` to mount the `Vue` application. + +```html + + + + + + + + Paperlib Preview + + + +
    + + + +``` + +The entry file of the UI logic code is `view/src/index.ts`. + +### UI Logic Code + +In `view/src/index.ts`, we use the `createApp` method of `Vue` to create a `Vue` application. In the `createApp` method, we pass in an `AppView` component, which is the root component of our UI. + +```typescript +import { createApp } from "vue"; +import AppView from "./app.vue"; + +async function initialize() { + const app = createApp(AppView); + ... + app.mount("#app"); +} + +initialize(); +``` + +In the above initialization function, the most important part is to create the `RPC` service of this process: + +```typescript +import { createApp } from "vue"; + +import AppView from "./app.vue"; +import { RendererRPCService } from "paperlib-api/rpc"; +import { PreviewService } from "./services/preview-service"; + +async function initialize() { + const app = createApp(AppView); + + // ============================================================ + // 1. Initilize the RPC service for current process + const rpcService = new RendererRPCService("paperlib-preview-extension-window"); + // ============================================================ + // 2. Start the port exchange process. + await rpcService.initCommunication(); + + // ============================================================ + // 3. Wait for the main process to expose its APIs (PLMainAPI) + const mainAPIExposed = await rpcService.waitForAPI( + "mainProcess", + "PLMainAPI", + 5000 + ); + + if (!mainAPIExposed) { + throw new Error("Main process API is not exposed"); + } else { + console.log("Main process API is exposed"); + } + + // 4. Wait for the renderer process to expose its APIs (PLRendererAPI) + const rendererAPIExposed = await rpcService.waitForAPI( + "rendererProcess", + "PLAPI", + 5000 + ); + + if (!rendererAPIExposed) { + throw new Error("Renderer process API is not exposed"); + } else { + console.log("Renderer process API is exposed"); + } + + app.mount("#app"); +} + +initialize(); +``` + +In the above code, we first created an instance of `RendererRPCService`. This will create an `RPC` service in the current process. Then, we called the `rpcService.initCommunication()` method, which will notify other processes and establish the corresponding `MessagePort` communication channel to expose the corresponding `API`. Please refer to `services/rpc-service.ts` for more. + +The first argument must be consistant with the ID passed in when creating the new window. + +When the `rpcService.initCommunication()` method is executed, we can use the `rpcService.waitForAPI` method to wait for other processes to expose their corresponding `API`. Here, we wait for the main process and the rendering process to expose `PLMainAPI` and `PLAPI`. If you also need to access the corresponding service of the extension process, such as `PLExtAPI.extensionPreferenceService`, you can also wait for the extension process to expose `PLExtAPI` here: + +```typescript +const extAPIExposed = await rpcService.waitForAPI( + "extensionProcess", // Process ID + "PLExtAPI", // API name + 5000 // Timeout +); + +if (!extAPIExposed) { + throw new Error("Ext process API is not exposed"); +} else { + console.log("Ext process API is exposed"); +} +``` + +So far, you can access the services exposed by other processes through `PLAPI, PLMainAPI, PLExtAPI` in the process of the new window. + + +### Service in New Window + +In the new window, we can implement any function we want. There are almost no restrictions here, just like developing an WebAPP. + +In our demo extension, we get the selected paper, get the path of the PDF document, and then render it to the new window. You can find this part of the code in `services/preview-service.ts`. \ No newline at end of file diff --git a/src/en/extension-doc/ext-types/simple-ext.md b/src/en/extension-doc/ext-types/simple-ext.md new file mode 100644 index 0000000..f2bc433 --- /dev/null +++ b/src/en/extension-doc/ext-types/simple-ext.md @@ -0,0 +1,55 @@ +# Simple Extension + +This type of extension is the simplest. You only needs to create an extension class that inherits from `PLExtension` and implements `initialize` and `dispose`. And the export of the extension entry file contains a function called `initialize`: + +```typescript + +import { PLAPI, PLExtAPI, PLExtension, PLMainAPI } from "paperlib-api/api"; + +class PaperlibHelloworldExtension extends PLExtension { + disposeCallbacks: (() => void)[]; + + constructor() { + super({ + id: "...", + defaultPreference: { + ... + }, + }); + + this.disposeCallbacks = []; + } + + async initialize() { + await PLExtAPI.extensionPreferenceService.register( + this.id, + this.defaultPreference, + ); + + this.printSomething(); + + } + + async dispose() { + PLExtAPI.extensionPreferenceService.unregister(this.id); + + for (const disposeCallback of this.disposeCallbacks) { + disposeCallback(); + } + } + + printSomething() { + console.log("Hello world from extension!"); + } +} + +async function initialize() { + const extension = new PaperlibHelloworldExtension(); + await extension.initialize(); + + return extension; +} + +export { initialize }; + +``` \ No newline at end of file diff --git a/src/en/extension-doc/ext-types/ui-ext.md b/src/en/extension-doc/ext-types/ui-ext.md new file mode 100644 index 0000000..406eded --- /dev/null +++ b/src/en/extension-doc/ext-types/ui-ext.md @@ -0,0 +1,37 @@ +# UI Extension + +This type of extension can display additional information in some panels of Paperlib. For example, in the paper detail panel, display the citation count of the paper. + +## Extension Class Structure + +Here we provide an example structure, of course, you can modify it according to your needs. + +```typescript +class UIExtension extends PLExtension { + constructor() { + // You can set the id of the extension here, the default preference etc. + super(...) + } + + async initialize() { + // Listen to events and modify UI here + this.disposeCallbacks.push( + PLAPI.uiStateService.onChanged("...", (newValues) => { + PLAPI.uiSlotService.updateSlot(...); + }), + ); + ... + } + + async dispose() { + // Cancel event listening here + for (const disposeCallback of this.disposeCallbacks) { + disposeCallback(); + } + } +} +``` + +## Others + +If the current slot cannot meet the development needs of your extension, please go to [GitHub Discussions](https://github.com/Future-Scholars/paperlib/discussions) to submit your needs, we will consider adding new slots in future versions. \ No newline at end of file diff --git a/src/en/extension-doc/index.md b/src/en/extension-doc/index.md new file mode 100644 index 0000000..f5ee125 --- /dev/null +++ b/src/en/extension-doc/index.md @@ -0,0 +1,71 @@ +# Extension System + +We are pleased to announce that in Paperlib 3.0, which means you can develop extensions for feature expansion in Paperlib! 🎉🎉🎉 + +## Preface + +This article introduces the overall design of the extension system to facilitate understanding of each part that may be involved in the development process. If you just want to develop a simple extension, you can skip this article. If you are developing a relatively complex extension, we highly recommend reading this article. + +## Process and Extension Runtime Environment + +To compromise multi-platform development, Paperlib chooses to be based on Electron. Therefore, in the entire Paperlib, we have three main processes: + +- Main Process: Responsible for managing the entire program's lifecycle and interacting with the system, such as context menus, etc. +- Renderer Process: Responsible for rendering the interface and most of the program logic. +- Extension Process: Responsible for extension management and operation. + +Each extension runs in the extension process, and the code is executed in a separate [node VM](https://nodejs.org/api/vm.html). Therefore, the crash of an extension will not cause Paperlib to crash, and each extension does not interfere with each other. Each extension runs in a `node` environment, without `html` and other related web environments (New Window Extension is an exception). + +## Inter-process Communication + +In addition to running the extension's code, the extension process is also responsible for extension downloading, updating, loading, uninstalling, and preference settings. Most of the logic code of Paperlib runs in the main or rendering process. Therefore, we extends `MessagePort` to implement `Remote Procedure Calling (RPC)` to expose the methods of various services in the main/rendering process for calling in the extension process. + +Each cross-process method will be internally convert into a `json` string and sent to the corresponding process. This includes the method to be called, the arguments, and other necessary information. After the corresponding process receives the message, it will parse the `json`, run the corresponding method, get the result and return it to the process where the caller is located. In addition, we also implement cross-process event listening to make development more convenient. For specific implementation details, please refer to [Github](https://github.com/Future-Scholars/paperlib/tree/dev-3.0.0/app/base/rpc). + +## Service + +In each process, there are many services that provide various methods to complete the functions of Paperlib. For example, the `ContextMenuService` of the main process is responsible for the right-click menu related functions, and the `PaperService` in the rendering process is responsible for the main paper-related additions, deletions, modifications, and queries. The relevant implementation can be found on [Github](https://github.com/Future-Scholars/paperlib/tree/dev-3.0.0/app/renderer/services). + +Almost all methods of the services in Paperlib are exposed to the extension process for calling. Almost all parts of Paperlib can be operated in the extension process. + +## Service Event + +Almost all services are `Eventable`. This means that each service will emit some events at different stages. Other codes/processes can listen for corresponding events to execute their own code. For example, you can listen to the user's selected paper changes and then run your method. + +## Cross-process Call API + +For convenience of development, we expose the services of the main process, rendering process, and extension process to the extension process, and concentrate them in three corresponding APIs. You will often encounter these three API groups in the following documents: + +- `PLMainAPI`: Contains all the main process services. +- `PLAPI`: Contains all the rendering process services. +- `PLExtAPI`: Contains all the extension process services. + +When you call a method of a service in a specific process, you only need to write code as following in your extension: + +```ts +// const result = await APIGroup.serviceName.methodName(...) +const papers = await PLAPI.paperService.load(...) +``` +Because it is a cross-process call, we need to use `await` to wait for the return of asynchronous results. If you are calling `PLExtAPI`, because it is the same process as your extenstion's, whether it is synchronous or asynchronous depends on the situation, please refer to the type autocompletion when developing. + +## Extension Types + +We have five different types of extensions: + +- Simple Extension: Simple functions, run some things on their own. +- Command Extension: Users run commands through the command bar introduced by Paperlib 3.0, and then the extension runs the corresponding functions. +- Hook Extension: Register hooks at different hook points in the Paperlib life cycle to intercept and modify the pipeline. +- UI Extension: Modify parts of the UI interface. +- New Window Extension: Create a completely new window to implement customized complex functions. + +These five types of extensions cover most extension scenarios and can be mixed with each other. We provide corresponding examples and will explain in detail later. + +## Extension Publishing and Downloading + +Each extension is an `npm` package, publishing is similar to other `npm` packages. We will explain the restrictions later. + +In the Paperlib's preference interface's extension page, extensions can be loaded through a local path or downloaded by searching the extension marketplace. Our extension marketplace relies on `npmjs.com`. + +## Conclusion + +In summary, an extension is some code in the form of an `npm` package that runs in a separate process and operates Paperlib through the APIs we expose. Next, we recommend reading the [Development Prepare](./env) to start developing your extension! \ No newline at end of file diff --git a/src/en/extension-doc/manifest-version.md b/src/en/extension-doc/manifest-version.md new file mode 100644 index 0000000..437f6ab --- /dev/null +++ b/src/en/extension-doc/manifest-version.md @@ -0,0 +1,32 @@ +--- +title: "Manifest Version" +--- + + + +# Manifest Version + +**The current manifest version is `{{ version }}`.** + +--- + +The `manifest_version` field must be included in `package.json` to indicate the API version used by the extension. Please keep it consistent with the version of the `paperlib-api` package you actually installed. We recommend that you always use the latest API version. + +```json +// package.json +{ + "manifest_version": "x.x.x" +} +`` \ No newline at end of file diff --git a/src/en/extension-doc/plapi/cache-service.md b/src/en/extension-doc/plapi/cache-service.md new file mode 100644 index 0000000..f45e3fd --- /dev/null +++ b/src/en/extension-doc/plapi/cache-service.md @@ -0,0 +1,74 @@ +# CacheService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.cacheService.methodname(...); +``` + +## Avaliable Methods + +### `fullTextFilter` + +```typescript +/** + * Filter the fulltext cache of the provided papers by the given query. + * @param query - The query to filter the fulltext cache by. + * @param paperEntities - The paper entities to filter. + * @returns The filtered paper entities. */ +fullTextFilter(query: string, paperEntities: IPaperEntityCollection): Promise>; +``` + +### `loadThumbnail` + +```typescript +/** + * Get the thumbnail of the paper entity. + * @param paperEntity - The paper entity to get the thumbnail of. + * @returns The thumbnail of the paper entity. */ +loadThumbnail(paperEntity: PaperEntity): Promise; +``` + +### `updateFullTextCache` + +```typescript +/** + * Update the fulltext cache of the provided paper entities. + * @param paperEntities - The paper entities to update the fulltext cache of. + */ +updateFullTextCache(paperEntities: IPaperEntityCollection): Promise; +``` + +### `updateThumbnailCache` + +```typescript +/** + * Update the thumbnail cache + * @param paperEntity - PaperEntity + * @param thumbnailCache - Cache of thumbnail + */ +updateThumbnailCache(paperEntity: PaperEntity, thumbnailCache: ThumbnailCache): Promise; +``` + +### `updateCache` + +```typescript +/** + * Update the cache of the provided paper entities. + * @param paperEntities - The paper entities. + * @returns + */ +updateCache(paperEntities: IPaperEntityCollection): Promise; +``` + +### `delete` + +```typescript +/** + * Delete the cache of the provided paper entity ids. + * @param ids - The ids of the paper entities to delete the cache of. + */ +delete(ids: OID[]): Promise; +``` \ No newline at end of file diff --git a/src/en/extension-doc/plapi/categorizer-service.md b/src/en/extension-doc/plapi/categorizer-service.md new file mode 100644 index 0000000..9383974 --- /dev/null +++ b/src/en/extension-doc/plapi/categorizer-service.md @@ -0,0 +1,112 @@ +# CategorizerService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.categorizerService.methodname(...); +``` + +## Avaliable Methods + +### `load` + +```typescript +/** + * Load categorizers. + * @param type - The type of the categorizer. + * @param sortBy - Sort by + * @param sortOrder - Sort order + * @returns + */ +load(type: CategorizerType, sortBy: string, sortOrder: string): Promise; +``` + +### `loadByIds` + +```typescript +/** + * Load categorizers by ids. + * @param type - The type of the categorizer. + * @param ids - The ids of the categorizers. + * @returns + */ +loadByIds(type: CategorizerType, ids: OID[]): Promise; +``` + +### `create` + +```typescript +/** + * Update a categorizer. + * @param type - The type of categorizer. + * @param categorizer - The categorizer. + * @param parent - The parent categorizer. + * @returns + */ +create(type: CategorizerType, categorizer: Categorizer, parent?: Categorizer): Promise; +``` + +### `delete` + +```typescript +/** + * Delete a categorizer. + * @param type - The type of categorizer. + * @param name - The name of categorizer. + * @param categorizer - The categorizer. + * @returns + */ +delete(type: CategorizerType, ids?: OID[], categorizers?: ICategorizerCollection): Promise; +``` + +### `colorize` + +```typescript +/** + * Colorize a categorizer. + * @param id - The id of the categorizer. + * @param color - The color. + * @param type - The type of the categorizer. + * @returns + */ +colorize(id: OID, color: Colors, type: CategorizerType): Promise; +``` + +### `rename` + +```typescript +/** + * Rename a categorizer. + * @param id - The id of the categorizer. + * @param name - The new name of the categorizer. + * @param type - The type of the categorizer. + * @returns + */ +rename(id: OID, name: string, type: CategorizerType): Promise; +``` + +### `update` + +```typescript +/** + * Update/Insert a categorizer. + * @param type - The type of the categorizer. + * @param categorizer - The categorizer. + * @param parentCategorizer - The parent categorizer to insert. + * @returns + */ +update( + type: CategorizerType, + categorizer: Categorizer, + parentCategorizer?: Categorizer +): Promise; +``` + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| `tagsUpdated` | `{key: 'tagsUpdated'}` | When Tags database are updated | +| `foldersUpdated` | `{key: 'foldersUpdated'}` | When Folders database are updated | \ No newline at end of file diff --git a/src/en/extension-doc/plapi/command-service.md b/src/en/extension-doc/plapi/command-service.md new file mode 100644 index 0000000..954a207 --- /dev/null +++ b/src/en/extension-doc/plapi/command-service.md @@ -0,0 +1,50 @@ +# CommandService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.commandService.methodname(...); +``` + +## Avaliable Methods + +### `getRegisteredCommands` + +```typescript +/** +* Get registered commands. +* @param filter - Filter string +* @returns - Sorted array of filtered commands +*/ +getRegisteredCommands(filter?: string): Promise; +``` + +### `run` + +```typescript +/** +* Run command. +* @param id - Command ID +* @param args - Command arguments +*/ +run(id: string, ...args: any[]): Promise; +``` + +### `registerExternel` + +```typescript +/** +* Register externel command. +* @param command - Externel command +*/ +registerExternel(command: IExternelCommand): () => Promise; +``` + + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| `your-registed-command-event` | `{key: 'your-registed-command-event', value: someargs}` | When Tags database are updated | \ No newline at end of file diff --git a/src/en/extension-doc/plapi/database-service.md b/src/en/extension-doc/plapi/database-service.md new file mode 100644 index 0000000..e50db66 --- /dev/null +++ b/src/en/extension-doc/plapi/database-service.md @@ -0,0 +1,43 @@ +# DatabaseService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.databaseService.methodname(...); +``` + +## Avaliable Methods + +### `initialize` + +```typescript +/** +* Initialize the database. +* @param reinit - Whether to reinitialize the database. */ +initialize(reinit?: boolean): Promise; +``` + +### `pauseSync` + +```typescript +/** +* Pause the synchronization of the cloud database. */ +pauseSync(): Promise; +``` + +### `resumeSync` + +```typescript +/** +* Resume the synchronization of the cloud database. */ +resumeSync(): Promise; +``` + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| `dbInitializing` | `{key: 'dbInitializing'}` | When database is initilizing | +| `dbInitialized` | `{key: 'dbInitialized'}` | When database is initialized | \ No newline at end of file diff --git a/src/en/extension-doc/plapi/feed-service.md b/src/en/extension-doc/plapi/feed-service.md new file mode 100644 index 0000000..145d26b --- /dev/null +++ b/src/en/extension-doc/plapi/feed-service.md @@ -0,0 +1,123 @@ +# FeedService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.feedService.methodname(...); +``` + +## Avaliable Methods + +### `load` + +```typescript +/** + * Load feeds. + * @param sortBy - Sort by. + * @param sortOrder - Sort order. + * @returns Feeds. + */ +load(sortBy: string, sortOrder: string): Promise; +``` + +### `loadEntities` + +```typescript +/** + * Load feed entities from the database. + * @param filter - Filter. + * @param sortBy - Sort by. + * @param sortOrder - Sort order. + * @returns Feed entities. + */ +loadEntities(filter: FeedEntityFilterOptions, sortBy: string, sortOrder: "asce" | "desc"): Promise; +``` + +### `update` + +```typescript +/** + * Update feeds. + * @param feeds - Feeds. + * @returns Updated feeds. + */ +update(feeds: IFeedCollection): Promise; +``` + +### `updateEntities` + +```typescript +/** + * Update feed entities. + * @param feedEntities - Feed entities + * @param ignoreReadState - Ignore read state. Default: false. + * @returns Updated feed entities. + */ +updateEntities(feedEntities: IFeedEntityCollection, ignoreReadState?: boolean): Promise; +``` + +### `create` + +```typescript +/** + * Create feeds. + * @param feeds - Feeds + */ +create(feeds: Feed[]): Promise; +``` + +### `refresh` + +```typescript +/** + * Refresh feeds. + * @param ids - Feed ids + * @param feeds - Feeds + * @returns + */ +refresh(ids?: OID[], feeds?: IFeedCollection): Promise; +``` + +### `colorize` + +```typescript +/** + * Colorize a feed. + * @param color - Color + * @param id - Feed ID + * @param feed - Feed + */ +colorize(color: Colors, id?: OID, feed?: IFeedObject): Promise; +``` + +### `delete` + +```typescript +/** + * Delete a feed. + * @param ids - Feed IDs + * @param feeds - Feeds + */ +delete(ids?: OID[], feeds?: IFeedCollection): Promise; +``` + +### `addToLib` + +```typescript +/** + * Add feed entities to library. + * @param feedEntities - Feed entities + */ +addToLib(feedEntities: IFeedEntityCollection): Promise; +``` + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| `updated` | `{key: 'updated'}` | When Feed database is updated | +| `entitiesUpdated` | `{key: 'entitiesUpdated'}` | When FeedEntity database is initialized | +| `entitiesCount` | `{key: 'entitiesCount', value: count}` | When FeedEntity database count is changed | + diff --git a/src/en/extension-doc/plapi/file-service.md b/src/en/extension-doc/plapi/file-service.md new file mode 100644 index 0000000..e2c7f2e --- /dev/null +++ b/src/en/extension-doc/plapi/file-service.md @@ -0,0 +1,182 @@ +# FileService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.fileService.methodname(...); +``` + +## Avaliable Methods + + +### `initialize` + +```typescript +/** +* Initialize the file backend. +*/ +initialize(): Promise; +``` + +### `startWatch` + +```typescript +/** + * Start watching file changes. (Only for WebDAV file backend) + */ +startWatch(): Promise; +``` + +### `stopWatch` + +```typescript +/** + * Stop watching file changes. (Only for WebDAV file backend) + */ +stopWatch(): Promise; +``` + +### `check` + +```typescript +/** + * Check if the file backend is available. + * @returns Whether the file backend is available. + */ +check(): Promise; +``` + +### `move` + +``` typescript +/** + * Move files of a paper entity to the library folder + * @param paperEntity - Paper entity to move + * @param moveMain - Move the main file + * @param moveSups - Move the supplementary files + * @returns + */ +move(paperEntity: PaperEntity, moveMain?: boolean, moveSups?: boolean): Promise; +``` + +### `moveFile` + +``` typescript +/** + * Move a file + * @param sourceURL - Source file URL + * @param targetURL - Target file URL + * @returns The target file URL + */ +moveFile(sourceURL: string, targetURL: string): Promise; +``` + +### `remove` + +```typescript +/** + * Remove files of a paper entity + * @param paperEntity - Paper entity to remove + */ +remove(paperEntity: PaperEntity): Promise; +``` + +### `removeFile` + +```typescript +/** + * Remove a file + * @param url - Url of the file to remove + */ +removeFile(url: string): Promise; +``` + +### `listAllFiles` + +```typescript +/** + * List all files in a folder + * @param folderURL - Url of the folder + * @returns List of file names + */ +listAllFiles(folderURL: string): Promise; +``` + +### `locateFileOnWeb` + +```typescript +/** + * Locate the paper files, such as the PDF, of paper entities. + * @param paperEntities - The paper entities. + * @returns The paper entities with the located file URLs. + */ +locateFileOnWeb(paperEntities: PaperEntity[]): Promise; +``` + +### `access` + +```typescript +/** + * Return the real and accessable path of the URL. + * If the URL is a local file, return the path of the file. + * If the URL is a remote file and `download` is `true`, download the file and return the path of the downloaded file. + * If the URL is a web URL, return the URL. + * @param url + * @param download + * @returns The real and accessable path of the URL. + */ +access(url: string, download: boolean): Promise; +``` + +### `open` + +```typescript +/** + * Open the URL. + * @param url - URL to open + */ +open(url: string): Promise; +``` + +### `showInFinder` + +```typescript +/** + * Show the URL in Finder / Explorer. + * @param url - URL to show + */ +showInFinder(url: string): Promise; +``` + +### `preview` + +```typescript +/** + * Preview the URL only for MacOS. + * Other platforms should install an extension. + * @param url - URL to preview + */ +preview(url: string): Promise; + +``` + +### `inferRelativeFileName` +```typescript +/** + * Infer the relative path of a paper entity. + * @param paperEntity - Paper entity to infer the relative path + */ +inferRelativeFileName(paperEntity: PaperEntity): Promise; +``` + + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| `backend` | `{key: 'backend', value: backendName}` | When file backend is changed | +| `available` | `{key: 'available', value: available}` | When file backend available status is changed | +| `backendInitializing` | `{key: 'backendInitializing'}` | When file backend is initializing | +| `backendInitialized` | `{key: 'backendInitialized'}` | When file backend is initialized | \ No newline at end of file diff --git a/src/en/extension-doc/plapi/hook-service.md b/src/en/extension-doc/plapi/hook-service.md new file mode 100644 index 0000000..87473e2 --- /dev/null +++ b/src/en/extension-doc/plapi/hook-service.md @@ -0,0 +1,49 @@ +# HookService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.hookService.methodname(...); +``` + +## Avaliable Methods + +### `hasHook` + +```typescript +/** + * Check if a hook point exists. + * @param hookName - Name of the hook point + * @returns Whether the hook point exists + */ +hasHook(hookName: string): false | "modify" | "transform"; +``` + +### `hookModify` + +```typescript +/** + * Hook a modify hook point. + * @param hookName - Name of the hook point + * @param extensionID - ID of the extension + * @param callbackName - Name of the callback function + * @returns A function to dispose the hook + */ +hookModify(hookName: string, extensionID: string, callbackName: string): () => void; +``` + +### `hookTransform` + +```typescript +/** + * Hook a transform hook point. + * @param hookName - Name of the hook point + * @param extensionID - ID of the extension + * @param callbackName - Name of the callback function + * @returns A function to dispose the hook + */ +hookTransform(hookName: string, extensionID: string, callbackName: string): () => void; +``` + diff --git a/src/en/extension-doc/plapi/index.md b/src/en/extension-doc/plapi/index.md new file mode 100644 index 0000000..4418a11 --- /dev/null +++ b/src/en/extension-doc/plapi/index.md @@ -0,0 +1,30 @@ +# PLAPI + +In this group of APIs, there are many services and their provided methods in the main renderer process. + +In your extension, you can call them by: + +```typescript +import { PLAPI } from "paperlib-api/api"; + +const results = await PLAPI.serviceName.methodName(...) +``` + +## Services + +- `logService`: Log service, used to record information, warnings, logs. You can pop up notifications in the lower left corner of the notification center to inform users. +- `cacheService`: Cache service, used to cache some data, such as the full text of the paper, thumbnails, etc. +- `categorizerService`: Categorizer service, used to manage tags and groups. +- `commandService`: Command service, used to register and execute commands. +- `databaseService`: Database service, used to initialize the database. +- `feedService`: RSS service, used to operate RSS-related content. +- `fileService`: File service, used to operate files. +- `hookService`: Hook service, used to register and execute hooks. +- `paperService`: Paper service, used to operate papers. +- `referenceService`: Reference service, used to export references. +- `renderService`: Render service, used to render PDF, markdown, etc. +- `scrapeService`: Scrape service, used to convert data sources to `PaperEntity`, search for paper metadata. +- `smartFilterService`: Smart filter service, used to operate smart filters. +- `uiStateService`: UI state service, used to operate UI state. +- `preferenceService`: Preference service, used to operate preferences. +- `uiSlotService`: UI slot service, used to operate UI slots. \ No newline at end of file diff --git a/src/en/extension-doc/plapi/log-service.md b/src/en/extension-doc/plapi/log-service.md new file mode 100644 index 0000000..a8663b6 --- /dev/null +++ b/src/en/extension-doc/plapi/log-service.md @@ -0,0 +1,90 @@ +# LogService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.logService.methodname(...); +``` + +## Avaliable Methods + +### `log` + +```typescript +/** + * Log info to the console and the log file. + * @param {string} level - Log level + * @param {string} msg - Message to log + * @param {string?} additional - Additional message to log + * @param {boolean?} notify - Show notification in the notification bar, default: false + * @param {string?} id - ID of the log, usually indicates who log this info */ +log(level: "info" | "warn" | "error", msg: string, additional?: string, notify?: boolean, id?: string): Promise; +``` + +### `info` + +```typescript +/** + * Log info to the console and the log file. + * @param {string} msg - Message to log + * @param {string?} additional - Additional message to log + * @param {boolean?} notify - Show notification, default: false + * @param {string?} id - ID of the log */ +info(msg: string, additional?: string, notify?: boolean, id?: string): Promise; +``` + +### `warn` + +```typescript +/** + * Log warning to the console and the log file. + * @param {string} msg - Message to log + * @param {string?} additional - Additional message to log + * @param {boolean?} notify - Show notification, default: false + * @param {string?} id - ID of the log */ +warn(msg: string, additional?: string, notify?: boolean, id?: string): Promise; +``` + +### `error` +```typescript +/** + * Log error to the console and the log file. + * @param {string} msg - Message to log + * @param {string?} additional - Additional message to log + * @param {boolean?} notify - Show notification, default: false + * @param {string?} id - ID of the log */ +error(msg: string, additional?: string | Error, notify?: boolean, id?: string): Promise; +``` + +### `progress` + +```typescript +/** + * Log progress to the console and the log file. + * @param {string} msg - Message to log + * @param {number?} value - Progress value + * @param {boolean?} notify - Show notification, default: false + * @param {string?} id - ID of the log */ +progress(msg: string, value: number, notify?: boolean, id?: string, progressId?: string): Promise; +``` + +### `getLogFilePath` + +```typescript +/** + * Get log file path. + * @returns {string} Log file path */ +getLogFilePath(): Promise; +``` + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| `infoLogMessage` | `{key: 'infoLogMessage', value: msg}` | When a new info message is logged | +| `warnLogMessage` | `{key: 'warnLogMessage', value: msg}` | When a new warning message is logged | +| `errorLogMessage` | `{key: 'errorLogMessage', value: msg}` | When a new error message is logged | +| `progressLogMessage` | `{key: 'progressLogMessage', value: percent}` | When a new progress is logged | + diff --git a/src/en/extension-doc/plapi/paper-service.md b/src/en/extension-doc/plapi/paper-service.md new file mode 100644 index 0000000..04c713d --- /dev/null +++ b/src/en/extension-doc/plapi/paper-service.md @@ -0,0 +1,167 @@ +# PaperService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.paperService.methodname(...); +``` + +## Avaliable Methods + +### `load` + +```typescript +/** + * Load paper entities with filter and sort. + * @param querySentence - Query sentence, string or PaperFilterOptions + * @param sortBy - Sort by + * @param sortOrder - Sort order + * @returns Paper entities + */ +load(querySentence: string, sortBy: string | undefined, sortOrder: "asce" | "desc", fulltextQuerySentence?: string): Promise; +``` + +### `loadByIds` + +```typescript +/** + * Load paper entities by IDs. + * @param ids - Paper entity ids + * @returns Paper entities + */ +loadByIds(ids: OID[]): Promise>>; +``` + +### `update` + +```typescript +/** + * Update paper entities. + * @param paperEntityDrafts - paper entity drafts + * @param updateCache - Update cache, default is true + * @param isUpdate - Is update, default is false, if false, it is insert. This is for preventing insert duplicated papers. + * @returns Updated paper entities + */ +update(paperEntityDrafts: IPaperEntityCollection, updateCache?: boolean, isUpdate?: boolean): Promise; +``` + +### `updateWithCategorizer` + +```typescript +/** + * Update paper entities with a categorizer. + * @param ids - The list of paper IDs. + * @param categorizer - The categorizer. + * @param type - The type of the categorizer. + */ +updateWithCategorizer(ids: OID[], categorizer: Categorizer, type: CategorizerType): Promise; +``` + +### `updateMainURL` + +```typescript +/** + * Update the main file of a paper entity. + * @param paperEntity - The paper entity. + * @param url - The URL of the main file. + * @returns The updated paper entity. + */ +updateMainURL(paperEntity: PaperEntity, url: string): Promise; +``` + +### `updateSupURLs` + +```typescript +/** + * Update the supplementary files of a paper entity. + * @param paperEntity - The paper entity. + * @param urls - The URLs of the supplementary files. + */ +updateSupURLs(paperEntity: PaperEntity, urls: string[]): Promise; + +``` + +### `delete` + +```typescript +/** + * Delete paper entities. + * @param ids - Paper entity ids + * @param paperEntity - Paper entities + */ +delete(ids?: OID[], paperEntities?: PaperEntity[]): Promise; +``` + +### `deleteSup` + +```typescript +/** + * Delete a suplementary file. + * @param paperEntity - The paper entity. + * @param url - The URL of the supplementary file. + */ +deleteSup(paperEntity: PaperEntity, url: string): Promise; +``` + +### `create` + +```typescript +/** + * Create paper entity from file URLs. + * @param urlList - The list of URLs. + * @returns The list of paper entity drafts. + */ +create(urlList: string[]): Promise; +``` + +### `createIntoCategorizer` + +```typescript +/** + * Create paper entity from a URL with a given categorizer. + * @param urlList - The list of URLs. + * @param categorizer - The categorizer. + * @param type - The type of categorizer. + * @returns The list of paper entity drafts. + */ +createIntoCategorizer(urlList: string[], categorizer: Categorizer, type: CategorizerType): Promise; +``` + +### `scrape` + +```typescript +/** + * Scrape paper entities. + * @param paperEntities - The list of paper entities. + * @param specificScrapers - The list of specific scrapers. + */ +scrape(paperEntities: IPaperEntityCollection, specificScrapers?: string[]): Promise; +``` + +### `scrapePreprint` + +```typescript +/** + * Scrape preprint paper entities. + */ +scrapePreprint(): Promise; +``` + +### `renameAll` + +```typescript +/** + * Rename all paper entities. + */ +renameAll(): Promise; +``` + + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| `updated` | `{key: 'updated'}` | When PaperEntity database is updated | +| `count` | `{key: 'count', value: count | When FeedEntity database count is changed | \ No newline at end of file diff --git a/src/en/extension-doc/plapi/preference-service.md b/src/en/extension-doc/plapi/preference-service.md new file mode 100644 index 0000000..46b9485 --- /dev/null +++ b/src/en/extension-doc/plapi/preference-service.md @@ -0,0 +1,132 @@ +# PreferenceService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.preferenceService.methodname(...); +``` + +## Avaliable Methods + +### `get` + +```typescript +/** + * Get the value of the preference + * @param key - Key of the preference + * @returns Value of the preference + */ +get(key: keyof IPreferenceStore): Promise; +``` + +### `set` + +```typescript +/** + * Set the value of the preference + * @param patch - Patch object + */ +set(patch: Partial): Promise; +``` + +### `getPassword` + +```typescript +/** + * Get the password + * @param key - Key of the password + * @returns Password + */ +getPassword(key: string): Promise; +``` + +### `setPassword` + +```typescript +/** + * Set the password + * @param key - Key of the password + * @param pwd - Password + */ +setPassword(key: string, pwd: string): Promise; +``` + + +## Avaliable Preferences + +```typescript +declare interface IPreferenceStore { + preferenceVersion: number; + windowSize: { + height: number; + width: number; + }; + appLibFolder: string; + sourceFileOperation: "cut" | "copy" | "link"; + showSidebarCount: boolean; + isSidebarCompact: boolean; + mainTableFields: IDataViewField[]; + feedFields: IDataViewField[]; + preferedTheme: "light" | "dark" | "system"; + invertColor: boolean; + sidebarSortBy: "name" | "count" | "color"; + sidebarSortOrder: "asce" | "desc"; + renamingFormat: "full" | "short" | "authortitle" | "custom"; + customRenamingFormat: string; + language: string; + enableExportReplacement: boolean; + exportReplacement: Array<{ + from: string; + to: string; + }>; + useSync: boolean; + syncCloudBackend: string; + isFlexibleSync: boolean; + syncAPPID: ""; + syncAPIKey: string; + syncEmail: string; + syncFileStorage: string; + webdavURL: string; + webdavUsername: string; + webdavPassword: string; + allowRoutineMatch: boolean; + lastRematchTime: number; + lastFeedRefreshTime: number; + allowproxy: boolean; + httpproxy: string; + httpsproxy: string; + lastVersion: string; + lastDBVersion: number; + shortcutPlugin: string; + shortcutPreview: string; + shortcutOpen: string; + shortcutCopy: string; + shortcutScrape: string; + shortcutEdit: string; + shortcutFlag: string; + shortcutCopyKey: string; + shortcutDelete: string; + sidebarWidth: number; + detailPanelWidth: number; + mainviewSortBy: string; + mainviewSortOrder: "desc" | "asce"; + mainviewType: string; + mainviewShortAuthor: boolean; + pluginLinkedFolder: string; + selectedPDFViewer: string; + selectedPDFViewerPath: string; + selectedCSLStyle: string; + importedCSLStylesPath: string; + showPresetting: boolean; + fontsize: "normal" | "large" | "larger"; +} + +``` + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| any preference key listed above | `{key: prefKey, value: newValue}` | When preference is changed | diff --git a/src/en/extension-doc/plapi/reference-service.md b/src/en/extension-doc/plapi/reference-service.md new file mode 100644 index 0000000..6d3b758 --- /dev/null +++ b/src/en/extension-doc/plapi/reference-service.md @@ -0,0 +1,116 @@ +# ReferenceService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.referenceService.methodname(...); +``` + +## Avaliable Methods + +### `replacePublication` + +```typescript +/** + * Abbreviate the publication name according to the abbreviation list set in the preference interface. + * @param source - The source paper entity. + * @returns The paper entity with publication name abbreviated. + */ +replacePublication(source: PaperEntity): PaperEntity; +``` + +### `toCite` + +```typescript +/** + * Convert paper entity to citationjs object. + * @param source - The source paper entity. + * @returns The cite object. + */ +toCite(source: PaperEntity | PaperEntity[] | string): any; +``` + +### `exportBibTexKey` + +```typescript +/** + * Export BibTex key. + * @param paperEntities - The paper entities. + * @returns The BibTex key. + */ +exportBibTexKey(paperEntities: PaperEntity[]): string; +``` + +### `exportBibTexBody` + +```typescript +/** + * Export BibTex body string. + * @param paperEntities - The paper entities. + * @returns The BibTex body string. + */ +exportBibTexBody(paperEntities: PaperEntity[]): string; +``` + +### `exportBibTex` + +```typescript +/** + * Export plain text. + * @param paperEntities - The paper entities. + * @returns The plain text. + */ +exportPlainText(paperEntities: PaperEntity[]): Promise; +``` + +### `exportCSV` +```typescript +/** + * Export papers as csv string. + * @param paperEntities - The paper entities. + * @returns The CSV string. + */ +exportCSV(paperEntities: PaperEntity[]): Promise; +``` + +### `exportBibTexKeyInFolder` +```typescript +/** + * Export BibTex body string in folder. + * @param folderName - The folder name. + */ +exportBibTexBodyInFolder(folderName: string): Promise; +``` + +### `exportBibTexBodyInFolder` +```typescript +/** + * Export plain text in folder. + * @param folderName - The folder name. + */ +exportPlainTextInFolder(folderName: string): Promise; +``` + +### `exportBibItem` +```typescript +/** + * Export BibItem. + * @param paperEntities - The paper entities. + * @returns The BibItem. + */ +exportBibItem(paperEntities: PaperEntity[]): Promise; +``` + + +### `export` + +```typescript +/** + * Export paper entities. + * @param paperEntities - The paper entities. + * @param format - The export format: "BibTex" | "BibTex-Key" | "PlainText" + */ +export(paperEntities: PaperEntity[], format: string): Promise; +``` \ No newline at end of file diff --git a/src/en/extension-doc/plapi/render-service.md b/src/en/extension-doc/plapi/render-service.md new file mode 100644 index 0000000..08b6100 --- /dev/null +++ b/src/en/extension-doc/plapi/render-service.md @@ -0,0 +1,80 @@ +# RenderService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.renderService.methodname(...); +``` + +## Avaliable Methods + + +### `renderPDF` + +```typescript +/** + * Render PDF file to canvas + * @param fileURL - File url + * @param canvasId - Canvas id + * @returns Renderer blob: {blob: ArrayBuffer | null, width: number, height: number} + */ +renderPDF(fileURL: string, canvasId: string): Promise<{ + blob: ArrayBuffer | null; + width: number; + height: number; +}>; +``` + +### `renderPDFCache` + +```typescript +/** + * Render PDF cache to canvas + * @param cachedThumbnail - Cached thumbnail + * @param canvasId - Canvas id + */ +renderPDFCache(cachedThumbnail: ThumbnailCache, canvasId: string): Promise; +``` + +### `renderMarkdown` + +```typescript +/** + * Render Markdown to HTML + * @param content - Markdown content + * @param renderFull - Render full content or not, default is false. If false, only render first 10 lines. + * @returns Rendered string: {renderedStr: string, overflow: boolean} + */ +renderMarkdown(content: string, renderFull?: boolean): Promise<{ + renderedStr: string; + overflow: boolean; +}>; +``` + +### `renderMarkdownFile` + +```typescript +/** + * Render Markdown file to HTML + * @param url - File url + * @param renderFull - Render full content or not, default is false. If false, only render first 10 lines. + * @returns Rendered string: {renderedStr: string, overflow: boolean} + */ +renderMarkdownFile(url: string, renderFull?: boolean): Promise<{ + renderedStr: string; + overflow: boolean; +}>; +``` + +### `renderMath` + +```typescript +/** + * Render Math to HTML + * @param content - Math content + * @returns Rendered HTML string + */ +renderMath(content: string): Promise; +``` \ No newline at end of file diff --git a/src/en/extension-doc/plapi/scrape-service.md b/src/en/extension-doc/plapi/scrape-service.md new file mode 100644 index 0000000..46bbef2 --- /dev/null +++ b/src/en/extension-doc/plapi/scrape-service.md @@ -0,0 +1,45 @@ +# ScrapeService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.scrapeService.methodname(...); +``` + +## Avaliable Methods + +### `scrape` + +```typescript +/** + * Scrape a data source's metadata. + * @param payloads - data source payloads. + * @param specificScrapers - list of metadata scrapers. + * @param force - force scraping metadata. + * @returns List of paper entities. */ +scrape(payloads: any[], specificScrapers: string[], force?: boolean): Promise; +``` + +### `scrapeEntry` + +```typescript +/** + * Scrape all entry scrapers to transform data source payloads into a PaperEntity list. + * @param payloads - data source payloads. + * @returns List of paper entities. */ +scrapeEntry(payloads: any[]): Promise; +``` + +### `scrapeMetadata` + +```typescript +/** + * Scrape all metadata scrapers to complete the metadata of PaperEntitys. + * @param paperEntityDrafts - list of paper entities. + * @param scrapers - list of metadata scrapers. + * @param force - force scraping metadata. + * @returns List of paper entities. */ +scrapeMetadata(paperEntityDrafts: PaperEntity[], scrapers: string[], force?: boolean): Promise; +``` \ No newline at end of file diff --git a/src/en/extension-doc/plapi/smartfilter-service.md b/src/en/extension-doc/plapi/smartfilter-service.md new file mode 100644 index 0000000..f6f8471 --- /dev/null +++ b/src/en/extension-doc/plapi/smartfilter-service.md @@ -0,0 +1,93 @@ +# SmartFilterService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.smartFilterService.methodname(...); +``` + +## Avaliable Methods + +### `load` + +```typescript +/** + * Load smartfilters. + * @param type - The type of the smartfilter + * @param sortBy - Sort by + * @param sortOrder - Sort order + * @returns + */ +load(type: PaperSmartFilterType, sortBy: string, sortOrder: string): Promise; +``` + +### `loadByIds` + +```typescript +/** + * Load smartfilters by ids. + * @param ids - The ids of the smartfilters + * @returns + */ +loadByIds(ids: OID[]): Promise; +``` + +### `delete` + +```typescript +/** + * Delete a smartfilter. + * @param type - The type of the smartfilter + * @param ids - The ids of the smartfilters + * @param smartfilters - The smartfilters + */ +delete(type: PaperSmartFilterType, ids?: OID[], smartfilters?: IPaperSmartFilterCollection): Promise; +``` + +### `colorize` + +```typescript +/** + * Colorize a smartfilter. + * @param id - The id of the smartfilter. + * @param color - The color. + * @param type - The type of the smartfilter. + * @returns + */ +colorize(id: OID, color: Colors, type: PaperSmartFilterType): Promise; +``` + +### `rename` +```typescript + /** + * Rename a smartfilter. + * @param id - The id of the smartfilter. + * @param name - The new name of the smartfilter. + * @param type - The type of the smartfilter. + * @returns + */ +rename(id: OID, name: string, type: PaperSmartFilterType): Promise; +``` + +### `update` +```typescript +/** + * Update/Insert a smartfilter. + * @param type - The type of the smartfilter + * @param smartfilter - The smartfilter + * @param parentSmartfilter - The parent smartfilter + * @returns + */ +update(type: PaperSmartFilterType, smartfilter: PaperSmartFilter, parentSmartfilter?: PaperSmartFilter): Promise; +``` + + + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| `updated` | `{key: updated}` | When PaperSmartFilter database is updated | + diff --git a/src/en/extension-doc/plapi/uislot-service.md b/src/en/extension-doc/plapi/uislot-service.md new file mode 100644 index 0000000..fb15d23 --- /dev/null +++ b/src/en/extension-doc/plapi/uislot-service.md @@ -0,0 +1,56 @@ +# UISlotService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.uiSlotService.methodname(...); +``` + +## Avaliable Methods + +```typescript +/** + * Update a slot with the given patch + * @param slotID - The slot to update + * @param patch - The patch to apply to the slot + * @returns + */ +updateSlot(slotID: keyof IUISlotState, patch: { + [id: string]: any; +}): Promise; +``` + +## Avaliable Slots + +```typescript + +interface IUISlotState { + paperDetailsPanelSlot1: { + [id: string]: { title: string; content: string }; + }; + paperDetailsPanelSlot2: { + [id: string]: { title: string; content: string }; + }; + paperDetailsPanelSlot3: { + [id: string]: { title: string; content: string }; + }; + overlayNotifications: { + [id: string]: { title: string; content: string }; + }; +} + +All support HTML string. + +``` + + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| `paperDetailsPanelSlot1` | `{key: paperDetailsPanelSlot1, value: newSlotState}` | When slot `paperDetailsPanelSlot1` is updated | +| `paperDetailsPanelSlot2` | `{key: paperDetailsPanelSlot2, value: newSlotState}` | When slot `paperDetailsPanelSlot2` is updated | +| `paperDetailsPanelSlot3` | `{key: paperDetailsPanelSlot3, value: newSlotState}` | When slot `paperDetailsPanelSlot3` is updated | +| `overlayNotifications` | `{key: overlayNotifications, value: newSlotState}` | When slot `overlayNotifications` is updated | diff --git a/src/en/extension-doc/plapi/uistate-service.md b/src/en/extension-doc/plapi/uistate-service.md new file mode 100644 index 0000000..37b515f --- /dev/null +++ b/src/en/extension-doc/plapi/uistate-service.md @@ -0,0 +1,103 @@ +# UIStateService + +## Call + +```typescript +import { PLAPI } from "paperlib-api/api"; + +PLAPI.uiStateService.methodname(...); +``` + +## Avaliable Methods + +### `setState` + +```typescript +/** + * Set the state of the UI service. Many UI components are controlled by the UI states. + * @param patch - patch to the state. It can be a single state, a partial state or a full state. + */ +setState(patch: Partial): Promise; +``` + +### `getState` + +```typescript +/** + * Get the UI state. + * @param stateKey - key of the state + * @returns The state + */ +getState(stateKey: keyof IUIStateServiceState): Promise; +``` + +### `getStates` + +```typescript +/** + * Get all UI states. + * @returns The state + */ +getStates(): Promise>; +``` + +### `resetState` + +```typescript +/** + * Reset all UI states to default. + */ +resetStates(): Promise; +``` + +## Avaliable States + +```typescript + +interface IUIStateServiceState { + // ========================================= + // Main Paper/Feed panel + contentType: string; // "library" | "feed" + mainViewFocused: boolean; + editViewShown: boolean; + feedEditViewShown: boolean; + paperSmartFilterEditViewShown: boolean; + preferenceViewShown: boolean; + deleteConfirmShown: boolean; + overlayNoticationShown: boolean; + renderRequired: number; // When assign a new value to this state, the rendering of some components, such as the PDF preview, will be triggered. + + entitiesReloaded: number; + + // selectedIndex: contains the index of the selected papers in the dataview. + // It should be the only state that is used to control the selection. + selectedIndex: Array; + // selectedIds: contains the ids of the selected papers in the current dataview. + // It can be accessed in any component. But it is read-only. It can be only changed by the event listener of selectedIndex in the dataview. + selectedIds: Array; + // selectedPaperEntities/selectedFeedEntities: contains the selected paper/feed entities in the current dataview. + // It can be accessed in any component. But it is read-only. It can be only changed by the event listener of selectedIndex in the dataview. + selectedPaperEntities: Array; + selectedFeedEntities: Array; + selectedQuerySentenceId: string; + selectedFeed: string; + editingPaperSmartFilter: PaperSmartFilter; + querySentenceSidebar: string; + querySentenceCommandbar: string; + + dragingIds: Array; + + // ========================================= + // Command / Search Bar + commandBarText: string; + commandBarSearchMode: string; // "general" | "advanced" | "fulltext" +} + +``` + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| any state key listed above | `{key: stateKey, value: newValue}` | When state is changed | + diff --git a/src/en/extension-doc/plextapi/extensionmanagement-service.md b/src/en/extension-doc/plextapi/extensionmanagement-service.md new file mode 100644 index 0000000..4d943d6 --- /dev/null +++ b/src/en/extension-doc/plextapi/extensionmanagement-service.md @@ -0,0 +1,106 @@ +# ExtensionManagementService + +## Call + +```typescript +import { PLExtAPI } from "paperlib-api/api"; + +PLExtAPI.extensionManagementService.methodname(...); +``` + +## Avaliable Methods + +### `loadInstalledExtensions` +```typescript +/** + * Load all installed extensions. + */ +loadInstalledExtensions(): Promise; +``` + +### `install` +```typescript +/** + * Install an extension from the given path or extensionID. + * @param extensionIDorPath - extensionID or path to the extension + * @param notify - whether to show notification, default to true + */ +install(extensionIDorPath: string, notify?: boolean): Promise; +``` + +### `uninstall` +```typescript +/** + * Uninstall an extension. + * @param extensionID - extensionID to uninstall + */ +uninstall(extensionID: string): Promise; +``` + +### `clean` +```typescript +/** + * Clean the extension related files, preference, etc. + * @param extensionIDorPath - extensionID or path to the extension + */ +clean(extensionIDorPath: string): Promise; +``` + +### `reload` +```typescript +/** + * Reload an extension. + * @param extensionID - extensionID to reload + */ +reload(extensionID: string): Promise; +``` + +### `reloadAll` +```typescript +/** + * Reload all installed extensions. + */ +reloadAll(): Promise; +``` + +### `installedExtensions` +```typescript +/** + * Get all installed extensions. + */ +installedExtensions(): Promise<{ + [key: string]: IExtensionInfo; +}>; +``` + +### `listExtensionMarketplace` + +```typescript +/** + * Get extensions from marketplace. + * @param query - Query string + * @returns A map of extensionID to extension info. + */ +listExtensionMarketplace(query: string): Promise<{ + [id: string]: { + id: string; + name: string; + version: string; + author: string; + verified: boolean; + description: string; + }; +}>; +``` + +### `callExtensionMethod` +```typescript +/** + * Call a method of an extension class. + * @param extensionID - extensionID to call method + * @param methodName - method name to call + * @param args - arguments to pass to the method + * @returns + */ +callExtensionMethod(extensionID: string, methodName: string, ...args: any): Promise; +``` \ No newline at end of file diff --git a/src/en/extension-doc/plextapi/extensionpreference-service.md b/src/en/extension-doc/plextapi/extensionpreference-service.md new file mode 100644 index 0000000..fc685ec --- /dev/null +++ b/src/en/extension-doc/plextapi/extensionpreference-service.md @@ -0,0 +1,133 @@ +# ExtensionPreferenceService + +## Call + +```typescript +import { PLExtAPI } from "paperlib-api/api"; + +PLExtAPI.extensionPreferenceService.methodname(...); +``` + +## Avaliable Methods + +### `register` + +```typescript +/** + * Register a preference store. + * @param extensionID - extension ID + * @param defaultPreference - default preference + */ +register(extensionID: string, defaultPreference: T): Promise; +``` + +### `unregister` + +```typescript +/** + * Unregister a preference store. + * @param extensionID - extension ID + */ +unregister(extensionID: string): Promise; +``` + +### `get` + +```typescript +/** + * Get the value of the preference + * @param extensionID - extension ID + * @param key - key of the preference + * @returns value of the preference or null + */ +get(extensionID: string, key: any): Promise; +``` + +### `getAll` + +```typescript +/** + * Get the value of all preferences + * @param extensionID - extension ID + * @returns value of all preferences + */ +getAll(extensionID: string): Promise>; +``` + +### `getMetadata` + +```typescript +/** + * Get the metadata of the preference + * @param extensionID - extension ID + * @param key - key of the preference + * @returns metadata of the preference or null + */ +getMetadata(extensionID: string, key: any): Promise; +``` + +### `getAllMetadata` + +```typescript +/** + * Get the metadata of all preferences + * @param extensionID - extension ID + * @returns metadata of all preferences + */ +getAllMetadata(extensionID: string): Promise>; +``` + +### `set` + +```typescript +/** + * Set the value of the preference + * @param extensionID - extension ID + * @param patch - patch object + * @returns + */ +set(extensionID: string, patch: any): Promise; +``` + +### `getPassword` + +```typescript +/** + * Get the password + * @param extensionID - extension ID + * @param key - key of the password + * @returns - password + */ +getPassword(extensionID: string, key: string): Promise; +``` + +### `setPassword` + +```typescript +/** + * Set the password + * @param extensionID - extension ID + * @param key - key of the password + * @param pwd - password + */ +setPassword(extensionID: string, key: string, pwd: string): Promise; +``` + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| `extensionID:prefKey` | `{key: 'extensionID:prefKey', value: prefValue}` | When the preference is changed | + +## Example + +```typescript +import { PLExtAPI } from "paperlib-api/api"; + +PLExtAPI.extensionPreferenceService.onChanged( + 'extensionID:prefKey', + (newValue: {key: string, value: any}) => { + console.log(newValue.value); +}); \ No newline at end of file diff --git a/src/en/extension-doc/plextapi/index.md b/src/en/extension-doc/plextapi/index.md new file mode 100644 index 0000000..fa75614 --- /dev/null +++ b/src/en/extension-doc/plextapi/index.md @@ -0,0 +1,20 @@ +# PLExtAPI + +In this group of APIs, most of the services and their methods in the extension process are included. + +As the services in PLExtAPI are running in the same process as the extension, some methods are synchronous, and some are asynchronous. + +In your extension, you can call them by: + +```typescript +import { PLExtAPI } from "paperlib-api/api"; + +const syncResults = PLExtAPI.serviceName.methodName(...) +const asyncResults = await PLExtAPI.serviceName.methodName(...) +``` + +## 可用的服务 + +- `extensionManagementService`: Extension management service, responsible for extension installation, loading, unloading, etc. +- `extensionPreferenceService`: Extension preference service, responsible for reading and writing extension preferences. +- `networkTool`: Network tool, provides methods such as `get`, `post`, `download`. \ No newline at end of file diff --git a/src/en/extension-doc/plextapi/network-tool.md b/src/en/extension-doc/plextapi/network-tool.md new file mode 100644 index 0000000..8a4db9f --- /dev/null +++ b/src/en/extension-doc/plextapi/network-tool.md @@ -0,0 +1,129 @@ +# NetworkTool + +## Call + +```typescript +import { PLExtAPI } from "paperlib-api/api"; + +PLExtAPI.networkTool.methodname(...); +``` + +## Avaliable Methods + +### `setProxyAgent` + +```typescript +/** + * Set proxy agent + * @param httpproxy - HTTP proxy + * @param httpsproxy - HTTPS proxy + */ +setProxyAgent(httpproxy?: string, httpsproxy?: string): void; +``` + +### `checkSystemProxy` + +```typescript +/** + * Check system proxy, if exists, set it as proxy agent + */ +checkProxy(): Promise; +``` + +### `get` + +```typescript +/** + * HTTP GET + * @param url - URL + * @param headers - Headers + * @param retry - Retry times + * @param timeout - Timeout + * @param cache - Use cache + * @param parse - Try to parse response body + * @returns Response + */ +get(url: string, headers?: Record, retry?: number, timeout?: number, cache?: boolean, parse?: boolean): Promise<{ + body: any; + status: number; + statusText: string; + headers: Record; +}>; +``` + +### `post` + +```typescript +/** + * HTTP POST + * @param url - URL + * @param data - Data + * @param headers - Headers + * @param retry - Retry times + * @param timeout - Timeout + * @param compress - Compress data + * @param parse - Try to parse response body + * @returns Response + */ +post(url: string, data: Record | string, headers?: Record, retry?: number, timeout?: number, compress?: boolean, parse?: boolean): Promise<{ + body: any; + status: number; + statusText: string; + headers: Record; +}>; +``` + +### `postForm` + +```typescript +/** + * HTTP POST with form data + * @param url - URL + * @param data - Data + * @param headers - Headers + * @param retry - Retry times + * @param timeout - Timeout + * @returns Response + */ +postForm(url: string, data: FormData, headers?: Record, retry?: number, timeout?: number, parse?: boolean): Promise<{ + body: any; + status: number; + statusText: string; + headers: Record; +}>; +``` + +### `download` + +```typescript +/** + * Download + * @param url - URL + * @param targetPath - Target path + * @param cookies - Cookies + * @returns Target path + */ +download(url: string, targetPath: string, cookies?: CookieJar | ICookieObject[]): Promise; +``` + +### `downloadPDFs` + +```typescript +/** + * Download PDFs + * @param urlList - URL list + * @param cookies - Cookies + * @returns Target paths + */ +downloadPDFs(urlList: string[], cookies?: CookieJar | ICookieObject[]): Promise; +``` + +### `connected` + +```typescript +/** + * Check if the network is connected + * @returns Whether the network is connected + */ +connected(): Promise; +``` \ No newline at end of file diff --git a/src/en/extension-doc/plmainapi/contextmenu-service.md b/src/en/extension-doc/plmainapi/contextmenu-service.md new file mode 100644 index 0000000..743665d --- /dev/null +++ b/src/en/extension-doc/plmainapi/contextmenu-service.md @@ -0,0 +1,82 @@ +# ContextMenuService + +## Call + +```typescript +import { PLMainAPI } from "paperlib-api/api"; + +PLMainAPI.contextMenuService.methodname(...); +``` + +## Avaliable Methods + +### `registerScraperExtension` +```typescript +/** + * Registers a scraper extension. It will be shown in the context menu. + */ +registerScraperExtension(extID: string, scrapers: { + [id: string]: string; +}): Promise; +``` + +### `unregisterScraperExtension` +```typescript +/** + * Unregisters a scraper extension. + * @param {string} extID - The ID of the extension. + */ +unregisterScraperExtension(extID: string): Promise; +``` + +### `registerContextMenu` +```typescript +/** + * Registers context menus form extensions. + * @param extID - The id of the extension to register menus + * @param items - The menu items to be registered + */ +registerContextMenu(extID: string, items: { + id: string; + label: string; +}[]): void; +``` + +### `unregisterContextMenu` +```typescript +/** + * Registers context menus form extensions. + * @param extID - The id of the extension to unregister menu items + */ +unregisterContextMenu(extID: string): void; +``` + + + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| `dataContextMenuScrapeFromClicked` | `{key: 'dataContextMenuScrapeFromClicked', value: scraperID}` | When `Scrape From` is clicked in the context menu of a paper in the library | +| `dataContextMenuOpenClicked` | `{key: 'dataContextMenuOpenClicked'}` | When `Open` is clicked in the context menu of a paper in the library | +| `dataContextMenuShowInFinderClicked` | `{key: 'dataContextMenuShowInFinderClicked'}` | When `Show in Finder` is clicked in the context menu of a paper in the library | +| `dataContextMenuEditClicked` | `{key: 'dataContextMenuEditClicked'}` | When `Edit` is clicked in the context menu of a paper in the library | +| `dataContextMenuScrapeClicked` | `{key: 'dataContextMenuScrapeClicked'}` | When `Scrape` is clicked in the context menu of a paper in the library | +| `dataContextMenuDeleteClicked` | `{key: 'dataContextMenuDeleteClicked'}` | When `Delete` is clicked in the context menu of a paper in the library | +| `dataContextMenuFlagClicked` | `{key: 'dataContextMenuFlagClicked'}` | When `Flag` is clicked in the context menu of a paper in the library | +| `dataContextMenuExportBibTexClicked` | `{key: 'dataContextMenuExportBibTexClicked'}` | When `Export BibTex` is clicked in the context menu of a paper in the library | +| `dataContextMenuExportBibTexKeyClicked` | `{key: 'dataContextMenuExportBibTexKeyClicked'}` | When `Export BibTex Key` is clicked in the context menu of a paper in the library | +| `dataContextMenuExportPlainTextClicked` | `{key: 'dataContextMenuExportPlainTextClicked'}` | When `Export Plain Text` is clicked in the context menu of a paper in the library | +| `dataContextMenuExportCSVClicked` | `{key: 'dataContextMenuExportCSVClicked'}` | When `Export CSV` is clicked in the context menu of a paper in the library | +| `feedContextMenuAddToLibraryClicked` | `{key: 'feedContextMenuAddToLibraryClicked'}` | When `Add to Library` is clicked in the context menu of a feed in the library | +| `feedContextMenuToggleReadClicked` | `{key: 'feedContextMenuToggleReadClicked'}` | When `Toggle Read` is clicked in the context menu of a feed in the library | +| `sidebarContextMenuFeedRefreshClicked` | `{key: 'sidebarContextMenuFeedRefreshClicked', value: {data: feedID}}` | When `Refresh` is clicked in the context menu of a feed in the sidebar | +| `sidebarContextMenuEditClicked` | `{key: 'sidebarContextMenuEditClicked', value: {data: id, type: Categorizer or Feed}}` | When `Edit` is clicked in the context menu of a feed in the sidebar | +| `sidebarContextMenuColorClicked` | `{key: 'sidebarContextMenuColorClicked', value: {data: id, type: Categorizer or Feed, color: color}}` | When `Color` is clicked in the context menu of a feed in the sidebar | +| `sidebarContextMenuDeleteClicked` | `{key: 'sidebarContextMenuDeleteClicked', value: {data: id, type: Categorizer or Feed}}` | When `Delete` is clicked in the context menu of a feed in the sidebar | +| `supContextMenuDeleteClicked` | `{key: 'supContextMenuDeleteClicked'}` | When `Delete` is clicked in the context menu of a supplementary file in the library | +| `thumbnailContextMenuReplaceClicked` | `{key: 'thumbnailContextMenuReplaceClicked'}` | When `Replace` is clicked in the context menu of a thumbnail in the library | +| `thumbnailContextMenuRefreshClicked` | `{key: 'thumbnailContextMenuRefreshClicked'}` | When `Refresh` is clicked in the context menu of a thumbnail in the library | +| `linkToFolderClicked` | `{key: 'linkToFolderClicked'}` | When `Link to Folder` is clicked in Quickpaste UI | +| `dataContextMenuFromExtensionsClicked` | `{ extID: string; itemID: string}` | When a context menu item from an extension is clicked | +| `dataContextMenuExportBibItemClicked` | `{key: 'dataContextMenuExportBibItemClicked'}` | When `Export BibItem` is clicked in the context menu of a paper in the library | \ No newline at end of file diff --git a/src/en/extension-doc/plmainapi/filesystem-service.md b/src/en/extension-doc/plmainapi/filesystem-service.md new file mode 100644 index 0000000..b80abde --- /dev/null +++ b/src/en/extension-doc/plmainapi/filesystem-service.md @@ -0,0 +1,63 @@ +# FileSystemService + +## Call + +```typescript +import { PLMainAPI } from "paperlib-api/api"; + +PLMainAPI.fileSystemService.methodname(...); +``` + +## Avaliable Methods + +### `getSystemPath` + +```typescript +/** + * Get the path of the given key. + * @param {string} key - The key to get the path of. + * @returns {string} - The path of the given key. + */ +getSystemPath(key: "home" | "appData" | "userData" | "sessionData" | "temp" | "exe" | "module" | "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos" | "recent" | "logs" | "crashDumps", windowId: string): Promise; +``` + +### `showFilePicker` + +```typescript +/** + * Show a file picker. + * @returns {Promise} The result of the file picker. + */ +showFilePicker(): Promise; +``` + +### `showFolderPicker` + +```typescript +/** + * Show a folder picker. + * @returns {Promise} The result of the folder picker. + */ +showFolderPicker(): Promise; +``` + +### `showSaveDialog` + +```typescript +/** + * Preview a file. + * @param {string} fileURL - The URL of the file to preview. + */ +preview(fileURL: string): Promise; +``` + +### `writeToFile` +```typescript +/** + * Write some text to a file. + * @param {string} filePath The path of the file to write to. + * @param {string} text The text to write to the file. + * @returns {void} Nothing. + */ +writeToFile(filePath: string, text: string): void; +``` \ No newline at end of file diff --git a/src/en/extension-doc/plmainapi/index.md b/src/en/extension-doc/plmainapi/index.md new file mode 100644 index 0000000..f08d526 --- /dev/null +++ b/src/en/extension-doc/plmainapi/index.md @@ -0,0 +1,18 @@ +# PLMainAPI + +In this group of APIs, most of the services and their methods in the main process are included. + +In your extension, you can call them by: + +```typescript +import { PLMainAPI } from "paperlib-api/api"; + +const results = await PLMainAPI.serviceName.methodName(...) +``` + +## Services + +- `contextMenuService`: Context menu service. +- `fileSystemService`: File system service, get some default folder paths, and control path selection boxes, preview files, etc. +- `menuService`: Menu service. Contains most of the shortcuts. +- `windowProcessManagementService`: Window management service for managing windows (and their corresponding process). \ No newline at end of file diff --git a/src/en/extension-doc/plmainapi/menu-service.md b/src/en/extension-doc/plmainapi/menu-service.md new file mode 100644 index 0000000..1d03331 --- /dev/null +++ b/src/en/extension-doc/plmainapi/menu-service.md @@ -0,0 +1,57 @@ +# MenuService + +## Call + +```typescript +import { PLMainAPI } from "paperlib-api/api"; + +PLMainAPI.menuService.methodname(...); +``` + +## Avaliable Methods + +### `enableGlobalShortcuts` + +```typescript +/** + * Enable global shortcuts. + */ +enableGlobalShortcuts(): void; +``` + +### `disableGlobalShortcuts` + +```typescript +/** + * Disable global shortcuts. + */ +disableGlobalShortcuts(): void; +``` + +### `click` + +```typescript +/** + * Click menu item in a programmatic way. + * @param key + */ +click(key: keyof IMenuServiceState): void; +``` + +## Events + +| Event ID | Callback Value | Description | +| --- | --- | --- | +| `preference` | `{key: 'preference'}` | When Preference is clicked | +| `File-enter` | `{key: 'File-enter'}` | When `File-Open` is clicked in the menu bar | +| `File-copyBibTex` | `{key: 'File-copyBibTex'}` | When `File-Copy BibTex` is clicked in the menu bar | +| `File-copyBibTexKey` | `{key: 'File-copyBibTexKey'}` | When `File-Copy BibTex Key` is clicked in the menu bar | +| `Edit-rescrape` | `{key: 'Edit-rescrape'}` | When `Edit-Rescrape` is clicked in the menu bar | +| `Edit-edit` | `{key: 'Edit-edit'}` | When `Edit-Edit` is clicked in the menu bar | +| `Edit-flag` | `{key: 'Edit-flag'}` | When `Edit-Flag` is clicked in the menu bar | +| `View-preview` | `{key: 'View-preview'}` | When `View-Preview` is clicked in the menu bar | +| `View-next` | `{key: 'View-next'}` | When `View-Next` is clicked in the menu bar | +| `View-previous` | `{key: 'View-previous'}` | When `View-Previous` is clicked in the menu bar | + + + diff --git a/src/en/extension-doc/plmainapi/windowprocessmanagement-service.md b/src/en/extension-doc/plmainapi/windowprocessmanagement-service.md new file mode 100644 index 0000000..5b1704b --- /dev/null +++ b/src/en/extension-doc/plmainapi/windowprocessmanagement-service.md @@ -0,0 +1,243 @@ +# WindowProcessManagementService + +## Call + +```typescript +import { PLMainAPI } from "paperlib-api/api"; + +PLMainAPI.windowProcessManagementService.methodname(...); +``` + +## Avaliable Methods + +### `create` + +```typescript +/** + * Create Process with a BrowserWindow + * @param id - window id + * @param options - window options + * @param eventCallbacks - callbacks for events + * @param additionalHeaders - additional response headers for the window + */ +create( + id: string, + options: WindowOptions, + eventCallbacks?: Record void>, + additionalHeaders?: Record +): void; +``` + +### `destroy` + +```typescript +/** + * Destroy the window with the given id. + * @param windowId - The id of the window to be destroyed + */ +destroy(windowId: string): Promise; +``` + +### `fireServiceReady` + +```typescript +/** + * Fire the serviceReady event. This event is fired when the service of the window is ready to be used by other processes. + * @param windowId - The id of the window that fires the event + */ +fireServiceReady(windowId: string): Promise; +``` + +### `show` + +```typescript +/** + * Show the window with the given id. + * @param windowId - The id of the window to be shown + */ +show(windowId: string): Promise; +``` + +### `hide` + +```typescript +/** + * Hide the window with the given id. + * @param windowId - The id of the window to be hidden + */ +hide(windowId: string, restoreFocus?: boolean): Promise; +``` + +### `minimize` + +```typescript +/** + * Minimize the window with the given id. + * @param windowId - The id of the window to be minimized + */ +minimize(windowId: string): Promise; +``` + +### `maximize` + +```typescript +/** + * Maximize the window with the given id. + * @param windowId - The id of the window to be maximized + */ +maximize(windowId: string): Promise; +``` + +### `close` + +```typescript +/** + * Close the window with the given id. + * @param windowId - The id of the window to be closed + */ +close(windowId: string): Promise; +``` + +### `forceClose` + +```typescript +/** + * Force close the window with the given id. + * @param windowId - The id of the window to be force closed + */ +forceClose(windowId: string): Promise; +``` + +### `changeTheme` + +```typescript +/** + * Change the theme of the app. + * @param theme - The theme to be changed to + */ +changeTheme(theme: APPTheme): Promise; +``` + +### `isDarkMode` + +```typescript +/** + * Check if the app is in dark mode. + * @returns Whether the app is in dark mode + */ +isDarkMode(): Promise; +``` + +### `resize` + +```typescript +/** + * Resize the window with the given id. + * @param windowId - The id of the window to be resized + * @param width - The width of the window + * @param height - The height of the window + */ +resize(windowId: string, width: number, height: number): Promise; +``` + +### `getScreenSize` + +```typescript +/** + * Get the size of the screen. + * @returns The size of the screen + */ +getScreenSize(): Promise<{ + width: number; + height: number; +}>; + +``` + +### `isFocused` + +```typescript +/** + * Whether the window is focused. + * @param windowId - The id of the window to be checked + */ +isFocused(windowId: string): Promise; +``` + +### `setParentWindow` +```typescript +/** + * Set parent as current window's parent window. + * @param parentId - The id of the parent window + * @param currentId - The id of the current window + */ +setParentWindow(parentId: string | null, currentId: string): void; +``` + +### `getBounds` +```typescript +/** + * Return the window's current bounds. + * @param windowId - The id of the window to be checked + */ +getBounds(windowId: string): Electron.Rectangle | undefined; +``` + +### `setBounds` +```typescript +/** + * Set the window's current bounds. + * @param windowId - The id of the window to be set + * @param bounds - The bounds of the window to be set + */ +setBounds(windowId: string, bounds: Partial): void; +``` + +### `hasParentWindow` +```typescript +/** + * Return whether the window has a parent. + * @param windowId - The id of the window to be checked + */ +hasParentWindow(windowId: string): boolean; +``` + +### `setAlwaysOnTop` +```typescript +/** + * Set whether the window should show always on top of other windows. + * @param windowId - The id of the window to be set + * @param flag - Whether the window should show always on top of other windows + */ +setAlwaysOnTop(windowId: string, flag: boolean): void; +``` + +### `center` +```typescript +/** + * Move the window to the center of the screen. + * @param windowId - The id of the window to be set + */ +center(windowId: string): void; +``` + +## Events + +| Event ID | Callback Value | Description | +| -------------- | ---------------------------------------------- | --------------------------------------------------------------------- | +| `serviceReady` | `{key: 'serviceReady', value: windowId}` | When the service of the window is ready to be used by other processes | +| `requestPort` | `{key: 'requestPort', value: senderProcessId}` | When a process is requesting MessagePort | +| `destroyed` | `{key: 'destroyed', value: windowId}` | When the window is destroyed | +| any window ID | `{key: windowId, value: event}` | event: `ready-to-show`, `blur`, `focus`, `close`, `show`, `created` | + +The main renderer window's ID is `rendererProcess`. If you want to listen to the `blur` event of the main renderer window, you can do this: + +```typescript +import { PLMainAPI } from "paperlib-api"; + +PLMainAPI.windowProcessManagementService.on("rendererProcess", (event) => { + if (event.value === "blur") { + // do something + } +}); +``` diff --git a/src/en/extension-doc/preference.md b/src/en/extension-doc/preference.md new file mode 100644 index 0000000..afabab0 --- /dev/null +++ b/src/en/extension-doc/preference.md @@ -0,0 +1,132 @@ +# User Preference + +## Default Preference + +When creating an extension class, we can provide default extension preferences, which will be used when the user installs the extension for the first time. + +```typescript +class PaperlibHelloworldExtension extends PLExtension { + constructor() { + super({ + id: "...", + defaultPreference: { + [id: string]: { + type: "string" | "boolean" | "options" | "pathpicker", + name: string, + description: string, + value: string | boolean, + order?: number, + options?: { [key: string]: string }, // only for options type + }, + ... + }, + }); + } +} +``` + +Each preference is a key-value pair, the key is the ID of the preference, and the value is an object that contains various information about the preference: + +- The `id` field is required, it specifies the ID of the preference. This is used to access the value of the preference later. +- The `type` field is required, it specifies the type of the preference. +- The `name` field is required, it specifies the name of the preference displayed in the UI. +- The `description` field is required, it specifies the description of the preference displayed in the UI. +- The `value` field is required, it specifies the default value of the preference. +- The `order` field is optional, it specifies the display order of the preference. +- The `options` field is optional, it is only needed when the preference type is `options`, it specifies the options of the option type preference. + +# Preference Type + +As shown above, we provide four types of preferences: + +- `string`: String type preference, the user can enter any string. +- `boolean`: Boolean type preference, the user can choose `true` or `false`. +- `options`: Option type preference, the user can select one from the predefined options. The `options` field must be provided, and each key-value pair in it is an option. The `key` is the identical value of the option, and the `value` is the display name of the option. +- `pathpicker`: Path picker type preference, the user can select a file or folder. + +Different types of preferences will display different components in Paperlib. + +## Access Preference + +To access the value of a preference in an extension, we can use `PLExtAPI.extensionPreferenceService.get`. For example: + +```typescript +PLExtAPI.extensionPreferenceService.get(this.id, "lang") +``` + +By doing so, we can get the value of the `lang` preference. Note that we need to provide the ID of the extension (`this.id`) so that the `ExtensionPreferenceService` knows which extension's preference we want to access. + +# Set Preference + +We can set the value of a preference by using `PLExtAPI.extensionPreferenceService.set`. For example: + +```typescript +PLExtAPI.extensionPreferenceService.set(this.id, {"lang": "en"}) +``` + +Here we provide a `patch object`` to modify the preferences. The `patch object`` is a key-value pair, where each key-value pair is a modification of a preference. The key is the ID of the preference, and the value is the new value of the preference. You can pass in modifications for multiple preferences at the same time. + +## Listen Preference Change + +We can listen to the change of a preference by using `PLExtAPI.extensionPreferenceService.onPreferenceChange`. For example: + +```typescript +PLExtAPI.extensionPreferenceService.onChanged( + `${this.id}:lang`, + (changes: {key: string, value: string}) => { + console.log(changes) // {key: "lang", value: "en"} + } +) +``` + +> ⚠️ Please note that listening to a setting of an extension must be in the form of `extID:preferenceKey` to form the ID of the event to be listened to. Because `extensionPreferenceService` need to distinguish the preferences of different extensions. + +By doing so, we can listen to the modification of the `lang` preference. When the user modifies the value of the `lang` in the preferences interface, we will run the callback function passed in. The function receives an Object as an argument , where the `key` field is the ID of the preference, and the `value` field is the new value of the preference. + +> ⚠️ Please note that all listener registrations must be cancelled in the `dispose` function of the extension to avoid memory leaks. + +## Cancel Preference Change Listener + +When the extension is uninstalling or reloading, Paperlib will call the `dispose` function of the extension. In this function, we need to cancel all listener registrations to avoid memory leaks. + +When you are listening for changes to a preference, `PLExtAPI.extensionPreferenceService.onChanged / on` will return a function. Running this function will cancel the listener. + +For example: + +```typescript +// Listen +const disposeCallback = PLExtAPI.extensionPreferenceService.onChanged( + `${this.id}:lang`, + (changes: {key: string, value: string}) => { + console.log(changes) // {key: "lang", value: "en"} + } +) + +// Cancel +disposeCallback() +``` + +## Storing and Accessing Passwords + +Paperlib also provides storage and retrieval of password items. Regular preferences are stored in a `.json` files near the extension directory, while password items are stored in different keychains depending on the platform. For example, the Keychain on macOS, the Credential Manager on Windows, etc. + + +```typescript +await PLExtAPI.extensionPreferenceService.setPassword( + this.id, "password-key", "your-password" +) + +const password = await PLExtAPI.extensionPreferenceService.getPassword( + this.id, "password-key" +) + +``` + +## Preference File + +In Paperlib, the user's preferences are stored in a `.json` file. + +- macOS: `~/Library/Application Support/paperlib/extensions/.json` +- Windows: `%APPDATA%/paperlib/extensions/.json` +- Linux: `~/.config/paperlib/extensions/.json` + diff --git a/src/en/extension-doc/process-hook.md b/src/en/extension-doc/process-hook.md new file mode 100644 index 0000000..b71c88d --- /dev/null +++ b/src/en/extension-doc/process-hook.md @@ -0,0 +1,247 @@ +# Operation Flows and Hooks + +This article introduces the important flows in Paperlib, as well as the hooks available in these operation flows. + +## Introduction to Hooks + +In Paperlib, we have placed hooks with different names in different places. A hook extension can register to the corresponding hook point to intervene in the operation flow of Paperlib. There are two types of hook points. + +### Modify Hook Points + +- **Purpose**: This type of hook point is used to modify the arguments passed by the hook point, or the variables within the argument objects, but cannot change the type, and finally returns the modified arguments. + +- **Type of Callback Return Value**: Modify Hook requires the return value of the callback function to be an array, each element of which corresponds to the input argument array. For example, if the arguments passed by the hook point are `(arg1: string, arg2: {value: number})`, you can modify `arg1` to another string and `arg2.value` to another number in the callback function of the hook, but you cannot change the type of `arg1` to a number, or change the type of `arg2` to another type. The return value of the callback function must be an array composed of modified arguments: `[arg1, arg2]`. **Note that even if only one argument is passed in, an array with one element needs to be returned. Because the input arguments are always treated as an arguments array** + +### Transform Hook Points + +- **Purpose**: This type of hook point can modify the data flow in the operation process of Paperlib. It is used to transform the input arguments into other forms of data and then return. +- **Callback Return Value Type**: It can be other types, but usually different hook points have expected return value types. For example, the `scrapeEntry` hook point expects the return value type to be an array of `PaperEntity`. + +For information on how to register hooks and how to write hook extensions, please refer to [Hook extensions](./ext-types/hook-ext). + +## Paper Import Process + +Whether it is imported by dragging files or imported from browser extensions, after forming the corresponding `source payload`, it will enter the paper import process. The diagram of the paper import process is as follows: + + + +In this process, the main hook points are in the `scrapeEntry()` and `scrapeMetadata()` methods of `ScrapeService`. + +--- + +### `scrapeEntry()` + +The main task of `scrapeEntry()` is to convert these data from different types of sources into the internal data structure `PaperEntity` of Paperlib, and fill in the important fields of `PaperEntity` as much as possible (such as: `title`, `doi`, `arxivID`, etc.) for the subsequent `scrapeMetadata()` method to search and complete its paper metadata. + +After receiving the import of the paper source, we first call `scrapeEntry()`. Its main argument is an array of `SourcePayload`, that is, the data of the paper source. `source payload` contains the `type` field indicating the type of source, and the data of the source: + +```typescript +interface SourcePayload { + type: "file" | "webcontent"; + // For file type, value is the path of the file + // For webcontent type, value is WebContentSourcePayload + value: string | WebContentSourcePayload; +} + +interface WebContentSourcePayload { + url: string; // Source page's url + document: string; // Source page's html + cookies?: string; // Some pages may contain cookies +} +``` + +The return type of `scrapeEntry()` is an array of `PaperEntity`, because even if the `SourcePayload` array only contains one `source payload`, it may contain multiple papers. For example, when importing by dragging files, the dragged file might be a BibTex file, which contains information about multiple papers. + +--- + +### `scrapeMetadata()` + +The return value of `scrapeEntry()` will be passed into the `scrapeMetadata()` method. The main task of `scrapeMetadata()` is to search for the paper's metadata from various databases on the internet and complete all fields in `PaperEntity`. The return type of `scrapeMetadata()` is also an array of `PaperEntity`. + +--- + +As shown in the diagram, there are six hook points in this operation flow, five of the `Modify` type and one of the `Transform` type. + + +### `beforeScrapeEntry` + +| Parameter | Value | +| --- | --- | +| Type | `Modify` | +| Location | At the very beginning of the `scrapeEntry()` method, before the SourcePayload being converted to `PaperEntity` | +| Callback arguments | `SourcePayload[]` | +| Callback Return Value | `ArgumentArray` | + + +### `scrapeEntry` + +| Parameter | Value | +| --- | --- | +| Type | `Transform` | +| Location | The main hook point of `scrapeEntry()`, accepts `SourcePayload` 数组 and outputs `PaperEntity` 数组 | +| Callback arguments | `SourcePayload[]` | +| Callback Return Value | `PaperEntity[]` | + +### `afterScrapeEntry` + +| Parameter | Value | +| --- | --- | +| Type | `Modify` | +| Location | At the end of the `scrapeEntry()` method, after the SourcePayload being converted to `PaperEntity` | +| Callback arguments | `PaperEntity[]` | +| Callback Return Value | `ArgumentArray` | + +### `beforeScrapeMetadata` + +| Parameter | Value | +| --- | --- | +| Type | `Modify` | +| Location | At the very beginning of the `scrapeMetadata()` method, before searching for metadata | +| Callback arguments | `paperEntities: PaperEntity[]`, `scrapers: string[]`, `force: boolean` | +| Callback Return Value | ArgumentArray<`paperEntities: PaperEntity[]`, `scrapers: string[]`, `force: boolean`> | + +Here, `scrapers` is an array of strings. If it is not empty, it means that the user has chosen to search with specific scrapers. `force` indicates whether to force the search. If `true`, it will ignore the existing metadata in `PaperEntity` and force the search. + +### `scrapeMetadata` + +| Parameter | Value | +| --- | --- | +| Type | `Modify` | +| Location | The main hook point of `scrapeMetadata()`, accepts an array of `PaperEntity`, can modify each property and return | +| Callback arguments | `paperEntities: PaperEntity[]`, `scrapers: string[]`, `force: boolean` | +| Callback Return Value | ArgumentArray<`paperEntities: PaperEntity[]`, `scrapers: string[]`, `force: boolean`> | + +### `afterScrapeMetadata` + +| Parameter | Value | +| --- | --- | +| Type | `Modify` | +| Location | At the end of the `scrapeMetadata()` method, after searching for metadata | +| Callback arguments | `paperEntities: PaperEntity[]`, `scrapers: string[]`, `force: boolean` | +| Callback Return Value | ArgumentArray<`paperEntities: PaperEntity[]`, `scrapers: string[]`, `force: boolean`> | + +## Paper PDF Locating + +When a paper does not have a corresponding PDF file, Paperlib will display a button in the detail panel to locate for available PDFs on the internet and download them. After clicking this button, it will enter the paper PDF locating process. The main function of the paper PDF locating process is the `locateFileOnWeb()` method of `FileService`. There is one available hook in this process. + + +### `locateFile` + +| Parameter | Value | +| --- | --- | +| Type | `Modify` | +| Location | Inside the `locateFileOnWeb()` method | +| Callback arguments | `PaperEntity[]` | +| Callback Return Value | ArgumentArray<`PaperEntity[]`> | + +## `Reference Exporting Process` + +In general, when exporting references, we need to get the array of `PaperEntity` to be exported, and then convert it into a `citation.js`'s `Cite` object. Finally, we convert the `Cite` object into a string of the corresponding format, such as a BibTex string. For details, please refer to the various functions of `ReferenceService` in the GitHub code. + +### `beforeExportBibItem` + +| Parameter | Value | +| --- | --- | +| Type | `Modify` | +| Location | At the very beginning of the `exportBibItem()` method | +| Callback arguments | `PaperEntity[]` | +| Callback Return Value | `ArgumentArray` | + +### `citeObjCreatedInExportBibItem` + +| Parameter | Value | +| --- | --- | +| Type | `Modify` | +| Location | Inside the `exportBibItem()` method, after the `Cite` object has been created | +| Callback arguments | `cite: Cite, paperEntities: PaperEntity[]` | +| Callback Return Value | ArgumentArray<`cite: Cite, paperEntities: PaperEntity[]`> | + +### `afterExportBibItem` + +| Parameter | Value | +| --- | --- | +| Type | `Modify` | +| Location | At the end of the `exportBibItem()` method | +| Callback arguments | `string` | +| Callback Return Value | ArgumentArray<`string`> | + +### `beforeExportBibTexKey` + +| Parameter | Value | +| --- | --- | +| Type | `Modify` | +| Location | At the beginning of the `exportBibTexKey()` method | +| Callback arguments | `PaperEntity[]` | +| Callback Return Value | ArgumentArray<`PaperEntity[]`> | + +### `citeObjCreatedInExportBibTexKey` + +| Parameter | Value | +| --- | --- | +| Type | `Modify` | +| Location | Inside the `exportBibTexKey()` method, after the `Cite` object has been created | +| Callback arguments | `cite: Cite, paperEntities: PaperEntity[]` | +| Callback Return Value | ArgumentArray<`cite: Cite, paperEntities: PaperEntity[]`> | + +### `afterExportBibTexKey` + +| Parameter | Value | +| --- | --- | +| Type | `Modify` | +| Location | At the end of the `exportBibTexKey()` method | +| Callback arguments | `string` | +| Callback Return Value | ArgumentArray<`string`> | + +### `beforeExportBibTexBody` + +| Parameter | Value | +| --- | --- | +| Type | `Modify` | +| Location | At the beginning of the `exportBibTexBody()` method | +| Callback arguments | `PaperEntity[]` | +| Callback Return Value | ArgumentArray<`PaperEntity[]`> | + +### `citeObjCreatedInExportBibTexBody` + +| Parameter | Value | +| --- | --- | +| Type | `Modify` | +| Location | Inside the `exportBibTexBody()` method, after the `Cite` object has been created | +| Callback arguments | `cite: Cite, paperEntities: PaperEntity[]` | +| Callback Return Value | ArgumentArray<`cite: Cite, paperEntities: PaperEntity[]`> | + +### `afterExportBibTexBody` + +| Parameter | Value | +| --- | --- | +| Type | `Modify` | +| Location | At the end of the `exportBibTexBody()` method | +| Callback arguments | `string` | +| Callback Return Value | ArgumentArray<`string`> | + +### `beforeExportPlainText` + +| Parameter | Value | +| --- | --- | +| Type | `Modify` | +| Location | At the beginning of the `exportPlainText()` method | +| Callback arguments | `PaperEntity[]` | +| Callback Return Value | ArgumentArray<`PaperEntity[]`> | + +### `citeObjCreatedInExportPlainText` + +| Parameter | Value | +| --- | --- | +| Type | `Modify` | +| Location | Inside the `exportPlainText()` method, after the `Cite` object has been created | +| Callback arguments | `cite: Cite, paperEntities: PaperEntity[]` | +| Callback Return Value | ArgumentArray<`cite: Cite, paperEntities: PaperEntity[]`> | + +### `afterExportPlainText` + +| Parameter | Value | +| --- | --- | +| Type | `Modify` | +| Location | At the end of the `exportPlainText()` method | +| Callback arguments | `string` | +| Callback Return Value | ArgumentArray<`string`> | \ No newline at end of file diff --git a/src/en/extension-doc/service-event.md b/src/en/extension-doc/service-event.md new file mode 100644 index 0000000..cbae71f --- /dev/null +++ b/src/en/extension-doc/service-event.md @@ -0,0 +1,71 @@ +# Service Events + +Almost all services in the API are `Eventable`. This means that each service will emit some events at different times. Other code locations and processes can listen for corresponding event to execute their own code. For example, you can listen for changes in the user's selected paper, and then run your own methods, etc. + +## Listening to Events + +The method to listen to events is `on`. It accepts two arguments, the first is the event name, and the second is the callback function. The arguments of the callback function are the arguments passed when the event is emitted. + + +```typescript +import { PLAPI } from 'paperlib-api/api'; + + +PLAPI.serviceName.on('event-id', (newValue: {key: string, value: any}) => { + ... +}); +``` + +The arguments received by the callback function are usually an object, which includes `key` and `value` fields. `key` is the event ID, and `value` is the corresponding new value. + +You can register the same callback function for multiple events: + +```typescript +PLAPI.serviceName.on( + ['event-id-1', 'event-id-2'], + (newValue: {key: string, value: any}) => { + ... + } +); +``` + +Here, the `key` field will help you determine which event was emitted. + +**In the callback of the event listener, please avoid `floating promise`. That is, if your callback function contains any `AsyncFunction`, please be sure to `await` or `.catch` the error exception. Because the error in the `floating promise` cannot be caught in Paperlib, it will cause the extension to crash:** + +```typescript +// Never do this: +PLAPI.serviceName.on('event-id', (newValue: {key: string, value: any}) => { + asyncFunction(); +}); + +async function asyncFunction() { + throw new Error('error'); // This error will not be caught by Paperlib, and will cause the extension to crash. +} +``` + + +## Cancel Listening + +Please note that you need to save the function returned by the `on` method so that you can cancel the listening when you don't need it. + +```typescript +const cancel = PLAPI.serviceName.on('event-id', (newValue: {key: string, value: any}) => { + ... +}); + +// Cancel listening +cancel(); +``` + +## Aliases + +For some services, we provide some aliases for `on()` function to make it easier to write code. + +For example, in `preferenceService`, we provide a `onChanged()` method, which has the same effect as `on()`. It is just an alias. + +## Service Event List + +Different services have different events. For the specific event list, please refer to the corresponding service documentation. + +If the existing events cannot meet your needs, you can also submit an issue to us on Github, and we will consider adding new events. \ No newline at end of file diff --git a/src/en/extension-doc/ui-slot.md b/src/en/extension-doc/ui-slot.md new file mode 100644 index 0000000..63837b5 --- /dev/null +++ b/src/en/extension-doc/ui-slot.md @@ -0,0 +1,49 @@ +# UI Slots + +In this article, we will introduce the UI slots that can be used and modified in Paperlib. + +## Slot in the Paper Details Panel + +In the Paper Details Panel, we have three slots: + +- `paperDetailsPanelSlot1`: Under the publication date of the paper. +- `paperDetailsPanelSlot2`: Under the rating of the paper. +- `paperDetailsPanelSlot3`: Under the supplementary materials of the paper. + +## Slot in the Notification View + +In the Notification View, we have one slot: + +- `overlayNotifications`: The notification that appears on the top of the screen. + +To show the content in the slot, you need to update the overlay `overlayNoticationShown = true`: + +```typescript +PLAPI.uiStateService.setState({"overlayNoticationShown": true}); +``` + +### Slot Content + +```typescript +{ + title: string, + content: string +} +``` + +### Update Slot + +```typescript +PLAPI.uiSlotService.updateSlot( + "paperDetailsPanelSlot1", + { + [id: string]: { + title: string, + content: string + } + } +); + +``` + +`id` is the unique identifier of the content. If the `id` is already in the slot, the content will be updated. Otherwise, a new content will be added. \ No newline at end of file diff --git a/src/en/index.md b/src/en/index.md new file mode 100644 index 0000000..e453f4c --- /dev/null +++ b/src/en/index.md @@ -0,0 +1,123 @@ +--- +layout: home + +hero: + name: Paperlib + text: to organise academic papers decently. + tagline: An open-source and simple academic paper management tool. + image: + src: /assets/images/ui.png + alt: PaperlibUI + actions: + - theme: brand + text: Get Started + link: ./doc/getting-started + - theme: alt + text: Download + link: ./download + - theme: alt + text: Donate + link: ./donate + + +features: +- title: Scrape Metadata + details: Scrape paper’s metadata in many metadata databases. Tailored for many disciplines (e.g., computer science, physics etc.). Especially, the precise metadata scraping for conference papers +- title: Organise your Paper + details: Flag, tag, and create folders to let your library clean and tidy. Also support markdown and plain text notes +- title: Export References + details: Export references by using the Quick Copy-paste plugin when you write your draft paper. Also supports Word plugin +- title: Extensible + details: Develop and publish your own extensions. +- title: RSS Feed + details: Subscribe to RSS feed to get new papers +- title: Modern UI, Cross Platform and Cloud Sync + details: Supports macOS, Linux and windows. Modern and clean UI (supports darkmode). Access your library from everywhere with a sync database +--- + + + +
    +
    Comments
    +
    + +
    + +
    +
    Users
    +
    +
    +
  • + {{ user }} +
  • +
    +
    + +
    +
    Sponsors
    +
    +
    + + +
    +
    diff --git a/src/en/release-note.md b/src/en/release-note.md new file mode 100644 index 0000000..2561c82 --- /dev/null +++ b/src/en/release-note.md @@ -0,0 +1,55 @@ +--- +title: "Release Note" +--- + + + + + + diff --git a/src/public/_vercel_script.js b/src/public/_vercel_script.js new file mode 100644 index 0000000..a2664af --- /dev/null +++ b/src/public/_vercel_script.js @@ -0,0 +1,2 @@ +window.va = window.va || function () { (window.vaq = window.vaq || []).push(arguments); }; +window.si = window.si || function () { (window.siq = window.siq || []).push(arguments); }; \ No newline at end of file diff --git a/src/public/apple-touch-icon.png b/src/public/apple-touch-icon.png new file mode 100644 index 0000000..4e191fe Binary files /dev/null and b/src/public/apple-touch-icon.png differ diff --git a/src/public/assets/icon.png b/src/public/assets/icon.png new file mode 100644 index 0000000..778df72 Binary files /dev/null and b/src/public/assets/icon.png differ diff --git a/src/public/assets/images/extension-doc/process-hook/import-process.svg b/src/public/assets/images/extension-doc/process-hook/import-process.svg new file mode 100644 index 0000000..1ce5330 --- /dev/null +++ b/src/public/assets/images/extension-doc/process-hook/import-process.svg @@ -0,0 +1,3 @@ + + +
    Array of souce payload
    Array of souce payload
    {
      type: "file",
      value: "file:///.../..."
    }
    {...
    Drag-drop PDF files
    Drag-drop PDF files
    beforeScrapeEntry
    beforeScrapeEntry
    scrapeEntry
    scrapeEntry
    Array of PaperEntity draft
    Array of PaperEntity draft
    {
      title: "...",
      authors: "..."
      ...
    }[]
    {...
    afterScrapeEntry
    afterScrapeEntry
    beforeScrapeMetadata
    beforeScrapeMetadata
    scrapeMetadata
    scrapeMetadata
    Completed PaperEntity drafts
    Completed PaperEntity drafts
    {
      title: "...",
      authors: "..."
      ...
    }
    {...
    afterScrapeMetadata
    afterScrapeMetadata
    Insert into Database
    Insert into Database
    scrapeService.scrapeEntry()
    scrapeService.scrapeEntry()
    scrapeService.scrapeMetadata()
    scrapeService.scrapeMetadata()
    hook_cb_1()
    hook_cb_1()
    hook_cb_N()
    hook_cb_N()
    input
    input
    ...
    ...
    transformed
    output
    transformed...
    transformed
    output
    transformed...
    flat()
    flat()
    array of
    transformed outputs
    array of...
    hook_cb_1()
    hook_cb_1()
    hook_cb_N()
    hook_cb_N()
    input
    input
    ...
    ...
    modified
    output
    modified...
    Transform Hook
    Transform Hook
    Modify Hook
    Modify Hook
    Text is not SVG - cannot display
    \ No newline at end of file diff --git a/src/public/assets/images/getting-started/add.png b/src/public/assets/images/getting-started/add.png new file mode 100644 index 0000000..ec43d79 Binary files /dev/null and b/src/public/assets/images/getting-started/add.png differ diff --git a/src/public/assets/images/getting-started/addsup.png b/src/public/assets/images/getting-started/addsup.png new file mode 100644 index 0000000..5478d3a Binary files /dev/null and b/src/public/assets/images/getting-started/addsup.png differ diff --git a/src/public/assets/images/getting-started/browser-ext.png b/src/public/assets/images/getting-started/browser-ext.png new file mode 100644 index 0000000..32d58f7 Binary files /dev/null and b/src/public/assets/images/getting-started/browser-ext.png differ diff --git a/src/public/assets/images/getting-started/edit.png b/src/public/assets/images/getting-started/edit.png new file mode 100644 index 0000000..73b9b49 Binary files /dev/null and b/src/public/assets/images/getting-started/edit.png differ diff --git a/src/public/assets/images/getting-started/feedadd.png b/src/public/assets/images/getting-started/feedadd.png new file mode 100644 index 0000000..536b80a Binary files /dev/null and b/src/public/assets/images/getting-started/feedadd.png differ diff --git a/src/public/assets/images/getting-started/library-folder.png b/src/public/assets/images/getting-started/library-folder.png new file mode 100644 index 0000000..199ca0a Binary files /dev/null and b/src/public/assets/images/getting-started/library-folder.png differ diff --git a/src/public/assets/images/getting-started/locate.png b/src/public/assets/images/getting-started/locate.png new file mode 100644 index 0000000..fe497b8 Binary files /dev/null and b/src/public/assets/images/getting-started/locate.png differ diff --git a/src/public/assets/images/getting-started/plugin.png b/src/public/assets/images/getting-started/plugin.png new file mode 100644 index 0000000..759f94e Binary files /dev/null and b/src/public/assets/images/getting-started/plugin.png differ diff --git a/src/public/assets/images/getting-started/preview.png b/src/public/assets/images/getting-started/preview.png new file mode 100644 index 0000000..bff89ec Binary files /dev/null and b/src/public/assets/images/getting-started/preview.png differ diff --git a/src/public/assets/images/getting-started/scraper.png b/src/public/assets/images/getting-started/scraper.png new file mode 100644 index 0000000..46e6c5b Binary files /dev/null and b/src/public/assets/images/getting-started/scraper.png differ diff --git a/src/public/assets/images/getting-started/search.png b/src/public/assets/images/getting-started/search.png new file mode 100644 index 0000000..d770bb9 Binary files /dev/null and b/src/public/assets/images/getting-started/search.png differ diff --git a/src/public/assets/images/guide/cloud-sync/n1.png b/src/public/assets/images/guide/cloud-sync/n1.png new file mode 100644 index 0000000..4963102 Binary files /dev/null and b/src/public/assets/images/guide/cloud-sync/n1.png differ diff --git a/src/public/assets/images/guide/cloud-sync/n10.png b/src/public/assets/images/guide/cloud-sync/n10.png new file mode 100644 index 0000000..ebd285b Binary files /dev/null and b/src/public/assets/images/guide/cloud-sync/n10.png differ diff --git a/src/public/assets/images/guide/cloud-sync/n11.png b/src/public/assets/images/guide/cloud-sync/n11.png new file mode 100644 index 0000000..86d9a5b Binary files /dev/null and b/src/public/assets/images/guide/cloud-sync/n11.png differ diff --git a/src/public/assets/images/guide/cloud-sync/n12.png b/src/public/assets/images/guide/cloud-sync/n12.png new file mode 100644 index 0000000..95d3709 Binary files /dev/null and b/src/public/assets/images/guide/cloud-sync/n12.png differ diff --git a/src/public/assets/images/guide/cloud-sync/n13.png b/src/public/assets/images/guide/cloud-sync/n13.png new file mode 100644 index 0000000..f313b5f Binary files /dev/null and b/src/public/assets/images/guide/cloud-sync/n13.png differ diff --git a/src/public/assets/images/guide/cloud-sync/n19.png b/src/public/assets/images/guide/cloud-sync/n19.png new file mode 100644 index 0000000..e5cdbf7 Binary files /dev/null and b/src/public/assets/images/guide/cloud-sync/n19.png differ diff --git a/src/public/assets/images/guide/cloud-sync/n2.png b/src/public/assets/images/guide/cloud-sync/n2.png new file mode 100644 index 0000000..ebcfa51 Binary files /dev/null and b/src/public/assets/images/guide/cloud-sync/n2.png differ diff --git a/src/public/assets/images/guide/cloud-sync/n3.png b/src/public/assets/images/guide/cloud-sync/n3.png new file mode 100644 index 0000000..2a86b6e Binary files /dev/null and b/src/public/assets/images/guide/cloud-sync/n3.png differ diff --git a/src/public/assets/images/guide/cloud-sync/n4.png b/src/public/assets/images/guide/cloud-sync/n4.png new file mode 100644 index 0000000..3e4f0d6 Binary files /dev/null and b/src/public/assets/images/guide/cloud-sync/n4.png differ diff --git a/src/public/assets/images/guide/cloud-sync/n5.png b/src/public/assets/images/guide/cloud-sync/n5.png new file mode 100644 index 0000000..b348e40 Binary files /dev/null and b/src/public/assets/images/guide/cloud-sync/n5.png differ diff --git a/src/public/assets/images/guide/cloud-sync/n6.png b/src/public/assets/images/guide/cloud-sync/n6.png new file mode 100644 index 0000000..fe7f4bc Binary files /dev/null and b/src/public/assets/images/guide/cloud-sync/n6.png differ diff --git a/src/public/assets/images/guide/cloud-sync/n7.png b/src/public/assets/images/guide/cloud-sync/n7.png new file mode 100644 index 0000000..f0bd168 Binary files /dev/null and b/src/public/assets/images/guide/cloud-sync/n7.png differ diff --git a/src/public/assets/images/guide/cloud-sync/n8.png b/src/public/assets/images/guide/cloud-sync/n8.png new file mode 100644 index 0000000..260de25 Binary files /dev/null and b/src/public/assets/images/guide/cloud-sync/n8.png differ diff --git a/src/public/assets/images/guide/cloud-sync/n9.png b/src/public/assets/images/guide/cloud-sync/n9.png new file mode 100644 index 0000000..0b88fa6 Binary files /dev/null and b/src/public/assets/images/guide/cloud-sync/n9.png differ diff --git a/src/public/assets/images/guide/cloud-sync/user1.png b/src/public/assets/images/guide/cloud-sync/user1.png new file mode 100644 index 0000000..d7c90ed Binary files /dev/null and b/src/public/assets/images/guide/cloud-sync/user1.png differ diff --git a/src/public/assets/images/guide/cloud-sync/user2.png b/src/public/assets/images/guide/cloud-sync/user2.png new file mode 100644 index 0000000..c1da8e5 Binary files /dev/null and b/src/public/assets/images/guide/cloud-sync/user2.png differ diff --git a/src/public/assets/images/guide/cloud-sync/user3.png b/src/public/assets/images/guide/cloud-sync/user3.png new file mode 100644 index 0000000..6104b0a Binary files /dev/null and b/src/public/assets/images/guide/cloud-sync/user3.png differ diff --git a/src/public/assets/images/guide/cloud-sync/user4.png b/src/public/assets/images/guide/cloud-sync/user4.png new file mode 100644 index 0000000..399cb27 Binary files /dev/null and b/src/public/assets/images/guide/cloud-sync/user4.png differ diff --git a/src/public/assets/images/guide/extensions/word/add-cite.png b/src/public/assets/images/guide/extensions/word/add-cite.png new file mode 100644 index 0000000..659c12b Binary files /dev/null and b/src/public/assets/images/guide/extensions/word/add-cite.png differ diff --git a/src/public/assets/images/guide/extensions/word/macos-open.png b/src/public/assets/images/guide/extensions/word/macos-open.png new file mode 100644 index 0000000..8880a0e Binary files /dev/null and b/src/public/assets/images/guide/extensions/word/macos-open.png differ diff --git a/src/public/assets/images/guide/extensions/word/others.png b/src/public/assets/images/guide/extensions/word/others.png new file mode 100644 index 0000000..077a11a Binary files /dev/null and b/src/public/assets/images/guide/extensions/word/others.png differ diff --git a/src/public/assets/images/guide/extensions/word/search.png b/src/public/assets/images/guide/extensions/word/search.png new file mode 100644 index 0000000..7ab7827 Binary files /dev/null and b/src/public/assets/images/guide/extensions/word/search.png differ diff --git a/src/public/assets/images/guide/extensions/word/update-ref.png b/src/public/assets/images/guide/extensions/word/update-ref.png new file mode 100644 index 0000000..250d759 Binary files /dev/null and b/src/public/assets/images/guide/extensions/word/update-ref.png differ diff --git a/src/public/assets/images/guide/extensions/word/web-install.png b/src/public/assets/images/guide/extensions/word/web-install.png new file mode 100644 index 0000000..8eb5891 Binary files /dev/null and b/src/public/assets/images/guide/extensions/word/web-install.png differ diff --git a/src/public/assets/images/guide/extensions/word/win-open-1.png b/src/public/assets/images/guide/extensions/word/win-open-1.png new file mode 100644 index 0000000..606b3cd Binary files /dev/null and b/src/public/assets/images/guide/extensions/word/win-open-1.png differ diff --git a/src/public/assets/images/guide/extensions/word/win-open-2.png b/src/public/assets/images/guide/extensions/word/win-open-2.png new file mode 100644 index 0000000..30c91c1 Binary files /dev/null and b/src/public/assets/images/guide/extensions/word/win-open-2.png differ diff --git a/src/public/assets/images/guide/metadata-scrapers/1.png b/src/public/assets/images/guide/metadata-scrapers/1.png new file mode 100644 index 0000000..8de4105 Binary files /dev/null and b/src/public/assets/images/guide/metadata-scrapers/1.png differ diff --git a/src/public/assets/images/guide/smart-filter/author.png b/src/public/assets/images/guide/smart-filter/author.png new file mode 100644 index 0000000..435d1d7 Binary files /dev/null and b/src/public/assets/images/guide/smart-filter/author.png differ diff --git a/src/public/assets/images/guide/smart-filter/pub.png b/src/public/assets/images/guide/smart-filter/pub.png new file mode 100644 index 0000000..7ce0244 Binary files /dev/null and b/src/public/assets/images/guide/smart-filter/pub.png differ diff --git a/src/public/assets/images/guide/smart-filter/recent.png b/src/public/assets/images/guide/smart-filter/recent.png new file mode 100644 index 0000000..e12656a Binary files /dev/null and b/src/public/assets/images/guide/smart-filter/recent.png differ diff --git a/src/public/assets/images/guide/smart-filter/tag.png b/src/public/assets/images/guide/smart-filter/tag.png new file mode 100644 index 0000000..20de521 Binary files /dev/null and b/src/public/assets/images/guide/smart-filter/tag.png differ diff --git a/src/public/assets/images/guide/smart-filter/title.png b/src/public/assets/images/guide/smart-filter/title.png new file mode 100644 index 0000000..89d8544 Binary files /dev/null and b/src/public/assets/images/guide/smart-filter/title.png differ diff --git a/src/public/assets/images/sponsors/MacStadium.png b/src/public/assets/images/sponsors/MacStadium.png new file mode 100644 index 0000000..3655b2f Binary files /dev/null and b/src/public/assets/images/sponsors/MacStadium.png differ diff --git a/src/public/assets/images/sponsors/digitalocean.svg b/src/public/assets/images/sponsors/digitalocean.svg new file mode 100644 index 0000000..f6776bd --- /dev/null +++ b/src/public/assets/images/sponsors/digitalocean.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/public/assets/images/ui.png b/src/public/assets/images/ui.png new file mode 100644 index 0000000..4175707 Binary files /dev/null and b/src/public/assets/images/ui.png differ diff --git a/src/public/assets/images/wechat_pay.png b/src/public/assets/images/wechat_pay.png new file mode 100644 index 0000000..fc1c4e0 Binary files /dev/null and b/src/public/assets/images/wechat_pay.png differ diff --git a/src/public/assets/logo-dark.svg b/src/public/assets/logo-dark.svg new file mode 100644 index 0000000..6902f3c --- /dev/null +++ b/src/public/assets/logo-dark.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/public/assets/logo-light.svg b/src/public/assets/logo-light.svg new file mode 100644 index 0000000..a053fad --- /dev/null +++ b/src/public/assets/logo-light.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/public/favicon.ico b/src/public/favicon.ico new file mode 100644 index 0000000..a21dd2a Binary files /dev/null and b/src/public/favicon.ico differ diff --git a/src/public/favicon.png b/src/public/favicon.png new file mode 100644 index 0000000..778df72 Binary files /dev/null and b/src/public/favicon.png differ diff --git a/src/public/robots.txt b/src/public/robots.txt new file mode 100644 index 0000000..4f9540b --- /dev/null +++ b/src/public/robots.txt @@ -0,0 +1 @@ +User-agent: * \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..0d3e6a5 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,7 @@ +module.exports = { + darkMode: "class", + content: [ + "./src/**/*.{html,js,vue,ts,md}", + "./src/.vitepress/**/*.{html,js,vue,ts,md}", + ], +}; diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..0591b63 --- /dev/null +++ b/vercel.json @@ -0,0 +1,19 @@ +{ + "rewrites": [ + { + "source": "/distribution/:path*", + "destination": "https://objectstorage.uk-london-1.oraclecloud.com/n/lrarf8ozesjn/b/bucket-20220130-2329/o/distribution/:path*" + }, + { + "source": "/release-notes/rss", + "destination": "https://api.paperlib.app/release-notes/rss" + } + ], + "redirects": [ + { + "source": "/", + "destination": "/en/", + "permanent": true + } + ] +}