mirror of
https://github.com/siyuan-note/siyuan.git
synced 2026-03-13 07:56:14 +01:00
♻️ Unified marketplace Package Type Model (#17152)
This commit is contained in:
parent
ab83e5d987
commit
3cac07dfd9
17 changed files with 1108 additions and 1919 deletions
|
|
@ -700,7 +700,7 @@ type="checkbox">
|
||||||
}, async response => {
|
}, async response => {
|
||||||
bazaar._onBazaar(response, bazaarType);
|
bazaar._onBazaar(response, bazaarType);
|
||||||
bazaar._genMyHTML(bazaarType, app, false);
|
bazaar._genMyHTML(bazaarType, app, false);
|
||||||
if (bazaarType === "plugins") {
|
if (response.code === 0 && bazaarType === "plugins") {
|
||||||
if (window.siyuan.config.bazaar.petalDisabled) {
|
if (window.siyuan.config.bazaar.petalDisabled) {
|
||||||
confirmDialog(window.siyuan.languages.confirm, window.siyuan.languages.enablePluginTip2);
|
confirmDialog(window.siyuan.languages.confirm, window.siyuan.languages.enablePluginTip2);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -756,27 +756,17 @@ type="checkbox">
|
||||||
packageName: dataObj.name,
|
packageName: dataObj.name,
|
||||||
repoHash: dataObj.repoHash,
|
repoHash: dataObj.repoHash,
|
||||||
mode: dataObj.themeMode === "dark" ? 1 : 0,
|
mode: dataObj.themeMode === "dark" ? 1 : 0,
|
||||||
update: true,
|
|
||||||
frontend: getFrontend()
|
frontend: getFrontend()
|
||||||
}, async response => {
|
}, async response => {
|
||||||
this._genMyHTML(bazaarType, app);
|
this._genMyHTML(bazaarType, app);
|
||||||
bazaar._onBazaar(response, bazaarType);
|
bazaar._onBazaar(response, bazaarType);
|
||||||
|
// TODO 集市包的相关逻辑应完全由内核处理并推送到所有前端实例,下面的代码需要确认
|
||||||
// https://github.com/siyuan-note/siyuan/issues/15177
|
// https://github.com/siyuan-note/siyuan/issues/15177
|
||||||
if (bazaarType === "themes" && response.data.appearance?.themeVer) {
|
if (bazaarType === "themes" && response.data.appearance?.themeVer) {
|
||||||
window.siyuan.config.appearance.themeVer = response.data.appearance.themeVer;
|
window.siyuan.config.appearance.themeVer = response.data.appearance.themeVer;
|
||||||
}
|
}
|
||||||
// 更新主题后不需要对该主题进行切换 https://github.com/siyuan-note/siyuan/issues/4966
|
// 更新主题后不需要对该主题进行切换 https://github.com/siyuan-note/siyuan/issues/4966
|
||||||
// https://github.com/siyuan-note/siyuan/issues/5411
|
// https://github.com/siyuan-note/siyuan/issues/5411
|
||||||
if (bazaarType === "plugins") {
|
|
||||||
app.plugins.find((item: Plugin) => {
|
|
||||||
if (item.name === dataObj.name) {
|
|
||||||
reloadPlugin(app, {
|
|
||||||
reloadPlugins: [dataObj.name],
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -1099,12 +1089,6 @@ type="checkbox">
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
_onBazaar(response: IWebSocketData, bazaarType: TBazaarType) {
|
_onBazaar(response: IWebSocketData, bazaarType: TBazaarType) {
|
||||||
if (bazaar.element.querySelector("#configBazaarReadme").classList.contains("config-bazaar__readme--show")) {
|
|
||||||
const dataObj = JSON.parse(bazaar.element.querySelector("#configBazaarReadme > .item__side").getAttribute("data-obj"));
|
|
||||||
bazaar._renderReadme((dataObj.bazaarType) as TBazaarType,
|
|
||||||
response.data.packages.find((item: IBazaarItem) => item.repoURL === dataObj.repoURL),
|
|
||||||
dataObj.downloaded);
|
|
||||||
}
|
|
||||||
let id = "#configBazaarTemplate";
|
let id = "#configBazaarTemplate";
|
||||||
if (bazaarType === "themes") {
|
if (bazaarType === "themes") {
|
||||||
id = "#configBazaarTheme";
|
id = "#configBazaarTheme";
|
||||||
|
|
@ -1117,10 +1101,18 @@ type="checkbox">
|
||||||
}
|
}
|
||||||
const element = bazaar.element.querySelector(id);
|
const element = bazaar.element.querySelector(id);
|
||||||
if (response.code === 1) {
|
if (response.code === 1) {
|
||||||
|
// 安装集市包 /api/bazaar/installBazaar* 失败
|
||||||
showMessage(response.msg);
|
showMessage(response.msg);
|
||||||
element.querySelectorAll("img[data-type='img-loading']").forEach((item) => {
|
element.querySelectorAll("img[data-type='img-loading']").forEach((item) => {
|
||||||
item.remove();
|
item.remove();
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (bazaar.element.querySelector("#configBazaarReadme").classList.contains("config-bazaar__readme--show")) {
|
||||||
|
const dataObj = JSON.parse(bazaar.element.querySelector("#configBazaarReadme > .item__side").getAttribute("data-obj"));
|
||||||
|
bazaar._renderReadme((dataObj.bazaarType) as TBazaarType,
|
||||||
|
response.data.packages.find((item: IBazaarItem) => item.repoURL === dataObj.repoURL),
|
||||||
|
dataObj.downloaded);
|
||||||
}
|
}
|
||||||
let html = "";
|
let html = "";
|
||||||
response.data.packages.forEach((item: IBazaarItem) => {
|
response.data.packages.forEach((item: IBazaarItem) => {
|
||||||
|
|
|
||||||
7
app/src/types/index.d.ts
vendored
7
app/src/types/index.d.ts
vendored
|
|
@ -878,8 +878,6 @@ interface IMenu {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IBazaarItem {
|
interface IBazaarItem {
|
||||||
incompatible?: boolean; // 仅 plugin
|
|
||||||
enabled: boolean;
|
|
||||||
preferredName: string;
|
preferredName: string;
|
||||||
minAppVersion: string;
|
minAppVersion: string;
|
||||||
preferredDesc: string;
|
preferredDesc: string;
|
||||||
|
|
@ -895,13 +893,11 @@ interface IBazaarItem {
|
||||||
outdated: false;
|
outdated: false;
|
||||||
name: string;
|
name: string;
|
||||||
previewURL: string;
|
previewURL: string;
|
||||||
previewURLThumb: string;
|
|
||||||
repoHash: string;
|
repoHash: string;
|
||||||
repoURL: string;
|
repoURL: string;
|
||||||
url: string;
|
url: string;
|
||||||
openIssues: number;
|
openIssues: number;
|
||||||
version: string;
|
version: string;
|
||||||
modes: string[];
|
|
||||||
hSize: string;
|
hSize: string;
|
||||||
hInstallSize: string;
|
hInstallSize: string;
|
||||||
hInstallDate: string;
|
hInstallDate: string;
|
||||||
|
|
@ -909,6 +905,9 @@ interface IBazaarItem {
|
||||||
preferredFunding: string;
|
preferredFunding: string;
|
||||||
disallowUpdate: boolean;
|
disallowUpdate: boolean;
|
||||||
updateRequiredMinAppVer: string;
|
updateRequiredMinAppVer: string;
|
||||||
|
incompatible?: boolean; // 仅 plugin
|
||||||
|
enabled?: boolean; // 仅 plugin
|
||||||
|
modes?: string[]; // 仅 theme
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IAV {
|
interface IAV {
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ func batchUpdatePackage(c *gin.Context) {
|
||||||
if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("frontend", true, &frontend)) {
|
if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("frontend", true, &frontend)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
model.BatchUpdateBazaarPackages(frontend)
|
model.BatchUpdatePackages(frontend)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUpdatedPackage(c *gin.Context) {
|
func getUpdatedPackage(c *gin.Context) {
|
||||||
|
|
@ -62,7 +62,7 @@ func getUpdatedPackage(c *gin.Context) {
|
||||||
if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("frontend", true, &frontend)) {
|
if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("frontend", true, &frontend)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
plugins, widgets, icons, themes, templates := model.UpdatedPackages(frontend)
|
plugins, widgets, icons, themes, templates := model.GetUpdatedPackages(frontend)
|
||||||
ret.Data = map[string]interface{}{
|
ret.Data = map[string]interface{}{
|
||||||
"plugins": plugins,
|
"plugins": plugins,
|
||||||
"widgets": widgets,
|
"widgets": widgets,
|
||||||
|
|
@ -117,7 +117,7 @@ func getBazaarPlugin(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.Data = map[string]interface{}{
|
ret.Data = map[string]interface{}{
|
||||||
"packages": model.BazaarPlugins(frontend, keyword),
|
"packages": model.GetBazaarPackages("plugins", frontend, keyword),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,7 +139,7 @@ func getInstalledPlugin(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.Data = map[string]interface{}{
|
ret.Data = map[string]interface{}{
|
||||||
"packages": model.InstalledPlugins(frontend, keyword),
|
"packages": model.GetInstalledPackages("plugins", frontend, keyword),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,7 +162,7 @@ func installBazaarPlugin(c *gin.Context) {
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := model.InstallBazaarPlugin(repoURL, repoHash, packageName)
|
err := model.InstallBazaarPackage("plugins", repoURL, repoHash, packageName, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ret.Code = 1
|
ret.Code = 1
|
||||||
ret.Msg = err.Error()
|
ret.Msg = err.Error()
|
||||||
|
|
@ -171,7 +171,7 @@ func installBazaarPlugin(c *gin.Context) {
|
||||||
|
|
||||||
util.PushMsg(model.Conf.Language(69), 3000)
|
util.PushMsg(model.Conf.Language(69), 3000)
|
||||||
ret.Data = map[string]interface{}{
|
ret.Data = map[string]interface{}{
|
||||||
"packages": model.BazaarPlugins(frontend, keyword),
|
"packages": model.GetBazaarPackages("plugins", frontend, keyword),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,7 +192,7 @@ func uninstallBazaarPlugin(c *gin.Context) {
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := model.UninstallBazaarPlugin(packageName, frontend)
|
err := model.UninstallPackage("plugins", packageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ret.Code = -1
|
ret.Code = -1
|
||||||
ret.Msg = err.Error()
|
ret.Msg = err.Error()
|
||||||
|
|
@ -200,7 +200,7 @@ func uninstallBazaarPlugin(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.Data = map[string]interface{}{
|
ret.Data = map[string]interface{}{
|
||||||
"packages": model.BazaarPlugins(frontend, keyword),
|
"packages": model.GetBazaarPackages("plugins", frontend, keyword),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,7 +219,7 @@ func getBazaarWidget(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.Data = map[string]interface{}{
|
ret.Data = map[string]interface{}{
|
||||||
"packages": model.BazaarWidgets(keyword),
|
"packages": model.GetBazaarPackages("widgets", "", keyword),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -238,7 +238,7 @@ func getInstalledWidget(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.Data = map[string]interface{}{
|
ret.Data = map[string]interface{}{
|
||||||
"packages": model.InstalledWidgets(keyword),
|
"packages": model.GetInstalledPackages("widgets", "", keyword),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -260,7 +260,7 @@ func installBazaarWidget(c *gin.Context) {
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := model.InstallBazaarWidget(repoURL, repoHash, packageName)
|
err := model.InstallBazaarPackage("widgets", repoURL, repoHash, packageName, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ret.Code = 1
|
ret.Code = 1
|
||||||
ret.Msg = err.Error()
|
ret.Msg = err.Error()
|
||||||
|
|
@ -269,7 +269,7 @@ func installBazaarWidget(c *gin.Context) {
|
||||||
|
|
||||||
util.PushMsg(model.Conf.Language(69), 3000)
|
util.PushMsg(model.Conf.Language(69), 3000)
|
||||||
ret.Data = map[string]interface{}{
|
ret.Data = map[string]interface{}{
|
||||||
"packages": model.BazaarWidgets(keyword),
|
"packages": model.GetBazaarPackages("widgets", "", keyword),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -289,7 +289,7 @@ func uninstallBazaarWidget(c *gin.Context) {
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := model.UninstallBazaarWidget(packageName)
|
err := model.UninstallPackage("widgets", packageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ret.Code = -1
|
ret.Code = -1
|
||||||
ret.Msg = err.Error()
|
ret.Msg = err.Error()
|
||||||
|
|
@ -297,7 +297,7 @@ func uninstallBazaarWidget(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.Data = map[string]interface{}{
|
ret.Data = map[string]interface{}{
|
||||||
"packages": model.BazaarWidgets(keyword),
|
"packages": model.GetBazaarPackages("widgets", "", keyword),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -316,7 +316,7 @@ func getBazaarIcon(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.Data = map[string]interface{}{
|
ret.Data = map[string]interface{}{
|
||||||
"packages": model.BazaarIcons(keyword),
|
"packages": model.GetBazaarPackages("icons", "", keyword),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -335,7 +335,7 @@ func getInstalledIcon(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.Data = map[string]interface{}{
|
ret.Data = map[string]interface{}{
|
||||||
"packages": model.InstalledIcons(keyword),
|
"packages": model.GetInstalledPackages("icons", "", keyword),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -357,7 +357,7 @@ func installBazaarIcon(c *gin.Context) {
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := model.InstallBazaarIcon(repoURL, repoHash, packageName)
|
err := model.InstallBazaarPackage("icons", repoURL, repoHash, packageName, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ret.Code = 1
|
ret.Code = 1
|
||||||
ret.Msg = err.Error()
|
ret.Msg = err.Error()
|
||||||
|
|
@ -366,7 +366,7 @@ func installBazaarIcon(c *gin.Context) {
|
||||||
util.PushMsg(model.Conf.Language(69), 3000)
|
util.PushMsg(model.Conf.Language(69), 3000)
|
||||||
|
|
||||||
ret.Data = map[string]interface{}{
|
ret.Data = map[string]interface{}{
|
||||||
"packages": model.BazaarIcons(keyword),
|
"packages": model.GetBazaarPackages("icons", "", keyword),
|
||||||
"appearance": model.Conf.Appearance,
|
"appearance": model.Conf.Appearance,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -387,7 +387,7 @@ func uninstallBazaarIcon(c *gin.Context) {
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := model.UninstallBazaarIcon(packageName)
|
err := model.UninstallPackage("icons", packageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ret.Code = -1
|
ret.Code = -1
|
||||||
ret.Msg = err.Error()
|
ret.Msg = err.Error()
|
||||||
|
|
@ -395,7 +395,7 @@ func uninstallBazaarIcon(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.Data = map[string]interface{}{
|
ret.Data = map[string]interface{}{
|
||||||
"packages": model.BazaarIcons(keyword),
|
"packages": model.GetBazaarPackages("icons", "", keyword),
|
||||||
"appearance": model.Conf.Appearance,
|
"appearance": model.Conf.Appearance,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -415,7 +415,7 @@ func getBazaarTemplate(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.Data = map[string]interface{}{
|
ret.Data = map[string]interface{}{
|
||||||
"packages": model.BazaarTemplates(keyword),
|
"packages": model.GetBazaarPackages("templates", "", keyword),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -434,7 +434,7 @@ func getInstalledTemplate(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.Data = map[string]interface{}{
|
ret.Data = map[string]interface{}{
|
||||||
"packages": model.InstalledTemplates(keyword),
|
"packages": model.GetInstalledPackages("templates", "", keyword),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -447,15 +447,16 @@ func installBazaarTemplate(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var keyword string
|
var keyword, repoURL, repoHash, packageName string
|
||||||
if keywordArg := arg["keyword"]; nil != keywordArg {
|
if !util.ParseJsonArgs(arg, ret,
|
||||||
keyword = keywordArg.(string)
|
util.BindJsonArg("keyword", false, &keyword),
|
||||||
|
util.BindJsonArg("repoURL", true, &repoURL),
|
||||||
|
util.BindJsonArg("repoHash", true, &repoHash),
|
||||||
|
util.BindJsonArg("packageName", true, &packageName),
|
||||||
|
) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
err := model.InstallBazaarPackage("templates", repoURL, repoHash, packageName, 0)
|
||||||
repoURL := arg["repoURL"].(string)
|
|
||||||
repoHash := arg["repoHash"].(string)
|
|
||||||
packageName := arg["packageName"].(string)
|
|
||||||
err := model.InstallBazaarTemplate(repoURL, repoHash, packageName)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ret.Code = 1
|
ret.Code = 1
|
||||||
ret.Msg = err.Error()
|
ret.Msg = err.Error()
|
||||||
|
|
@ -463,7 +464,7 @@ func installBazaarTemplate(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.Data = map[string]interface{}{
|
ret.Data = map[string]interface{}{
|
||||||
"packages": model.BazaarTemplates(keyword),
|
"packages": model.GetBazaarPackages("templates", "", keyword),
|
||||||
}
|
}
|
||||||
|
|
||||||
util.PushMsg(model.Conf.Language(69), 3000)
|
util.PushMsg(model.Conf.Language(69), 3000)
|
||||||
|
|
@ -478,13 +479,14 @@ func uninstallBazaarTemplate(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var keyword string
|
var keyword, packageName string
|
||||||
if keywordArg := arg["keyword"]; nil != keywordArg {
|
if !util.ParseJsonArgs(arg, ret,
|
||||||
keyword = keywordArg.(string)
|
util.BindJsonArg("keyword", false, &keyword),
|
||||||
|
util.BindJsonArg("packageName", true, &packageName),
|
||||||
|
) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
err := model.UninstallPackage("templates", packageName)
|
||||||
packageName := arg["packageName"].(string)
|
|
||||||
err := model.UninstallBazaarTemplate(packageName)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ret.Code = -1
|
ret.Code = -1
|
||||||
ret.Msg = err.Error()
|
ret.Msg = err.Error()
|
||||||
|
|
@ -492,7 +494,7 @@ func uninstallBazaarTemplate(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.Data = map[string]interface{}{
|
ret.Data = map[string]interface{}{
|
||||||
"packages": model.BazaarTemplates(keyword),
|
"packages": model.GetBazaarPackages("templates", "", keyword),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -506,12 +508,12 @@ func getBazaarTheme(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var keyword string
|
var keyword string
|
||||||
if keywordArg := arg["keyword"]; nil != keywordArg {
|
if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("keyword", false, &keyword)) {
|
||||||
keyword = keywordArg.(string)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.Data = map[string]interface{}{
|
ret.Data = map[string]interface{}{
|
||||||
"packages": model.BazaarThemes(keyword),
|
"packages": model.GetBazaarPackages("themes", "", keyword),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -525,12 +527,12 @@ func getInstalledTheme(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var keyword string
|
var keyword string
|
||||||
if keywordArg := arg["keyword"]; nil != keywordArg {
|
if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("keyword", false, &keyword)) {
|
||||||
keyword = keywordArg.(string)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.Data = map[string]interface{}{
|
ret.Data = map[string]interface{}{
|
||||||
"packages": model.InstalledThemes(keyword),
|
"packages": model.GetInstalledPackages("themes", "", keyword),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -554,11 +556,7 @@ func installBazaarTheme(c *gin.Context) {
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
update := false
|
err := model.InstallBazaarPackage("themes", repoURL, repoHash, packageName, int(mode))
|
||||||
if nil != arg["update"] {
|
|
||||||
update = arg["update"].(bool)
|
|
||||||
}
|
|
||||||
err := model.InstallBazaarTheme(repoURL, repoHash, packageName, int(mode), update)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ret.Code = 1
|
ret.Code = 1
|
||||||
ret.Msg = err.Error()
|
ret.Msg = err.Error()
|
||||||
|
|
@ -572,7 +570,7 @@ func installBazaarTheme(c *gin.Context) {
|
||||||
|
|
||||||
util.PushMsg(model.Conf.Language(69), 3000)
|
util.PushMsg(model.Conf.Language(69), 3000)
|
||||||
ret.Data = map[string]interface{}{
|
ret.Data = map[string]interface{}{
|
||||||
"packages": model.BazaarThemes(keyword),
|
"packages": model.GetBazaarPackages("themes", "", keyword),
|
||||||
"appearance": model.Conf.Appearance,
|
"appearance": model.Conf.Appearance,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -586,13 +584,14 @@ func uninstallBazaarTheme(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var keyword string
|
var keyword, packageName string
|
||||||
if keywordArg := arg["keyword"]; nil != keywordArg {
|
if !util.ParseJsonArgs(arg, ret,
|
||||||
keyword = keywordArg.(string)
|
util.BindJsonArg("keyword", false, &keyword),
|
||||||
|
util.BindJsonArg("packageName", true, &packageName),
|
||||||
|
) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
err := model.UninstallPackage("themes", packageName)
|
||||||
packageName := arg["packageName"].(string)
|
|
||||||
err := model.UninstallBazaarTheme(packageName)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ret.Code = -1
|
ret.Code = -1
|
||||||
ret.Msg = err.Error()
|
ret.Msg = err.Error()
|
||||||
|
|
@ -600,7 +599,7 @@ func uninstallBazaarTheme(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.Data = map[string]interface{}{
|
ret.Data = map[string]interface{}{
|
||||||
"packages": model.BazaarThemes(keyword),
|
"packages": model.GetBazaarPackages("themes", "", keyword),
|
||||||
"appearance": model.Conf.Appearance,
|
"appearance": model.Conf.Appearance,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
108
kernel/bazaar/bazaar.go
Normal file
108
kernel/bazaar/bazaar.go
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
// SiYuan - Refactor your thinking
|
||||||
|
// Copyright (c) 2020-present, b3log.org
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package bazaar
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/88250/go-humanize"
|
||||||
|
"github.com/araddon/dateparse"
|
||||||
|
"github.com/siyuan-note/siyuan/kernel/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetBazaarPackages 返回指定类型的在线集市包列表(plugins 类型需要传递 frontend 参数)。
|
||||||
|
func GetBazaarPackages(pkgType string, frontend string) (packages []*Package) {
|
||||||
|
result := getStageAndBazaar(pkgType)
|
||||||
|
|
||||||
|
if !result.Online || nil != result.StageErr || nil == result.StageIndex {
|
||||||
|
return make([]*Package, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
packages = make([]*Package, 0, len(result.StageIndex.Repos))
|
||||||
|
for _, repo := range result.StageIndex.Repos {
|
||||||
|
pkg := buildBazaarPackageWithMetadata(repo, result.BazaarIndex, pkgType, frontend)
|
||||||
|
if nil == pkg {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
packages = append(packages, pkg)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildBazaarPackageWithMetadata 从 StageRepo 构建带有在线元数据的集市包。
|
||||||
|
func buildBazaarPackageWithMetadata(repo *StageRepo, bazaarIndex map[string]*bazaarPackage, pkgType string, frontend string) *Package {
|
||||||
|
if nil == repo || nil == repo.Package {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pkg := *repo.Package
|
||||||
|
pkg.URL = strings.TrimSuffix(pkg.URL, "/")
|
||||||
|
repoURLHash := strings.Split(repo.URL, "@")
|
||||||
|
if 2 != len(repoURLHash) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
pkg.RepoURL = "https://github.com/" + repoURLHash[0]
|
||||||
|
pkg.RepoHash = repoURLHash[1]
|
||||||
|
|
||||||
|
// 展示信息
|
||||||
|
pkg.IconURL = util.BazaarOSSServer + "/package/" + repo.URL + "/icon.png"
|
||||||
|
pkg.PreviewURL = util.BazaarOSSServer + "/package/" + repo.URL + "/preview.png?imageslim"
|
||||||
|
pkg.PreferredName = GetPreferredLocaleString(pkg.DisplayName, pkg.Name)
|
||||||
|
pkg.PreferredDesc = GetPreferredLocaleString(pkg.Description, "")
|
||||||
|
pkg.PreferredFunding = getPreferredFunding(pkg.Funding)
|
||||||
|
|
||||||
|
// 更新信息
|
||||||
|
disallow := isBelowRequiredAppVersion(&pkg)
|
||||||
|
pkg.DisallowInstall = disallow
|
||||||
|
pkg.DisallowUpdate = disallow
|
||||||
|
pkg.UpdateRequiredMinAppVer = pkg.MinAppVersion
|
||||||
|
if "plugins" == pkgType {
|
||||||
|
incompatible := IsIncompatiblePlugin(&pkg, frontend)
|
||||||
|
pkg.Incompatible = &incompatible
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计信息
|
||||||
|
pkg.Updated = repo.Updated
|
||||||
|
pkg.HUpdated = formatUpdated(pkg.Updated)
|
||||||
|
pkg.Stars = repo.Stars
|
||||||
|
pkg.OpenIssues = repo.OpenIssues
|
||||||
|
pkg.Size = repo.Size
|
||||||
|
pkg.HSize = humanize.BytesCustomCeil(uint64(pkg.Size), 2)
|
||||||
|
pkg.InstallSize = repo.InstallSize
|
||||||
|
pkg.HInstallSize = humanize.BytesCustomCeil(uint64(pkg.InstallSize), 2)
|
||||||
|
if bp := bazaarIndex[repoURLHash[0]]; nil != bp {
|
||||||
|
pkg.Downloads = bp.Downloads
|
||||||
|
}
|
||||||
|
packageInstallSizeCache.SetDefault(pkg.RepoURL, pkg.InstallSize)
|
||||||
|
return &pkg
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatUpdated 格式化发布日期字符串。
|
||||||
|
func formatUpdated(updated string) (ret string) {
|
||||||
|
t, e := dateparse.ParseIn(updated, time.Now().Location())
|
||||||
|
if nil == e {
|
||||||
|
ret = t.Format("2006-01-02")
|
||||||
|
} else {
|
||||||
|
if strings.Contains(updated, "T") {
|
||||||
|
ret = updated[:strings.Index(updated, "T")]
|
||||||
|
} else {
|
||||||
|
ret = strings.ReplaceAll(strings.ReplaceAll(updated, "T", ""), "Z", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
@ -1,187 +0,0 @@
|
||||||
// SiYuan - Refactor your thinking
|
|
||||||
// Copyright (c) 2020-present, b3log.org
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package bazaar
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/88250/go-humanize"
|
|
||||||
"github.com/siyuan-note/logging"
|
|
||||||
"github.com/siyuan-note/siyuan/kernel/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Icon struct {
|
|
||||||
*Package
|
|
||||||
}
|
|
||||||
|
|
||||||
// Icons 返回集市图标列表
|
|
||||||
func Icons() (icons []*Icon) {
|
|
||||||
icons = []*Icon{}
|
|
||||||
result := getStageAndBazaar("icons")
|
|
||||||
|
|
||||||
if !result.Online {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if result.StageErr != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if 1 > len(result.BazaarIndex) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, repo := range result.StageIndex.Repos {
|
|
||||||
if nil == repo.Package {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
icon := buildIconFromStageRepo(repo, result.BazaarIndex)
|
|
||||||
if nil != icon {
|
|
||||||
icons = append(icons, icon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(icons, func(i, j int) bool { return icons[i].Updated > icons[j].Updated })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildIconFromStageRepo 使用 stage 内嵌的 package 构建 *Icon,不发起 HTTP 请求。
|
|
||||||
func buildIconFromStageRepo(repo *StageRepo, bazaarIndex map[string]*bazaarPackage) *Icon {
|
|
||||||
pkg := *repo.Package
|
|
||||||
pkg.URL = strings.TrimSuffix(pkg.URL, "/")
|
|
||||||
repoURLHash := strings.Split(repo.URL, "@")
|
|
||||||
if 2 != len(repoURLHash) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
pkg.RepoURL = "https://github.com/" + repoURLHash[0]
|
|
||||||
pkg.RepoHash = repoURLHash[1]
|
|
||||||
pkg.PreviewURL = util.BazaarOSSServer + "/package/" + repo.URL + "/preview.png?imageslim"
|
|
||||||
pkg.PreviewURLThumb = util.BazaarOSSServer + "/package/" + repo.URL + "/preview.png?imageView2/2/w/436/h/232"
|
|
||||||
pkg.IconURL = util.BazaarOSSServer + "/package/" + repo.URL + "/icon.png"
|
|
||||||
pkg.Updated = repo.Updated
|
|
||||||
pkg.Stars = repo.Stars
|
|
||||||
pkg.OpenIssues = repo.OpenIssues
|
|
||||||
pkg.Size = repo.Size
|
|
||||||
pkg.HSize = humanize.BytesCustomCeil(uint64(pkg.Size), 2)
|
|
||||||
pkg.InstallSize = repo.InstallSize
|
|
||||||
pkg.HInstallSize = humanize.BytesCustomCeil(uint64(pkg.InstallSize), 2)
|
|
||||||
pkg.HUpdated = formatUpdated(pkg.Updated)
|
|
||||||
pkg.PreferredFunding = getPreferredFunding(pkg.Funding)
|
|
||||||
pkg.PreferredName = GetPreferredName(&pkg)
|
|
||||||
pkg.PreferredDesc = getPreferredDesc(pkg.Description)
|
|
||||||
pkg.DisallowInstall = disallowInstallBazaarPackage(&pkg)
|
|
||||||
pkg.DisallowUpdate = disallowInstallBazaarPackage(&pkg)
|
|
||||||
pkg.UpdateRequiredMinAppVer = pkg.MinAppVersion
|
|
||||||
if bp := bazaarIndex[repoURLHash[0]]; nil != bp {
|
|
||||||
pkg.Downloads = bp.Downloads
|
|
||||||
}
|
|
||||||
packageInstallSizeCache.SetDefault(pkg.RepoURL, pkg.InstallSize)
|
|
||||||
return &Icon{Package: &pkg}
|
|
||||||
}
|
|
||||||
|
|
||||||
func InstalledIcons() (ret []*Icon) {
|
|
||||||
ret = []*Icon{}
|
|
||||||
|
|
||||||
if !util.IsPathRegularDirOrSymlinkDir(util.IconsPath) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
iconDirs, err := os.ReadDir(util.IconsPath)
|
|
||||||
if err != nil {
|
|
||||||
logging.LogWarnf("read icons folder failed: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
bazaarIcons := Icons()
|
|
||||||
|
|
||||||
for _, iconDir := range iconDirs {
|
|
||||||
if !util.IsDirRegularOrSymlink(iconDir) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
dirName := iconDir.Name()
|
|
||||||
if isBuiltInIcon(dirName) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
icon, parseErr := IconJSON(dirName)
|
|
||||||
if nil != parseErr || nil == icon {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
icon.RepoURL = icon.URL
|
|
||||||
icon.DisallowInstall = disallowInstallBazaarPackage(icon.Package)
|
|
||||||
if bazaarPkg := getBazaarIcon(icon.Name, bazaarIcons); nil != bazaarPkg {
|
|
||||||
icon.DisallowUpdate = disallowInstallBazaarPackage(bazaarPkg.Package)
|
|
||||||
icon.UpdateRequiredMinAppVer = bazaarPkg.MinAppVersion
|
|
||||||
icon.RepoURL = bazaarPkg.RepoURL
|
|
||||||
}
|
|
||||||
|
|
||||||
installPath := filepath.Join(util.IconsPath, dirName)
|
|
||||||
icon.Installed = true
|
|
||||||
icon.PreviewURL = "/appearance/icons/" + dirName + "/preview.png"
|
|
||||||
icon.PreviewURLThumb = "/appearance/icons/" + dirName + "/preview.png"
|
|
||||||
icon.IconURL = "/appearance/icons/" + dirName + "/icon.png"
|
|
||||||
icon.PreferredFunding = getPreferredFunding(icon.Funding)
|
|
||||||
icon.PreferredName = GetPreferredName(icon.Package)
|
|
||||||
icon.PreferredDesc = getPreferredDesc(icon.Description)
|
|
||||||
info, statErr := os.Stat(filepath.Join(installPath, "icon.json"))
|
|
||||||
if nil != statErr {
|
|
||||||
logging.LogWarnf("stat install icon.json failed: %s", statErr)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
icon.HInstallDate = info.ModTime().Format("2006-01-02")
|
|
||||||
if installSize, ok := packageInstallSizeCache.Get(icon.RepoURL); ok {
|
|
||||||
icon.InstallSize = installSize.(int64)
|
|
||||||
} else {
|
|
||||||
is, _ := util.SizeOfDirectory(installPath)
|
|
||||||
icon.InstallSize = is
|
|
||||||
packageInstallSizeCache.SetDefault(icon.RepoURL, is)
|
|
||||||
}
|
|
||||||
icon.HInstallSize = humanize.BytesCustomCeil(uint64(icon.InstallSize), 2)
|
|
||||||
icon.PreferredReadme = getInstalledPackageREADME(installPath, "/appearance/icons/"+dirName+"/", icon.Readme)
|
|
||||||
icon.Outdated = isOutdatedIcon(icon, bazaarIcons)
|
|
||||||
ret = append(ret, icon)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func isBuiltInIcon(dirName string) bool {
|
|
||||||
return "ant" == dirName || "material" == dirName
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBazaarIcon(name string, icon []*Icon) *Icon {
|
|
||||||
for _, p := range icon {
|
|
||||||
if p.Name == name {
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func InstallIcon(repoURL, repoHash, installPath string, systemID string) error {
|
|
||||||
repoURLHash := repoURL + "@" + repoHash
|
|
||||||
data, err := downloadPackage(repoURLHash, true, systemID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return installPackage(data, installPath, repoURLHash)
|
|
||||||
}
|
|
||||||
|
|
||||||
func UninstallIcon(installPath string) error {
|
|
||||||
return uninstallPackage(installPath)
|
|
||||||
}
|
|
||||||
148
kernel/bazaar/install.go
Normal file
148
kernel/bazaar/install.go
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
// SiYuan - Refactor your thinking
|
||||||
|
// Copyright (c) 2020-present, b3log.org
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package bazaar
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/88250/gulu"
|
||||||
|
"github.com/imroc/req/v3"
|
||||||
|
"github.com/siyuan-note/filelock"
|
||||||
|
"github.com/siyuan-note/httpclient"
|
||||||
|
"github.com/siyuan-note/logging"
|
||||||
|
"github.com/siyuan-note/siyuan/kernel/util"
|
||||||
|
"golang.org/x/sync/singleflight"
|
||||||
|
)
|
||||||
|
|
||||||
|
var downloadPackageFlight singleflight.Group
|
||||||
|
|
||||||
|
// downloadBazaarFile 下载集市文件
|
||||||
|
func downloadBazaarFile(repoURLHash string, pushProgress bool) (data []byte, err error) {
|
||||||
|
repoURLHashTrimmed := strings.TrimPrefix(repoURLHash, "https://github.com/")
|
||||||
|
v, err, _ := downloadPackageFlight.Do(repoURLHash, func() (interface{}, error) {
|
||||||
|
// repoURLHash: https://github.com/88250/Comfortably-Numb@6286912c381ef3f83e455d06ba4d369c498238dc 或带路径 /README.md
|
||||||
|
repoURL := repoURLHash[:strings.LastIndex(repoURLHash, "@")]
|
||||||
|
u := util.BazaarOSSServer + "/package/" + repoURLHashTrimmed
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
resp, err := httpclient.NewCloudFileRequest2m().SetOutput(buf).SetDownloadCallback(func(info req.DownloadInfo) {
|
||||||
|
if pushProgress {
|
||||||
|
progress := float32(info.DownloadedSize) / float32(info.Response.ContentLength)
|
||||||
|
util.PushDownloadProgress(repoURL, progress)
|
||||||
|
}
|
||||||
|
}).Get(u)
|
||||||
|
if err != nil {
|
||||||
|
logging.LogErrorf("get bazaar package [%s] failed: %s", u, err)
|
||||||
|
return nil, errors.New("get bazaar package failed, please check your network")
|
||||||
|
}
|
||||||
|
if 200 != resp.StatusCode {
|
||||||
|
logging.LogErrorf("get bazaar package [%s] failed: %d", u, resp.StatusCode)
|
||||||
|
return nil, errors.New("get bazaar package failed: " + resp.Status)
|
||||||
|
}
|
||||||
|
data := buf.Bytes()
|
||||||
|
return data, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return v.([]byte), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// incPackageDownloads 增加集市包下载次数
|
||||||
|
func incPackageDownloads(repoURL, systemID string) {
|
||||||
|
if "" == systemID {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
repo := strings.TrimPrefix(repoURL, "https://github.com/")
|
||||||
|
u := util.GetCloudServer() + "/apis/siyuan/bazaar/addBazaarPackageDownloadCount"
|
||||||
|
httpclient.NewCloudRequest30s().SetBody(
|
||||||
|
map[string]interface{}{
|
||||||
|
"systemID": systemID,
|
||||||
|
"repo": repo,
|
||||||
|
}).Post(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstallPackage 安装集市包
|
||||||
|
func InstallPackage(repoURL, repoHash, installPath, systemID, pkgType, packageName string) error {
|
||||||
|
repoURLHash := repoURL + "@" + repoHash
|
||||||
|
data, err := downloadBazaarFile(repoURLHash, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = installPackage(data, installPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录安装时间
|
||||||
|
now := time.Now()
|
||||||
|
setPackageInstallTime(pkgType, packageName, now)
|
||||||
|
|
||||||
|
// 文件夹的修改时间设置为当前安装时间
|
||||||
|
if err = os.Chtimes(installPath, now, now); err != nil {
|
||||||
|
logging.LogWarnf("set package [%s] folder mtime failed: %s", packageName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go incPackageDownloads(repoURL, systemID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func installPackage(data []byte, installPath string) (err error) {
|
||||||
|
tmpPackage := filepath.Join(util.TempDir, "bazaar", "package")
|
||||||
|
if err = os.MkdirAll(tmpPackage, 0755); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name := gulu.Rand.String(7)
|
||||||
|
tmp := filepath.Join(tmpPackage, name+".zip")
|
||||||
|
if err = os.WriteFile(tmp, data, 0644); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
unzipPath := filepath.Join(tmpPackage, name)
|
||||||
|
if err = gulu.Zip.Unzip(tmp, unzipPath); err != nil {
|
||||||
|
logging.LogErrorf("write file [%s] failed: %s", installPath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dirs, err := os.ReadDir(unzipPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
srcPath := unzipPath
|
||||||
|
if 1 == len(dirs) && dirs[0].IsDir() {
|
||||||
|
srcPath = filepath.Join(unzipPath, dirs[0].Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = filelock.Copy(srcPath, installPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// UninstallPackage 卸载集市包
|
||||||
|
func UninstallPackage(installPath string) (err error) {
|
||||||
|
if err = os.RemoveAll(installPath); err != nil {
|
||||||
|
logging.LogErrorf("remove [%s] failed: %s", installPath, err)
|
||||||
|
return fmt.Errorf("remove community package [%s] failed", filepath.Base(installPath))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
278
kernel/bazaar/installed.go
Normal file
278
kernel/bazaar/installed.go
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
// SiYuan - Refactor your thinking
|
||||||
|
// Copyright (c) 2020-present, b3log.org
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package bazaar
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/88250/go-humanize"
|
||||||
|
"github.com/88250/gulu"
|
||||||
|
gcache "github.com/patrickmn/go-cache"
|
||||||
|
"github.com/siyuan-note/filelock"
|
||||||
|
"github.com/siyuan-note/logging"
|
||||||
|
"github.com/siyuan-note/siyuan/kernel/util"
|
||||||
|
"golang.org/x/mod/semver"
|
||||||
|
"golang.org/x/sync/singleflight"
|
||||||
|
)
|
||||||
|
|
||||||
|
// packageInstallSizeCache 缓存集市包的安装大小,与 cachedStageIndex 使用相同的缓存时间
|
||||||
|
var packageInstallSizeCache = gcache.New(time.Duration(util.RhyCacheDuration)*time.Second, time.Duration(util.RhyCacheDuration)*time.Second/6) // [repoURL]*int64
|
||||||
|
|
||||||
|
// CleanBazaarPackageCache 清空集市包相关缓存(如切换语言后需刷新展示名等)
|
||||||
|
func CleanBazaarPackageCache() {
|
||||||
|
packageInstallSizeCache.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadInstalledPackageDirs 读取本地集市包的目录列表
|
||||||
|
func ReadInstalledPackageDirs(basePath string) ([]os.DirEntry, error) {
|
||||||
|
if !util.IsPathRegularDirOrSymlinkDir(basePath) {
|
||||||
|
return []os.DirEntry{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(basePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dirs := make([]os.DirEntry, 0, len(entries))
|
||||||
|
for _, e := range entries {
|
||||||
|
if util.IsDirRegularOrSymlink(e) {
|
||||||
|
dirs = append(dirs, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dirs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetInstalledPackageMetadata 设置本地集市包的通用元数据
|
||||||
|
func SetInstalledPackageMetadata(pkg *Package, installPath, baseURLPath, pkgType string, bazaarPackagesMap map[string]*Package) bool {
|
||||||
|
// 展示信息
|
||||||
|
pkg.IconURL = baseURLPath + "icon.png"
|
||||||
|
pkg.PreviewURL = baseURLPath + "preview.png"
|
||||||
|
pkg.PreferredName = GetPreferredLocaleString(pkg.DisplayName, pkg.Name)
|
||||||
|
pkg.PreferredDesc = GetPreferredLocaleString(pkg.Description, "")
|
||||||
|
pkg.PreferredReadme = getInstalledPackageREADME(installPath, baseURLPath, pkg.Readme)
|
||||||
|
pkg.PreferredFunding = getPreferredFunding(pkg.Funding)
|
||||||
|
|
||||||
|
// 更新信息
|
||||||
|
pkg.Installed = true
|
||||||
|
pkg.DisallowInstall = isBelowRequiredAppVersion(pkg)
|
||||||
|
if bazaarPkg := bazaarPackagesMap[pkg.Name]; nil != bazaarPkg {
|
||||||
|
pkg.DisallowUpdate = isBelowRequiredAppVersion(bazaarPkg)
|
||||||
|
pkg.UpdateRequiredMinAppVer = bazaarPkg.MinAppVersion
|
||||||
|
pkg.RepoURL = bazaarPkg.RepoURL // 更新链接使用在线数据,避免本地元数据的链接错误
|
||||||
|
|
||||||
|
if 0 > semver.Compare("v"+pkg.Version, "v"+bazaarPkg.Version) {
|
||||||
|
pkg.RepoHash = bazaarPkg.RepoHash
|
||||||
|
pkg.Outdated = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pkg.RepoURL = pkg.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安装信息
|
||||||
|
pkg.HInstallDate = getPackageHInstallDate(pkgType, pkg.Name, installPath)
|
||||||
|
// TODO 本地安装大小的缓存改成 1 分钟有效,打开集市包 README 的时候才遍历集市包文件夹进行统计,异步返回结果到前端显示 https://github.com/siyuan-note/siyuan/issues/16983
|
||||||
|
// 目前优先使用在线 stage 数据:不耗时,但可能不准确,比如本地旧版本与云端最新版本的安装大小可能不一致;其次使用本地目录大小:耗时,但准确
|
||||||
|
if installSize, ok := packageInstallSizeCache.Get(pkg.RepoURL); ok {
|
||||||
|
pkg.InstallSize = installSize.(int64)
|
||||||
|
} else {
|
||||||
|
size, _ := util.SizeOfDirectory(installPath)
|
||||||
|
pkg.InstallSize = size
|
||||||
|
packageInstallSizeCache.SetDefault(pkg.RepoURL, size)
|
||||||
|
}
|
||||||
|
pkg.HInstallSize = humanize.BytesCustomCeil(uint64(pkg.InstallSize), 2)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add marketplace package config item `minAppVersion` https://github.com/siyuan-note/siyuan/issues/8330
|
||||||
|
func isBelowRequiredAppVersion(pkg *Package) bool {
|
||||||
|
// 如果包没有指定 minAppVersion,则允许安装
|
||||||
|
if "" == pkg.MinAppVersion {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果包要求的 minAppVersion 大于当前版本,则不允许安装
|
||||||
|
if 0 < semver.Compare("v"+pkg.MinAppVersion, "v"+util.Ver) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// BazaarInfo 集市的持久化信息
|
||||||
|
type BazaarInfo struct {
|
||||||
|
Packages map[string]map[string]*PackageInfo `json:"packages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackageInfo 集市包的持久化信息
|
||||||
|
type PackageInfo struct {
|
||||||
|
InstallTime int64 `json:"installTime"` // 安装时间戳(毫秒)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
bazaarInfoCache *BazaarInfo
|
||||||
|
bazaarInfoModTime time.Time
|
||||||
|
bazaarInfoCacheLock = sync.RWMutex{}
|
||||||
|
bazaarInfoSingleFlight singleflight.Group
|
||||||
|
)
|
||||||
|
|
||||||
|
// getBazaarInfo 确保集市持久化信息已加载到 bazaarInfoCache
|
||||||
|
func getBazaarInfo() {
|
||||||
|
infoPath := filepath.Join(util.DataDir, "storage", "bazaar.json")
|
||||||
|
info, err := os.Stat(infoPath)
|
||||||
|
|
||||||
|
bazaarInfoCacheLock.RLock()
|
||||||
|
cache := bazaarInfoCache
|
||||||
|
modTime := bazaarInfoModTime
|
||||||
|
bazaarInfoCacheLock.RUnlock()
|
||||||
|
// 文件修改时间没变则认为缓存有效
|
||||||
|
if cache != nil && err == nil && info.ModTime().Equal(modTime) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, _ = bazaarInfoSingleFlight.Do("loadBazaarInfo", func() (interface{}, error) {
|
||||||
|
// 缓存失效时从磁盘加载
|
||||||
|
newRet := loadBazaarInfo()
|
||||||
|
// 更新缓存和修改时间
|
||||||
|
bazaarInfoCacheLock.Lock()
|
||||||
|
bazaarInfoCache = newRet
|
||||||
|
if err == nil {
|
||||||
|
bazaarInfoModTime = info.ModTime()
|
||||||
|
}
|
||||||
|
bazaarInfoCacheLock.Unlock()
|
||||||
|
return newRet, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadBazaarInfo 从磁盘加载集市持久化信息
|
||||||
|
func loadBazaarInfo() (ret *BazaarInfo) {
|
||||||
|
// 初始化一个空的 BazaarInfo,后续使用时无需判断 nil
|
||||||
|
ret = &BazaarInfo{
|
||||||
|
Packages: make(map[string]map[string]*PackageInfo),
|
||||||
|
}
|
||||||
|
|
||||||
|
infoDir := filepath.Join(util.DataDir, "storage")
|
||||||
|
if err := os.MkdirAll(infoDir, 0755); err != nil {
|
||||||
|
logging.LogErrorf("create bazaar info dir [%s] failed: %s", infoDir, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
infoPath := filepath.Join(infoDir, "bazaar.json")
|
||||||
|
if !filelock.IsExist(infoPath) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := filelock.ReadFile(infoPath)
|
||||||
|
if err != nil {
|
||||||
|
logging.LogErrorf("read bazaar info [%s] failed: %s", infoPath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = gulu.JSON.UnmarshalJSON(data, &ret); err != nil {
|
||||||
|
logging.LogErrorf("unmarshal bazaar info [%s] failed: %s", infoPath, err)
|
||||||
|
ret = &BazaarInfo{
|
||||||
|
Packages: make(map[string]map[string]*PackageInfo),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveBazaarInfo 保存集市持久化信息(调用者需持有写锁)
|
||||||
|
func saveBazaarInfo() {
|
||||||
|
infoPath := filepath.Join(util.DataDir, "storage", "bazaar.json")
|
||||||
|
|
||||||
|
data, err := gulu.JSON.MarshalIndentJSON(bazaarInfoCache, "", "\t")
|
||||||
|
if err != nil {
|
||||||
|
logging.LogErrorf("marshal bazaar info [%s] failed: %s", infoPath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = filelock.WriteFile(infoPath, data); err != nil {
|
||||||
|
logging.LogErrorf("write bazaar info [%s] failed: %s", infoPath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if fi, statErr := os.Stat(infoPath); statErr == nil {
|
||||||
|
bazaarInfoModTime = fi.ModTime()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setPackageInstallTime 设置集市包的安装时间
|
||||||
|
func setPackageInstallTime(pkgType, pkgName string, installTime time.Time) {
|
||||||
|
getBazaarInfo()
|
||||||
|
|
||||||
|
bazaarInfoCacheLock.Lock()
|
||||||
|
defer bazaarInfoCacheLock.Unlock()
|
||||||
|
|
||||||
|
if bazaarInfoCache == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if bazaarInfoCache.Packages[pkgType] == nil {
|
||||||
|
bazaarInfoCache.Packages[pkgType] = make(map[string]*PackageInfo)
|
||||||
|
}
|
||||||
|
p := bazaarInfoCache.Packages[pkgType][pkgName]
|
||||||
|
if p == nil {
|
||||||
|
p = &PackageInfo{}
|
||||||
|
bazaarInfoCache.Packages[pkgType][pkgName] = p
|
||||||
|
}
|
||||||
|
p.InstallTime = installTime.UnixMilli()
|
||||||
|
saveBazaarInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPackageHInstallDate 获取集市包的安装日期
|
||||||
|
func getPackageHInstallDate(pkgType, pkgName, installPath string) string {
|
||||||
|
getBazaarInfo()
|
||||||
|
bazaarInfoCacheLock.RLock()
|
||||||
|
var installTime int64
|
||||||
|
if bazaarInfoCache != nil && bazaarInfoCache.Packages[pkgType] != nil {
|
||||||
|
if p := bazaarInfoCache.Packages[pkgType][pkgName]; p != nil {
|
||||||
|
installTime = p.InstallTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bazaarInfoCacheLock.RUnlock()
|
||||||
|
|
||||||
|
if installTime > 0 {
|
||||||
|
return time.UnixMilli(installTime).Format("2006-01-02")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果 bazaar.json 中没有记录,使用文件夹修改时间并记录到 bazaar.json 中
|
||||||
|
fi, err := os.Stat(installPath)
|
||||||
|
if err != nil {
|
||||||
|
logging.LogWarnf("stat install package folder [%s] failed: %s", installPath, err)
|
||||||
|
return time.Now().Format("2006-01-02")
|
||||||
|
}
|
||||||
|
setPackageInstallTime(pkgType, pkgName, fi.ModTime())
|
||||||
|
|
||||||
|
return fi.ModTime().Format("2006-01-02")
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemovePackageInfo 删除集市包的持久化信息
|
||||||
|
func RemovePackageInfo(pkgType, pkgName string) {
|
||||||
|
getBazaarInfo()
|
||||||
|
|
||||||
|
bazaarInfoCacheLock.Lock()
|
||||||
|
defer bazaarInfoCacheLock.Unlock()
|
||||||
|
|
||||||
|
if bazaarInfoCache != nil && bazaarInfoCache.Packages[pkgType] != nil {
|
||||||
|
delete(bazaarInfoCache.Packages[pkgType], pkgName)
|
||||||
|
}
|
||||||
|
|
||||||
|
saveBazaarInfo()
|
||||||
|
}
|
||||||
|
|
@ -17,24 +17,16 @@
|
||||||
package bazaar
|
package bazaar
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"html"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/88250/gulu"
|
"github.com/88250/gulu"
|
||||||
"github.com/araddon/dateparse"
|
|
||||||
"github.com/imroc/req/v3"
|
|
||||||
gcache "github.com/patrickmn/go-cache"
|
|
||||||
"github.com/siyuan-note/filelock"
|
"github.com/siyuan-note/filelock"
|
||||||
"github.com/siyuan-note/httpclient"
|
|
||||||
"github.com/siyuan-note/logging"
|
"github.com/siyuan-note/logging"
|
||||||
"github.com/siyuan-note/siyuan/kernel/util"
|
"github.com/siyuan-note/siyuan/kernel/util"
|
||||||
"golang.org/x/mod/semver"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// LocaleStrings 表示按语种 key 的字符串表,key 为语种如 "default"、"en_US"、"zh_CN" 等
|
// LocaleStrings 表示按语种 key 的字符串表,key 为语种如 "default"、"en_US"、"zh_CN" 等
|
||||||
|
|
@ -47,6 +39,8 @@ type Funding struct {
|
||||||
Custom []string `json:"custom"`
|
Custom []string `json:"custom"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Package 描述了集市包元数据和传递给前端的其他信息。
|
||||||
|
// - 集市包新增元数据字段需要同步修改 bazaar 的工作流,参考 https://github.com/siyuan-note/bazaar/commit/aa36d0003139c52d8e767c6e18a635be006323e2
|
||||||
type Package struct {
|
type Package struct {
|
||||||
Author string `json:"author"`
|
Author string `json:"author"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
|
|
@ -66,12 +60,11 @@ type Package struct {
|
||||||
PreferredDesc string `json:"preferredDesc"`
|
PreferredDesc string `json:"preferredDesc"`
|
||||||
PreferredReadme string `json:"preferredReadme"`
|
PreferredReadme string `json:"preferredReadme"`
|
||||||
|
|
||||||
Name string `json:"name"`
|
Name string `json:"name"` // 包名,不一定是仓库名
|
||||||
RepoURL string `json:"repoURL"`
|
RepoURL string `json:"repoURL"` // 形式为 https://github.com/owner/repo
|
||||||
RepoHash string `json:"repoHash"`
|
RepoHash string `json:"repoHash"`
|
||||||
PreviewURL string `json:"previewURL"`
|
PreviewURL string `json:"previewURL"`
|
||||||
PreviewURLThumb string `json:"previewURLThumb"`
|
IconURL string `json:"iconURL"`
|
||||||
IconURL string `json:"iconURL"`
|
|
||||||
|
|
||||||
Installed bool `json:"installed"`
|
Installed bool `json:"installed"`
|
||||||
Outdated bool `json:"outdated"`
|
Outdated bool `json:"outdated"`
|
||||||
|
|
@ -90,11 +83,14 @@ type Package struct {
|
||||||
DisallowUpdate bool `json:"disallowUpdate"`
|
DisallowUpdate bool `json:"disallowUpdate"`
|
||||||
UpdateRequiredMinAppVer string `json:"updateRequiredMinAppVer"`
|
UpdateRequiredMinAppVer string `json:"updateRequiredMinAppVer"`
|
||||||
|
|
||||||
Incompatible bool `json:"incompatible"`
|
// 专用字段,nil 时不序列化
|
||||||
|
Incompatible *bool `json:"incompatible,omitempty"` // Plugin:是否不兼容
|
||||||
|
Enabled *bool `json:"enabled,omitempty"` // Plugin:是否启用
|
||||||
|
Modes *[]string `json:"modes,omitempty"` // Theme:支持的模式列表
|
||||||
}
|
}
|
||||||
|
|
||||||
type StageRepo struct {
|
type StageRepo struct {
|
||||||
URL string `json:"url"`
|
URL string `json:"url"` // owner/repo@hash 形式
|
||||||
Updated string `json:"updated"`
|
Updated string `json:"updated"`
|
||||||
Stars int `json:"stars"`
|
Stars int `json:"stars"`
|
||||||
OpenIssues int `json:"openIssues"`
|
OpenIssues int `json:"openIssues"`
|
||||||
|
|
@ -107,10 +103,48 @@ type StageRepo struct {
|
||||||
|
|
||||||
type StageIndex struct {
|
type StageIndex struct {
|
||||||
Repos []*StageRepo `json:"repos"`
|
Repos []*StageRepo `json:"repos"`
|
||||||
|
|
||||||
|
reposByURL map[string]*StageRepo // 不序列化,首次按 URL 查找时懒构建
|
||||||
|
reposOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
// getPreferredLocaleString 从 LocaleStrings 中按当前语种取值,无则回退 default、en_US,再回退 fallback。
|
// ParsePackageJSON 解析集市包 JSON 文件
|
||||||
func getPreferredLocaleString(m LocaleStrings, fallback string) string {
|
func ParsePackageJSON(filePath string) (ret *Package, err error) {
|
||||||
|
if !filelock.IsExist(filePath) {
|
||||||
|
err = os.ErrNotExist
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := filelock.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
logging.LogErrorf("read [%s] failed: %s", filePath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = gulu.JSON.UnmarshalJSON(data, &ret); err != nil {
|
||||||
|
logging.LogErrorf("parse [%s] failed: %s", filePath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仅对本地集市包做 HTML 转义,在线 stage 由 bazaar 工作流处理
|
||||||
|
sanitizePackageDisplayStrings(ret)
|
||||||
|
ret.URL = strings.TrimSuffix(ret.URL, "/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanitizePackageDisplayStrings 对集市包直接显示的信息做 HTML 转义,避免 XSS。
|
||||||
|
func sanitizePackageDisplayStrings(pkg *Package) {
|
||||||
|
if pkg == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for k, v := range pkg.DisplayName {
|
||||||
|
pkg.DisplayName[k] = html.EscapeString(v)
|
||||||
|
}
|
||||||
|
for k, v := range pkg.Description {
|
||||||
|
pkg.Description[k] = html.EscapeString(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPreferredLocaleString 从 LocaleStrings 中按当前语种取值,无则回退 default、en_US,再回退 fallback。
|
||||||
|
func GetPreferredLocaleString(m LocaleStrings, fallback string) string {
|
||||||
if len(m) == 0 {
|
if len(m) == 0 {
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
@ -126,40 +160,19 @@ func getPreferredLocaleString(m LocaleStrings, fallback string) string {
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPreferredName(pkg *Package) string {
|
// getPreferredFunding 获取包的首选赞助链接
|
||||||
return getPreferredLocaleString(pkg.DisplayName, pkg.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPreferredDesc(desc LocaleStrings) string {
|
|
||||||
return getPreferredLocaleString(desc, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPreferredReadme(readme LocaleStrings) string {
|
|
||||||
return getPreferredLocaleString(readme, "README.md")
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPreferredFunding(funding *Funding) string {
|
func getPreferredFunding(funding *Funding) string {
|
||||||
if nil == funding {
|
if nil == funding {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
if v := normalizeFundingURL(funding.OpenCollective, "https://opencollective.com/"); "" != v {
|
||||||
if "" != funding.OpenCollective {
|
return v
|
||||||
if strings.HasPrefix(funding.OpenCollective, "http://") || strings.HasPrefix(funding.OpenCollective, "https://") {
|
|
||||||
return funding.OpenCollective
|
|
||||||
}
|
|
||||||
return "https://opencollective.com/" + funding.OpenCollective
|
|
||||||
}
|
}
|
||||||
if "" != funding.Patreon {
|
if v := normalizeFundingURL(funding.Patreon, "https://www.patreon.com/"); "" != v {
|
||||||
if strings.HasPrefix(funding.Patreon, "http://") || strings.HasPrefix(funding.Patreon, "https://") {
|
return v
|
||||||
return funding.Patreon
|
|
||||||
}
|
|
||||||
return "https://www.patreon.com/" + funding.Patreon
|
|
||||||
}
|
}
|
||||||
if "" != funding.GitHub {
|
if v := normalizeFundingURL(funding.GitHub, "https://github.com/sponsors/"); "" != v {
|
||||||
if strings.HasPrefix(funding.GitHub, "http://") || strings.HasPrefix(funding.GitHub, "https://") {
|
return v
|
||||||
return funding.GitHub
|
|
||||||
}
|
|
||||||
return "https://github.com/sponsors/" + funding.GitHub
|
|
||||||
}
|
}
|
||||||
if 0 < len(funding.Custom) {
|
if 0 < len(funding.Custom) {
|
||||||
return funding.Custom[0]
|
return funding.Custom[0]
|
||||||
|
|
@ -167,249 +180,82 @@ func getPreferredFunding(funding *Funding) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func PluginJSON(pluginDirName string) (ret *Plugin, err error) {
|
func normalizeFundingURL(s, base string) string {
|
||||||
p := filepath.Join(util.DataDir, "plugins", pluginDirName, "plugin.json")
|
if "" == s {
|
||||||
if !filelock.IsExist(p) {
|
return ""
|
||||||
err = os.ErrNotExist
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
data, err := filelock.ReadFile(p)
|
if strings.HasPrefix(s, "https://") || strings.HasPrefix(s, "http://") {
|
||||||
if err != nil {
|
return s
|
||||||
logging.LogErrorf("read plugin.json [%s] failed: %s", p, err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if err = gulu.JSON.UnmarshalJSON(data, &ret); err != nil {
|
return base + s
|
||||||
logging.LogErrorf("parse plugin.json [%s] failed: %s", p, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ret.URL = strings.TrimSuffix(ret.URL, "/")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func WidgetJSON(widgetDirName string) (ret *Widget, err error) {
|
// FilterPackages 按关键词过滤集市包列表
|
||||||
p := filepath.Join(util.DataDir, "widgets", widgetDirName, "widget.json")
|
func FilterPackages(packages []*Package, keyword string) []*Package {
|
||||||
if !filelock.IsExist(p) {
|
keywords := getSearchKeywords(keyword)
|
||||||
err = os.ErrNotExist
|
if 0 == len(keywords) {
|
||||||
return
|
return packages
|
||||||
}
|
}
|
||||||
data, err := filelock.ReadFile(p)
|
ret := []*Package{}
|
||||||
if err != nil {
|
for _, pkg := range packages {
|
||||||
logging.LogErrorf("read widget.json [%s] failed: %s", p, err)
|
if packageContainsKeywords(pkg, keywords) {
|
||||||
return
|
ret = append(ret, pkg)
|
||||||
}
|
|
||||||
if err = gulu.JSON.UnmarshalJSON(data, &ret); err != nil {
|
|
||||||
logging.LogErrorf("parse widget.json [%s] failed: %s", p, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ret.URL = strings.TrimSuffix(ret.URL, "/")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func IconJSON(iconDirName string) (ret *Icon, err error) {
|
|
||||||
p := filepath.Join(util.IconsPath, iconDirName, "icon.json")
|
|
||||||
if !gulu.File.IsExist(p) {
|
|
||||||
err = os.ErrNotExist
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data, err := os.ReadFile(p)
|
|
||||||
if err != nil {
|
|
||||||
logging.LogErrorf("read icon.json [%s] failed: %s", p, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = gulu.JSON.UnmarshalJSON(data, &ret); err != nil {
|
|
||||||
logging.LogErrorf("parse icon.json [%s] failed: %s", p, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ret.URL = strings.TrimSuffix(ret.URL, "/")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func TemplateJSON(templateDirName string) (ret *Template, err error) {
|
|
||||||
p := filepath.Join(util.DataDir, "templates", templateDirName, "template.json")
|
|
||||||
if !filelock.IsExist(p) {
|
|
||||||
err = os.ErrNotExist
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data, err := filelock.ReadFile(p)
|
|
||||||
if err != nil {
|
|
||||||
logging.LogErrorf("read template.json [%s] failed: %s", p, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = gulu.JSON.UnmarshalJSON(data, &ret); err != nil {
|
|
||||||
logging.LogErrorf("parse template.json [%s] failed: %s", p, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ret.URL = strings.TrimSuffix(ret.URL, "/")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func ThemeJSON(themeDirName string) (ret *Theme, err error) {
|
|
||||||
p := filepath.Join(util.ThemesPath, themeDirName, "theme.json")
|
|
||||||
if !gulu.File.IsExist(p) {
|
|
||||||
err = os.ErrNotExist
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data, err := os.ReadFile(p)
|
|
||||||
if err != nil {
|
|
||||||
logging.LogErrorf("read theme.json [%s] failed: %s", p, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ret = &Theme{}
|
|
||||||
if err = gulu.JSON.UnmarshalJSON(data, &ret); err != nil {
|
|
||||||
logging.LogErrorf("parse theme.json [%s] failed: %s", p, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ret.URL = strings.TrimSuffix(ret.URL, "/")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
packageLocks = map[string]*sync.Mutex{}
|
|
||||||
packageLocksLock = sync.Mutex{}
|
|
||||||
)
|
|
||||||
|
|
||||||
func downloadPackage(repoURLHash string, pushProgress bool, systemID string) (data []byte, err error) {
|
|
||||||
packageLocksLock.Lock()
|
|
||||||
defer packageLocksLock.Unlock()
|
|
||||||
|
|
||||||
// repoURLHash: https://github.com/88250/Comfortably-Numb@6286912c381ef3f83e455d06ba4d369c498238dc
|
|
||||||
repoURL := repoURLHash[:strings.LastIndex(repoURLHash, "@")]
|
|
||||||
lock, ok := packageLocks[repoURLHash]
|
|
||||||
if !ok {
|
|
||||||
lock = &sync.Mutex{}
|
|
||||||
packageLocks[repoURLHash] = lock
|
|
||||||
}
|
|
||||||
lock.Lock()
|
|
||||||
defer lock.Unlock()
|
|
||||||
|
|
||||||
repoURLHash = strings.TrimPrefix(repoURLHash, "https://github.com/")
|
|
||||||
u := util.BazaarOSSServer + "/package/" + repoURLHash
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
resp, err := httpclient.NewCloudFileRequest2m().SetOutput(buf).SetDownloadCallback(func(info req.DownloadInfo) {
|
|
||||||
if pushProgress {
|
|
||||||
progress := float32(info.DownloadedSize) / float32(info.Response.ContentLength)
|
|
||||||
//logging.LogDebugf("downloading bazaar package [%f]", progress)
|
|
||||||
util.PushDownloadProgress(repoURL, progress)
|
|
||||||
}
|
}
|
||||||
}).Get(u)
|
|
||||||
if err != nil {
|
|
||||||
logging.LogErrorf("get bazaar package [%s] failed: %s", u, err)
|
|
||||||
return nil, errors.New("get bazaar package failed, please check your network")
|
|
||||||
}
|
}
|
||||||
if 200 != resp.StatusCode {
|
return ret
|
||||||
logging.LogErrorf("get bazaar package [%s] failed: %d", u, resp.StatusCode)
|
|
||||||
return nil, errors.New("get bazaar package failed: " + resp.Status)
|
|
||||||
}
|
|
||||||
data = buf.Bytes()
|
|
||||||
|
|
||||||
go incPackageDownloads(repoURLHash, systemID)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func incPackageDownloads(repoURLHash, systemID string) {
|
func getSearchKeywords(query string) (ret []string) {
|
||||||
if strings.Contains(repoURLHash, ".md") || "" == systemID {
|
query = strings.TrimSpace(query)
|
||||||
|
if "" == query {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
keywords := strings.Split(query, " ")
|
||||||
repo := strings.Split(repoURLHash, "@")[0]
|
for _, k := range keywords {
|
||||||
u := util.GetCloudServer() + "/apis/siyuan/bazaar/addBazaarPackageDownloadCount"
|
if "" != k {
|
||||||
httpclient.NewCloudRequest30s().SetBody(
|
ret = append(ret, strings.ToLower(k))
|
||||||
map[string]interface{}{
|
|
||||||
"systemID": systemID,
|
|
||||||
"repo": repo,
|
|
||||||
}).Post(u)
|
|
||||||
}
|
|
||||||
|
|
||||||
func uninstallPackage(installPath string) (err error) {
|
|
||||||
if err = os.RemoveAll(installPath); err != nil {
|
|
||||||
logging.LogErrorf("remove [%s] failed: %s", installPath, err)
|
|
||||||
return fmt.Errorf("remove community package [%s] failed", filepath.Base(installPath))
|
|
||||||
}
|
|
||||||
packageCache.Flush()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func installPackage(data []byte, installPath, repoURLHash string) (err error) {
|
|
||||||
err = installPackage0(data, installPath)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
packageCache.Delete(strings.TrimPrefix(repoURLHash, "https://github.com/"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func installPackage0(data []byte, installPath string) (err error) {
|
|
||||||
tmpPackage := filepath.Join(util.TempDir, "bazaar", "package")
|
|
||||||
if err = os.MkdirAll(tmpPackage, 0755); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
name := gulu.Rand.String(7)
|
|
||||||
tmp := filepath.Join(tmpPackage, name+".zip")
|
|
||||||
if err = os.WriteFile(tmp, data, 0644); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
unzipPath := filepath.Join(tmpPackage, name)
|
|
||||||
if err = gulu.Zip.Unzip(tmp, unzipPath); err != nil {
|
|
||||||
logging.LogErrorf("write file [%s] failed: %s", installPath, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dirs, err := os.ReadDir(unzipPath)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
srcPath := unzipPath
|
|
||||||
if 1 == len(dirs) && dirs[0].IsDir() {
|
|
||||||
srcPath = filepath.Join(unzipPath, dirs[0].Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = filelock.Copy(srcPath, installPath); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatUpdated(updated string) (ret string) {
|
|
||||||
t, e := dateparse.ParseIn(updated, time.Now().Location())
|
|
||||||
if nil == e {
|
|
||||||
ret = t.Format("2006-01-02")
|
|
||||||
} else {
|
|
||||||
if strings.Contains(updated, "T") {
|
|
||||||
ret = updated[:strings.Index(updated, "T")]
|
|
||||||
} else {
|
|
||||||
ret = strings.ReplaceAll(strings.ReplaceAll(updated, "T", ""), "Z", "")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add marketplace package config item `minAppVersion` https://github.com/siyuan-note/siyuan/issues/8330
|
func packageContainsKeywords(pkg *Package, keywords []string) bool {
|
||||||
func disallowInstallBazaarPackage(pkg *Package) bool {
|
if 0 == len(keywords) {
|
||||||
// 如果包没有指定 minAppVersion,则允许安装
|
return true
|
||||||
if "" == pkg.MinAppVersion {
|
}
|
||||||
|
if nil == pkg {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
for _, kw := range keywords {
|
||||||
|
if !packageContainsKeyword(pkg, kw) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// 如果包要求的 minAppVersion 大于当前版本,则不允许安装
|
func packageContainsKeyword(pkg *Package, kw string) bool {
|
||||||
if 0 < semver.Compare("v"+pkg.MinAppVersion, "v"+util.Ver) {
|
if strings.Contains(strings.ToLower(pkg.Name), kw) || // https://github.com/siyuan-note/siyuan/issues/10515
|
||||||
|
strings.Contains(strings.ToLower(pkg.Author), kw) { // https://github.com/siyuan-note/siyuan/issues/11673
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, s := range pkg.DisplayName {
|
||||||
|
if strings.Contains(strings.ToLower(s), kw) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range pkg.Description {
|
||||||
|
if strings.Contains(strings.ToLower(s), kw) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range pkg.Keywords {
|
||||||
|
if strings.Contains(strings.ToLower(s), kw) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(strings.ToLower(path.Base(pkg.RepoURL)), kw) { // 仓库名,不一定是包名
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
var packageCache = gcache.New(6*time.Hour, 30*time.Minute) // [repoURL]*Package
|
|
||||||
|
|
||||||
func CleanBazaarPackageCache() {
|
|
||||||
packageCache.Flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
var packageInstallSizeCache = gcache.New(48*time.Hour, 6*time.Hour) // [repoURL]*int64
|
|
||||||
|
|
|
||||||
|
|
@ -20,83 +20,11 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/88250/go-humanize"
|
|
||||||
"github.com/siyuan-note/logging"
|
"github.com/siyuan-note/logging"
|
||||||
"github.com/siyuan-note/siyuan/kernel/util"
|
"github.com/siyuan-note/siyuan/kernel/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Plugin struct {
|
|
||||||
*Package
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Plugins 返回集市插件列表
|
|
||||||
func Plugins(frontend string) (plugins []*Plugin) {
|
|
||||||
plugins = []*Plugin{}
|
|
||||||
result := getStageAndBazaar("plugins")
|
|
||||||
|
|
||||||
if !result.Online {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if result.StageErr != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if 1 > len(result.BazaarIndex) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, repo := range result.StageIndex.Repos {
|
|
||||||
if nil == repo.Package {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
plugin := buildPluginFromStageRepo(repo, frontend, result.BazaarIndex)
|
|
||||||
if nil != plugin {
|
|
||||||
plugins = append(plugins, plugin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(plugins, func(i, j int) bool { return plugins[i].Updated > plugins[j].Updated })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildPluginFromStageRepo 使用 stage 内嵌的 package 构建 *Plugin,不发起 HTTP 请求。
|
|
||||||
func buildPluginFromStageRepo(repo *StageRepo, frontend string, bazaarIndex map[string]*bazaarPackage) *Plugin {
|
|
||||||
pkg := *repo.Package
|
|
||||||
pkg.URL = strings.TrimSuffix(pkg.URL, "/")
|
|
||||||
repoURLHash := strings.Split(repo.URL, "@")
|
|
||||||
if 2 != len(repoURLHash) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
pkg.RepoURL = "https://github.com/" + repoURLHash[0]
|
|
||||||
pkg.RepoHash = repoURLHash[1]
|
|
||||||
pkg.PreviewURL = util.BazaarOSSServer + "/package/" + repo.URL + "/preview.png?imageslim"
|
|
||||||
pkg.PreviewURLThumb = util.BazaarOSSServer + "/package/" + repo.URL + "/preview.png?imageView2/2/w/436/h/232"
|
|
||||||
pkg.IconURL = util.BazaarOSSServer + "/package/" + repo.URL + "/icon.png"
|
|
||||||
pkg.Updated = repo.Updated
|
|
||||||
pkg.Stars = repo.Stars
|
|
||||||
pkg.OpenIssues = repo.OpenIssues
|
|
||||||
pkg.Size = repo.Size
|
|
||||||
pkg.HSize = humanize.BytesCustomCeil(uint64(pkg.Size), 2)
|
|
||||||
pkg.InstallSize = repo.InstallSize
|
|
||||||
pkg.HInstallSize = humanize.BytesCustomCeil(uint64(pkg.InstallSize), 2)
|
|
||||||
pkg.HUpdated = formatUpdated(pkg.Updated)
|
|
||||||
pkg.PreferredFunding = getPreferredFunding(pkg.Funding)
|
|
||||||
pkg.PreferredName = GetPreferredName(&pkg)
|
|
||||||
pkg.PreferredDesc = getPreferredDesc(pkg.Description)
|
|
||||||
pkg.DisallowInstall = disallowInstallBazaarPackage(&pkg)
|
|
||||||
pkg.DisallowUpdate = disallowInstallBazaarPackage(&pkg)
|
|
||||||
pkg.UpdateRequiredMinAppVer = pkg.MinAppVersion
|
|
||||||
pkg.Incompatible = isIncompatiblePlugin(&Plugin{Package: &pkg}, frontend)
|
|
||||||
if bp := bazaarIndex[repoURLHash[0]]; nil != bp {
|
|
||||||
pkg.Downloads = bp.Downloads
|
|
||||||
}
|
|
||||||
packageInstallSizeCache.SetDefault(pkg.RepoURL, pkg.InstallSize)
|
|
||||||
return &Plugin{Package: &pkg}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseInstalledPlugin(name, frontend string) (found bool, displayName string, incompatible, disabledInPublish, disallowInstall bool) {
|
func ParseInstalledPlugin(name, frontend string) (found bool, displayName string, incompatible, disabledInPublish, disallowInstall bool) {
|
||||||
pluginsPath := filepath.Join(util.DataDir, "plugins")
|
pluginsPath := filepath.Join(util.DataDir, "plugins")
|
||||||
if !util.IsPathRegularDirOrSymlinkDir(pluginsPath) {
|
if !util.IsPathRegularDirOrSymlinkDir(pluginsPath) {
|
||||||
|
|
@ -118,142 +46,57 @@ func ParseInstalledPlugin(name, frontend string) (found bool, displayName string
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
plugin, parseErr := PluginJSON(dirName)
|
plugin, parseErr := ParsePackageJSON(filepath.Join(util.DataDir, "plugins", dirName, "plugin.json"))
|
||||||
if nil != parseErr || nil == plugin {
|
if nil != parseErr || nil == plugin {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
found = true
|
found = true
|
||||||
displayName = GetPreferredName(plugin.Package)
|
displayName = GetPreferredLocaleString(plugin.DisplayName, plugin.Name)
|
||||||
incompatible = isIncompatiblePlugin(plugin, frontend)
|
incompatible = IsIncompatiblePlugin(plugin, frontend)
|
||||||
disabledInPublish = plugin.DisabledInPublish
|
disabledInPublish = plugin.DisabledInPublish
|
||||||
disallowInstall = disallowInstallBazaarPackage(plugin.Package)
|
disallowInstall = isBelowRequiredAppVersion(plugin)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func InstalledPlugins(frontend string) (ret []*Plugin) {
|
// IsIncompatiblePlugin 判断插件是否与当前环境不兼容
|
||||||
ret = []*Plugin{}
|
func IsIncompatiblePlugin(plugin *Package, frontend string) bool {
|
||||||
|
backend := getCurrentBackend()
|
||||||
pluginsPath := filepath.Join(util.DataDir, "plugins")
|
if !isTargetSupported(plugin.Backends, backend) {
|
||||||
if !util.IsPathRegularDirOrSymlinkDir(pluginsPath) {
|
return true
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginDirs, err := os.ReadDir(pluginsPath)
|
if !isTargetSupported(plugin.Frontends, frontend) {
|
||||||
if err != nil {
|
return true
|
||||||
logging.LogWarnf("read plugins folder failed: %s", err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bazaarPlugins := Plugins(frontend)
|
return false
|
||||||
|
|
||||||
for _, pluginDir := range pluginDirs {
|
|
||||||
if !util.IsDirRegularOrSymlink(pluginDir) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
dirName := pluginDir.Name()
|
|
||||||
|
|
||||||
plugin, parseErr := PluginJSON(dirName)
|
|
||||||
if nil != parseErr || nil == plugin {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
plugin.RepoURL = plugin.URL
|
|
||||||
plugin.DisallowInstall = disallowInstallBazaarPackage(plugin.Package)
|
|
||||||
if bazaarPkg := getBazaarPlugin(plugin.Name, bazaarPlugins); nil != bazaarPkg {
|
|
||||||
plugin.DisallowUpdate = disallowInstallBazaarPackage(bazaarPkg.Package)
|
|
||||||
plugin.UpdateRequiredMinAppVer = bazaarPkg.MinAppVersion
|
|
||||||
plugin.RepoURL = bazaarPkg.RepoURL
|
|
||||||
}
|
|
||||||
|
|
||||||
installPath := filepath.Join(util.DataDir, "plugins", dirName)
|
|
||||||
plugin.Installed = true
|
|
||||||
plugin.PreviewURL = "/plugins/" + dirName + "/preview.png"
|
|
||||||
plugin.PreviewURLThumb = "/plugins/" + dirName + "/preview.png"
|
|
||||||
plugin.IconURL = "/plugins/" + dirName + "/icon.png"
|
|
||||||
plugin.PreferredFunding = getPreferredFunding(plugin.Funding)
|
|
||||||
plugin.PreferredName = GetPreferredName(plugin.Package)
|
|
||||||
plugin.PreferredDesc = getPreferredDesc(plugin.Description)
|
|
||||||
info, statErr := os.Stat(filepath.Join(installPath, "plugin.json"))
|
|
||||||
if nil != statErr {
|
|
||||||
logging.LogWarnf("stat install plugin.json failed: %s", statErr)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
plugin.HInstallDate = info.ModTime().Format("2006-01-02")
|
|
||||||
if installSize, ok := packageInstallSizeCache.Get(plugin.RepoURL); ok {
|
|
||||||
plugin.InstallSize = installSize.(int64)
|
|
||||||
} else {
|
|
||||||
is, _ := util.SizeOfDirectory(installPath)
|
|
||||||
plugin.InstallSize = is
|
|
||||||
packageInstallSizeCache.SetDefault(plugin.RepoURL, is)
|
|
||||||
}
|
|
||||||
plugin.HInstallSize = humanize.BytesCustomCeil(uint64(plugin.InstallSize), 2)
|
|
||||||
plugin.PreferredReadme = getInstalledPackageREADME(installPath, "/plugins/"+dirName+"/", plugin.Readme)
|
|
||||||
plugin.Outdated = isOutdatedPlugin(plugin, bazaarPlugins)
|
|
||||||
plugin.Incompatible = isIncompatiblePlugin(plugin, frontend)
|
|
||||||
ret = append(ret, plugin)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getBazaarPlugin(name string, plugins []*Plugin) *Plugin {
|
var cachedBackend string
|
||||||
for _, p := range plugins {
|
|
||||||
if p.Name == name {
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func InstallPlugin(repoURL, repoHash, installPath string, systemID string) error {
|
|
||||||
repoURLHash := repoURL + "@" + repoHash
|
|
||||||
data, err := downloadPackage(repoURLHash, true, systemID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return installPackage(data, installPath, repoURLHash)
|
|
||||||
}
|
|
||||||
|
|
||||||
func UninstallPlugin(installPath string) error {
|
|
||||||
return uninstallPackage(installPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isIncompatiblePlugin(plugin *Plugin, currentFrontend string) bool {
|
|
||||||
if 1 > len(plugin.Backends) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
currentBackend := getCurrentBackend()
|
|
||||||
backendOk := false
|
|
||||||
for _, backend := range plugin.Backends {
|
|
||||||
if backend == currentBackend || "all" == backend {
|
|
||||||
backendOk = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
frontendOk := false
|
|
||||||
for _, frontend := range plugin.Frontends {
|
|
||||||
if frontend == currentFrontend || "all" == frontend {
|
|
||||||
frontendOk = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return !backendOk || !frontendOk
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCurrentBackend() string {
|
func getCurrentBackend() string {
|
||||||
switch util.Container {
|
if cachedBackend == "" {
|
||||||
case util.ContainerDocker:
|
if util.Container == util.ContainerStd {
|
||||||
return "docker"
|
cachedBackend = runtime.GOOS
|
||||||
case util.ContainerIOS:
|
} else {
|
||||||
return "ios"
|
cachedBackend = util.Container
|
||||||
case util.ContainerAndroid:
|
}
|
||||||
return "android"
|
|
||||||
case util.ContainerHarmony:
|
|
||||||
return "harmony"
|
|
||||||
default:
|
|
||||||
return runtime.GOOS
|
|
||||||
}
|
}
|
||||||
|
return cachedBackend
|
||||||
|
}
|
||||||
|
|
||||||
|
// isTargetSupported 检查 platforms 中是否包含 target 或 "all"
|
||||||
|
func isTargetSupported(platforms []string, target string) bool {
|
||||||
|
// 缺失字段时跳过检查,相当于 all
|
||||||
|
if len(platforms) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, v := range platforms {
|
||||||
|
if v == target || v == "all" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ import (
|
||||||
|
|
||||||
// getReadmeFileCandidates 根据包的 README 配置返回去重的按优先级排序的 README 候选文件名列表:当前语言首选、default、README.md。
|
// getReadmeFileCandidates 根据包的 README 配置返回去重的按优先级排序的 README 候选文件名列表:当前语言首选、default、README.md。
|
||||||
func getReadmeFileCandidates(readme LocaleStrings) []string {
|
func getReadmeFileCandidates(readme LocaleStrings) []string {
|
||||||
preferred := getPreferredReadme(readme)
|
preferred := GetPreferredLocaleString(readme, "README.md")
|
||||||
defaultName := "README.md"
|
defaultName := "README.md"
|
||||||
if v := strings.TrimSpace(readme["default"]); v != "" {
|
if v := strings.TrimSpace(readme["default"]); v != "" {
|
||||||
defaultName = v
|
defaultName = v
|
||||||
|
|
@ -76,7 +76,7 @@ func GetBazaarPackageREADME(ctx context.Context, repoURL, repoHash, packageType
|
||||||
var loadErr error
|
var loadErr error
|
||||||
var errMsgs []string
|
var errMsgs []string
|
||||||
for _, name := range candidates {
|
for _, name := range candidates {
|
||||||
data, loadErr = downloadPackage(repoURLHash+"/"+name, false, "")
|
data, loadErr = downloadBazaarFile(repoURLHash+"/"+name, false)
|
||||||
if loadErr == nil {
|
if loadErr == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,12 @@ package bazaar
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/siyuan-note/httpclient"
|
"github.com/siyuan-note/httpclient"
|
||||||
"github.com/siyuan-note/logging"
|
"github.com/siyuan-note/logging"
|
||||||
"github.com/siyuan-note/siyuan/kernel/util"
|
"github.com/siyuan-note/siyuan/kernel/util"
|
||||||
"golang.org/x/mod/semver"
|
|
||||||
"golang.org/x/sync/singleflight"
|
"golang.org/x/sync/singleflight"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -164,106 +162,6 @@ func getStageIndex(ctx context.Context, pkgType string) (ret *StageIndex, err er
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func isOutdatedTheme(theme *Theme, bazaarThemes []*Theme) bool {
|
|
||||||
if !strings.HasPrefix(theme.URL, "https://github.com/") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
repo := strings.TrimPrefix(theme.URL, "https://github.com/")
|
|
||||||
parts := strings.Split(repo, "/")
|
|
||||||
if 2 != len(parts) || "" == strings.TrimSpace(parts[1]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pkg := range bazaarThemes {
|
|
||||||
if theme.Name == pkg.Name && 0 > semver.Compare("v"+theme.Version, "v"+pkg.Version) {
|
|
||||||
theme.RepoHash = pkg.RepoHash
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isOutdatedIcon(icon *Icon, bazaarIcons []*Icon) bool {
|
|
||||||
if !strings.HasPrefix(icon.URL, "https://github.com/") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
repo := strings.TrimPrefix(icon.URL, "https://github.com/")
|
|
||||||
parts := strings.Split(repo, "/")
|
|
||||||
if 2 != len(parts) || "" == strings.TrimSpace(parts[1]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pkg := range bazaarIcons {
|
|
||||||
if icon.Name == pkg.Name && 0 > semver.Compare("v"+icon.Version, "v"+pkg.Version) {
|
|
||||||
icon.RepoHash = pkg.RepoHash
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isOutdatedPlugin(plugin *Plugin, bazaarPlugins []*Plugin) bool {
|
|
||||||
if !strings.HasPrefix(plugin.URL, "https://github.com/") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
repo := strings.TrimPrefix(plugin.URL, "https://github.com/")
|
|
||||||
parts := strings.Split(repo, "/")
|
|
||||||
if 2 != len(parts) || "" == strings.TrimSpace(parts[1]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pkg := range bazaarPlugins {
|
|
||||||
if plugin.Name == pkg.Name && 0 > semver.Compare("v"+plugin.Version, "v"+pkg.Version) {
|
|
||||||
plugin.RepoHash = pkg.RepoHash
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isOutdatedWidget(widget *Widget, bazaarWidgets []*Widget) bool {
|
|
||||||
if !strings.HasPrefix(widget.URL, "https://github.com/") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
repo := strings.TrimPrefix(widget.URL, "https://github.com/")
|
|
||||||
parts := strings.Split(repo, "/")
|
|
||||||
if 2 != len(parts) || "" == strings.TrimSpace(parts[1]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pkg := range bazaarWidgets {
|
|
||||||
if widget.Name == pkg.Name && 0 > semver.Compare("v"+widget.Version, "v"+pkg.Version) {
|
|
||||||
widget.RepoHash = pkg.RepoHash
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isOutdatedTemplate(template *Template, bazaarTemplates []*Template) bool {
|
|
||||||
if !strings.HasPrefix(template.URL, "https://github.com/") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
repo := strings.TrimPrefix(template.URL, "https://github.com/")
|
|
||||||
parts := strings.Split(repo, "/")
|
|
||||||
if 2 != len(parts) || "" == strings.TrimSpace(parts[1]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pkg := range bazaarTemplates {
|
|
||||||
if template.Name == pkg.Name && 0 > semver.Compare("v"+template.Version, "v"+pkg.Version) {
|
|
||||||
template.RepoHash = pkg.RepoHash
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isBazzarOnline() bool {
|
func isBazzarOnline() bool {
|
||||||
v, err, _ := onlineCheckFlight.Do("bazaarOnline", func() (interface{}, error) {
|
v, err, _ := onlineCheckFlight.Do("bazaarOnline", func() (interface{}, error) {
|
||||||
return isBazzarOnline0(), nil
|
return isBazzarOnline0(), nil
|
||||||
|
|
|
||||||
|
|
@ -1,202 +0,0 @@
|
||||||
// SiYuan - Refactor your thinking
|
|
||||||
// Copyright (c) 2020-present, b3log.org
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package bazaar
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/88250/go-humanize"
|
|
||||||
"github.com/siyuan-note/logging"
|
|
||||||
"github.com/siyuan-note/siyuan/kernel/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Template struct {
|
|
||||||
*Package
|
|
||||||
}
|
|
||||||
|
|
||||||
// Templates 返回集市模板列表
|
|
||||||
func Templates() (templates []*Template) {
|
|
||||||
templates = []*Template{}
|
|
||||||
result := getStageAndBazaar("templates")
|
|
||||||
|
|
||||||
if !result.Online {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if result.StageErr != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if 1 > len(result.BazaarIndex) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, repo := range result.StageIndex.Repos {
|
|
||||||
if nil == repo.Package {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
template := buildTemplateFromStageRepo(repo, result.BazaarIndex)
|
|
||||||
if nil != template {
|
|
||||||
templates = append(templates, template)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
templates = filterLegacyTemplates(templates)
|
|
||||||
|
|
||||||
sort.Slice(templates, func(i, j int) bool { return templates[i].Updated > templates[j].Updated })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildTemplateFromStageRepo 使用 stage 内嵌的 package 构建 *Template,不发起 HTTP 请求。
|
|
||||||
func buildTemplateFromStageRepo(repo *StageRepo, bazaarIndex map[string]*bazaarPackage) *Template {
|
|
||||||
pkg := *repo.Package
|
|
||||||
pkg.URL = strings.TrimSuffix(pkg.URL, "/")
|
|
||||||
repoURLHash := strings.Split(repo.URL, "@")
|
|
||||||
if 2 != len(repoURLHash) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
pkg.RepoURL = "https://github.com/" + repoURLHash[0]
|
|
||||||
pkg.RepoHash = repoURLHash[1]
|
|
||||||
pkg.PreviewURL = util.BazaarOSSServer + "/package/" + repo.URL + "/preview.png?imageslim"
|
|
||||||
pkg.PreviewURLThumb = util.BazaarOSSServer + "/package/" + repo.URL + "/preview.png?imageView2/2/w/436/h/232"
|
|
||||||
pkg.IconURL = util.BazaarOSSServer + "/package/" + repo.URL + "/icon.png"
|
|
||||||
pkg.Updated = repo.Updated
|
|
||||||
pkg.Stars = repo.Stars
|
|
||||||
pkg.OpenIssues = repo.OpenIssues
|
|
||||||
pkg.Size = repo.Size
|
|
||||||
pkg.HSize = humanize.BytesCustomCeil(uint64(pkg.Size), 2)
|
|
||||||
pkg.InstallSize = repo.InstallSize
|
|
||||||
pkg.HInstallSize = humanize.BytesCustomCeil(uint64(pkg.InstallSize), 2)
|
|
||||||
pkg.HUpdated = formatUpdated(pkg.Updated)
|
|
||||||
pkg.PreferredFunding = getPreferredFunding(pkg.Funding)
|
|
||||||
pkg.PreferredName = GetPreferredName(&pkg)
|
|
||||||
pkg.PreferredDesc = getPreferredDesc(pkg.Description)
|
|
||||||
pkg.DisallowInstall = disallowInstallBazaarPackage(&pkg)
|
|
||||||
pkg.DisallowUpdate = disallowInstallBazaarPackage(&pkg)
|
|
||||||
pkg.UpdateRequiredMinAppVer = pkg.MinAppVersion
|
|
||||||
if bp := bazaarIndex[repoURLHash[0]]; nil != bp {
|
|
||||||
pkg.Downloads = bp.Downloads
|
|
||||||
}
|
|
||||||
packageInstallSizeCache.SetDefault(pkg.RepoURL, pkg.InstallSize)
|
|
||||||
return &Template{Package: &pkg}
|
|
||||||
}
|
|
||||||
|
|
||||||
func InstalledTemplates() (ret []*Template) {
|
|
||||||
ret = []*Template{}
|
|
||||||
|
|
||||||
templatesPath := filepath.Join(util.DataDir, "templates")
|
|
||||||
if !util.IsPathRegularDirOrSymlinkDir(templatesPath) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
templateDirs, err := os.ReadDir(templatesPath)
|
|
||||||
if err != nil {
|
|
||||||
logging.LogWarnf("read templates folder failed: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
bazaarTemplates := Templates()
|
|
||||||
|
|
||||||
for _, templateDir := range templateDirs {
|
|
||||||
if !util.IsDirRegularOrSymlink(templateDir) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
dirName := templateDir.Name()
|
|
||||||
|
|
||||||
template, parseErr := TemplateJSON(dirName)
|
|
||||||
if nil != parseErr || nil == template {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
template.RepoURL = template.URL
|
|
||||||
template.DisallowInstall = disallowInstallBazaarPackage(template.Package)
|
|
||||||
if bazaarPkg := getBazaarTemplate(template.Name, bazaarTemplates); nil != bazaarPkg {
|
|
||||||
template.DisallowUpdate = disallowInstallBazaarPackage(bazaarPkg.Package)
|
|
||||||
template.UpdateRequiredMinAppVer = bazaarPkg.MinAppVersion
|
|
||||||
template.RepoURL = bazaarPkg.RepoURL
|
|
||||||
}
|
|
||||||
|
|
||||||
installPath := filepath.Join(util.DataDir, "templates", dirName)
|
|
||||||
template.Installed = true
|
|
||||||
template.PreviewURL = "/templates/" + dirName + "/preview.png"
|
|
||||||
template.PreviewURLThumb = "/templates/" + dirName + "/preview.png"
|
|
||||||
template.IconURL = "/templates/" + dirName + "/icon.png"
|
|
||||||
template.PreferredFunding = getPreferredFunding(template.Funding)
|
|
||||||
template.PreferredName = GetPreferredName(template.Package)
|
|
||||||
template.PreferredDesc = getPreferredDesc(template.Description)
|
|
||||||
info, statErr := os.Stat(filepath.Join(installPath, "template.json"))
|
|
||||||
if nil != statErr {
|
|
||||||
logging.LogWarnf("stat install template.json failed: %s", statErr)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
template.HInstallDate = info.ModTime().Format("2006-01-02")
|
|
||||||
if installSize, ok := packageInstallSizeCache.Get(template.RepoURL); ok {
|
|
||||||
template.InstallSize = installSize.(int64)
|
|
||||||
} else {
|
|
||||||
is, _ := util.SizeOfDirectory(installPath)
|
|
||||||
template.InstallSize = is
|
|
||||||
packageInstallSizeCache.SetDefault(template.RepoURL, is)
|
|
||||||
}
|
|
||||||
template.HInstallSize = humanize.BytesCustomCeil(uint64(template.InstallSize), 2)
|
|
||||||
template.PreferredReadme = getInstalledPackageREADME(installPath, "/templates/"+dirName+"/", template.Readme)
|
|
||||||
template.Outdated = isOutdatedTemplate(template, bazaarTemplates)
|
|
||||||
ret = append(ret, template)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBazaarTemplate(name string, templates []*Template) *Template {
|
|
||||||
for _, p := range templates {
|
|
||||||
if p.Name == name {
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func InstallTemplate(repoURL, repoHash, installPath string, systemID string) error {
|
|
||||||
repoURLHash := repoURL + "@" + repoHash
|
|
||||||
data, err := downloadPackage(repoURLHash, true, systemID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return installPackage(data, installPath, repoURLHash)
|
|
||||||
}
|
|
||||||
|
|
||||||
func UninstallTemplate(installPath string) error {
|
|
||||||
return uninstallPackage(installPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterLegacyTemplates(templates []*Template) (ret []*Template) {
|
|
||||||
verTime, _ := time.Parse("2006-01-02T15:04:05", "2021-05-12T00:00:00")
|
|
||||||
for _, theme := range templates {
|
|
||||||
if "" != theme.Updated {
|
|
||||||
updated := theme.Updated[:len("2006-01-02T15:04:05")]
|
|
||||||
t, err := time.Parse("2006-01-02T15:04:05", updated)
|
|
||||||
if err != nil {
|
|
||||||
logging.LogErrorf("convert update time [%s] failed: %s", updated, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if t.After(verTime) {
|
|
||||||
ret = append(ret, theme)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
@ -1,190 +0,0 @@
|
||||||
// SiYuan - Refactor your thinking
|
|
||||||
// Copyright (c) 2020-present, b3log.org
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package bazaar
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/88250/go-humanize"
|
|
||||||
"github.com/siyuan-note/logging"
|
|
||||||
"github.com/siyuan-note/siyuan/kernel/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Theme struct {
|
|
||||||
*Package
|
|
||||||
|
|
||||||
Modes []string `json:"modes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Themes 返回集市主题列表
|
|
||||||
func Themes() (ret []*Theme) {
|
|
||||||
ret = []*Theme{}
|
|
||||||
result := getStageAndBazaar("themes")
|
|
||||||
|
|
||||||
if !result.Online {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if result.StageErr != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if 1 > len(result.BazaarIndex) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, repo := range result.StageIndex.Repos {
|
|
||||||
if nil == repo.Package {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
theme := buildThemeFromStageRepo(repo, result.BazaarIndex)
|
|
||||||
if nil != theme {
|
|
||||||
ret = append(ret, theme)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(ret, func(i, j int) bool { return ret[i].Updated > ret[j].Updated })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildThemeFromStageRepo 使用 stage 内嵌的 package 构建 *Theme,不发起 HTTP 请求。
|
|
||||||
func buildThemeFromStageRepo(repo *StageRepo, bazaarIndex map[string]*bazaarPackage) *Theme {
|
|
||||||
pkg := *repo.Package
|
|
||||||
pkg.URL = strings.TrimSuffix(pkg.URL, "/")
|
|
||||||
repoURLHash := strings.Split(repo.URL, "@")
|
|
||||||
if 2 != len(repoURLHash) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
pkg.RepoURL = "https://github.com/" + repoURLHash[0]
|
|
||||||
pkg.RepoHash = repoURLHash[1]
|
|
||||||
pkg.PreviewURL = util.BazaarOSSServer + "/package/" + repo.URL + "/preview.png?imageslim"
|
|
||||||
pkg.PreviewURLThumb = util.BazaarOSSServer + "/package/" + repo.URL + "/preview.png?imageView2/2/w/436/h/232"
|
|
||||||
pkg.IconURL = util.BazaarOSSServer + "/package/" + repo.URL + "/icon.png"
|
|
||||||
pkg.Updated = repo.Updated
|
|
||||||
pkg.Stars = repo.Stars
|
|
||||||
pkg.OpenIssues = repo.OpenIssues
|
|
||||||
pkg.Size = repo.Size
|
|
||||||
pkg.HSize = humanize.BytesCustomCeil(uint64(pkg.Size), 2)
|
|
||||||
pkg.InstallSize = repo.InstallSize
|
|
||||||
pkg.HInstallSize = humanize.BytesCustomCeil(uint64(pkg.InstallSize), 2)
|
|
||||||
pkg.HUpdated = formatUpdated(pkg.Updated)
|
|
||||||
pkg.PreferredFunding = getPreferredFunding(pkg.Funding)
|
|
||||||
pkg.PreferredName = GetPreferredName(&pkg)
|
|
||||||
pkg.PreferredDesc = getPreferredDesc(pkg.Description)
|
|
||||||
pkg.DisallowInstall = disallowInstallBazaarPackage(&pkg)
|
|
||||||
pkg.DisallowUpdate = disallowInstallBazaarPackage(&pkg)
|
|
||||||
pkg.UpdateRequiredMinAppVer = pkg.MinAppVersion
|
|
||||||
if bp := bazaarIndex[repoURLHash[0]]; nil != bp {
|
|
||||||
pkg.Downloads = bp.Downloads
|
|
||||||
}
|
|
||||||
packageInstallSizeCache.SetDefault(pkg.RepoURL, pkg.InstallSize)
|
|
||||||
theme := &Theme{Package: &pkg, Modes: []string{}}
|
|
||||||
return theme
|
|
||||||
}
|
|
||||||
|
|
||||||
func InstalledThemes() (ret []*Theme) {
|
|
||||||
ret = []*Theme{}
|
|
||||||
|
|
||||||
if !util.IsPathRegularDirOrSymlinkDir(util.ThemesPath) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
themeDirs, err := os.ReadDir(util.ThemesPath)
|
|
||||||
if err != nil {
|
|
||||||
logging.LogWarnf("read appearance themes folder failed: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
bazaarThemes := Themes()
|
|
||||||
|
|
||||||
for _, themeDir := range themeDirs {
|
|
||||||
if !util.IsDirRegularOrSymlink(themeDir) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
dirName := themeDir.Name()
|
|
||||||
if isBuiltInTheme(dirName) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
theme, parseErr := ThemeJSON(dirName)
|
|
||||||
if nil != parseErr || nil == theme {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
theme.RepoURL = theme.URL
|
|
||||||
theme.DisallowInstall = disallowInstallBazaarPackage(theme.Package)
|
|
||||||
if bazaarPkg := getBazaarTheme(theme.Name, bazaarThemes); nil != bazaarPkg {
|
|
||||||
theme.DisallowUpdate = disallowInstallBazaarPackage(bazaarPkg.Package)
|
|
||||||
theme.UpdateRequiredMinAppVer = bazaarPkg.MinAppVersion
|
|
||||||
theme.RepoURL = bazaarPkg.RepoURL
|
|
||||||
}
|
|
||||||
|
|
||||||
installPath := filepath.Join(util.ThemesPath, dirName)
|
|
||||||
theme.Installed = true
|
|
||||||
theme.PreviewURL = "/appearance/themes/" + dirName + "/preview.png"
|
|
||||||
theme.PreviewURLThumb = "/appearance/themes/" + dirName + "/preview.png"
|
|
||||||
theme.IconURL = "/appearance/themes/" + dirName + "/icon.png"
|
|
||||||
theme.PreferredFunding = getPreferredFunding(theme.Funding)
|
|
||||||
theme.PreferredName = GetPreferredName(theme.Package)
|
|
||||||
theme.PreferredDesc = getPreferredDesc(theme.Description)
|
|
||||||
info, statErr := os.Stat(filepath.Join(installPath, "theme.json"))
|
|
||||||
if nil != statErr {
|
|
||||||
logging.LogWarnf("stat install theme.json failed: %s", statErr)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
theme.HInstallDate = info.ModTime().Format("2006-01-02")
|
|
||||||
if installSize, ok := packageInstallSizeCache.Get(theme.RepoURL); ok {
|
|
||||||
theme.InstallSize = installSize.(int64)
|
|
||||||
} else {
|
|
||||||
is, _ := util.SizeOfDirectory(installPath)
|
|
||||||
theme.InstallSize = is
|
|
||||||
packageInstallSizeCache.SetDefault(theme.RepoURL, is)
|
|
||||||
}
|
|
||||||
theme.HInstallSize = humanize.BytesCustomCeil(uint64(theme.InstallSize), 2)
|
|
||||||
theme.PreferredReadme = getInstalledPackageREADME(installPath, "/appearance/themes/"+dirName+"/", theme.Readme)
|
|
||||||
theme.Outdated = isOutdatedTheme(theme, bazaarThemes)
|
|
||||||
ret = append(ret, theme)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func isBuiltInTheme(dirName string) bool {
|
|
||||||
return "daylight" == dirName || "midnight" == dirName
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBazaarTheme(name string, themes []*Theme) *Theme {
|
|
||||||
for _, p := range themes {
|
|
||||||
if p.Name == name {
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func InstallTheme(repoURL, repoHash, installPath string, systemID string) error {
|
|
||||||
repoURLHash := repoURL + "@" + repoHash
|
|
||||||
data, err := downloadPackage(repoURLHash, true, systemID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return installPackage(data, installPath, repoURLHash)
|
|
||||||
}
|
|
||||||
|
|
||||||
func UninstallTheme(installPath string) error {
|
|
||||||
return uninstallPackage(installPath)
|
|
||||||
}
|
|
||||||
|
|
@ -1,181 +0,0 @@
|
||||||
// SiYuan - Refactor your thinking
|
|
||||||
// Copyright (c) 2020-present, b3log.org
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package bazaar
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/88250/go-humanize"
|
|
||||||
"github.com/siyuan-note/logging"
|
|
||||||
"github.com/siyuan-note/siyuan/kernel/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Widget struct {
|
|
||||||
*Package
|
|
||||||
}
|
|
||||||
|
|
||||||
// Widgets 返回集市挂件列表
|
|
||||||
func Widgets() (widgets []*Widget) {
|
|
||||||
widgets = []*Widget{}
|
|
||||||
result := getStageAndBazaar("widgets")
|
|
||||||
|
|
||||||
if !result.Online {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if result.StageErr != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if 1 > len(result.BazaarIndex) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, repo := range result.StageIndex.Repos {
|
|
||||||
if nil == repo.Package {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
widget := buildWidgetFromStageRepo(repo, result.BazaarIndex)
|
|
||||||
if nil != widget {
|
|
||||||
widgets = append(widgets, widget)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(widgets, func(i, j int) bool { return widgets[i].Updated > widgets[j].Updated })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildWidgetFromStageRepo 使用 stage 内嵌的 package 构建 *Widget,不发起 HTTP 请求。
|
|
||||||
func buildWidgetFromStageRepo(repo *StageRepo, bazaarIndex map[string]*bazaarPackage) *Widget {
|
|
||||||
pkg := *repo.Package
|
|
||||||
pkg.URL = strings.TrimSuffix(pkg.URL, "/")
|
|
||||||
repoURLHash := strings.Split(repo.URL, "@")
|
|
||||||
if 2 != len(repoURLHash) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
pkg.RepoURL = "https://github.com/" + repoURLHash[0]
|
|
||||||
pkg.RepoHash = repoURLHash[1]
|
|
||||||
pkg.PreviewURL = util.BazaarOSSServer + "/package/" + repo.URL + "/preview.png?imageslim"
|
|
||||||
pkg.PreviewURLThumb = util.BazaarOSSServer + "/package/" + repo.URL + "/preview.png?imageView2/2/w/436/h/232"
|
|
||||||
pkg.IconURL = util.BazaarOSSServer + "/package/" + repo.URL + "/icon.png"
|
|
||||||
pkg.Updated = repo.Updated
|
|
||||||
pkg.Stars = repo.Stars
|
|
||||||
pkg.OpenIssues = repo.OpenIssues
|
|
||||||
pkg.Size = repo.Size
|
|
||||||
pkg.HSize = humanize.BytesCustomCeil(uint64(pkg.Size), 2)
|
|
||||||
pkg.InstallSize = repo.InstallSize
|
|
||||||
pkg.HInstallSize = humanize.BytesCustomCeil(uint64(pkg.InstallSize), 2)
|
|
||||||
pkg.HUpdated = formatUpdated(pkg.Updated)
|
|
||||||
pkg.PreferredFunding = getPreferredFunding(pkg.Funding)
|
|
||||||
pkg.PreferredName = GetPreferredName(&pkg)
|
|
||||||
pkg.PreferredDesc = getPreferredDesc(pkg.Description)
|
|
||||||
pkg.DisallowInstall = disallowInstallBazaarPackage(&pkg)
|
|
||||||
pkg.DisallowUpdate = disallowInstallBazaarPackage(&pkg)
|
|
||||||
pkg.UpdateRequiredMinAppVer = pkg.MinAppVersion
|
|
||||||
if bp := bazaarIndex[repoURLHash[0]]; nil != bp {
|
|
||||||
pkg.Downloads = bp.Downloads
|
|
||||||
}
|
|
||||||
packageInstallSizeCache.SetDefault(pkg.RepoURL, pkg.InstallSize)
|
|
||||||
return &Widget{Package: &pkg}
|
|
||||||
}
|
|
||||||
|
|
||||||
func InstalledWidgets() (ret []*Widget) {
|
|
||||||
ret = []*Widget{}
|
|
||||||
|
|
||||||
widgetsPath := filepath.Join(util.DataDir, "widgets")
|
|
||||||
if !util.IsPathRegularDirOrSymlinkDir(widgetsPath) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
widgetDirs, err := os.ReadDir(widgetsPath)
|
|
||||||
if err != nil {
|
|
||||||
logging.LogWarnf("read widgets folder failed: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
bazaarWidgets := Widgets()
|
|
||||||
|
|
||||||
for _, widgetDir := range widgetDirs {
|
|
||||||
if !util.IsDirRegularOrSymlink(widgetDir) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
dirName := widgetDir.Name()
|
|
||||||
|
|
||||||
widget, parseErr := WidgetJSON(dirName)
|
|
||||||
if nil != parseErr || nil == widget {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
widget.RepoURL = widget.URL
|
|
||||||
widget.DisallowInstall = disallowInstallBazaarPackage(widget.Package)
|
|
||||||
if bazaarPkg := getBazaarWidget(widget.Name, bazaarWidgets); nil != bazaarPkg {
|
|
||||||
widget.DisallowUpdate = disallowInstallBazaarPackage(bazaarPkg.Package)
|
|
||||||
widget.UpdateRequiredMinAppVer = bazaarPkg.MinAppVersion
|
|
||||||
widget.RepoURL = bazaarPkg.RepoURL
|
|
||||||
}
|
|
||||||
|
|
||||||
installPath := filepath.Join(util.DataDir, "widgets", dirName)
|
|
||||||
widget.Installed = true
|
|
||||||
widget.PreviewURL = "/widgets/" + dirName + "/preview.png"
|
|
||||||
widget.PreviewURLThumb = "/widgets/" + dirName + "/preview.png"
|
|
||||||
widget.IconURL = "/widgets/" + dirName + "/icon.png"
|
|
||||||
widget.PreferredFunding = getPreferredFunding(widget.Funding)
|
|
||||||
widget.PreferredName = GetPreferredName(widget.Package)
|
|
||||||
widget.PreferredDesc = getPreferredDesc(widget.Description)
|
|
||||||
info, statErr := os.Stat(filepath.Join(installPath, "widget.json"))
|
|
||||||
if nil != statErr {
|
|
||||||
logging.LogWarnf("stat install widget.json failed: %s", statErr)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
widget.HInstallDate = info.ModTime().Format("2006-01-02")
|
|
||||||
if installSize, ok := packageInstallSizeCache.Get(widget.RepoURL); ok {
|
|
||||||
widget.InstallSize = installSize.(int64)
|
|
||||||
} else {
|
|
||||||
is, _ := util.SizeOfDirectory(installPath)
|
|
||||||
widget.InstallSize = is
|
|
||||||
packageInstallSizeCache.SetDefault(widget.RepoURL, is)
|
|
||||||
}
|
|
||||||
widget.HInstallSize = humanize.BytesCustomCeil(uint64(widget.InstallSize), 2)
|
|
||||||
widget.PreferredReadme = getInstalledPackageREADME(installPath, "/widgets/"+dirName+"/", widget.Readme)
|
|
||||||
widget.Outdated = isOutdatedWidget(widget, bazaarWidgets)
|
|
||||||
ret = append(ret, widget)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBazaarWidget(name string, widgets []*Widget) *Widget {
|
|
||||||
for _, p := range widgets {
|
|
||||||
if p.Name == name {
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func InstallWidget(repoURL, repoHash, installPath string, systemID string) error {
|
|
||||||
repoURLHash := repoURL + "@" + repoHash
|
|
||||||
data, err := downloadPackage(repoURLHash, true, systemID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return installPackage(data, installPath, repoURLHash)
|
|
||||||
}
|
|
||||||
|
|
||||||
func UninstallWidget(installPath string) error {
|
|
||||||
return uninstallPackage(installPath)
|
|
||||||
}
|
|
||||||
|
|
@ -97,12 +97,16 @@ func loadThemes() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name := themeDir.Name()
|
name := themeDir.Name()
|
||||||
themeConf, parseErr := bazaar.ThemeJSON(name)
|
themeConf, parseErr := bazaar.ParsePackageJSON(filepath.Join(util.ThemesPath, name, "theme.json"))
|
||||||
if nil != parseErr || nil == themeConf {
|
if nil != parseErr || nil == themeConf {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, mode := range themeConf.Modes {
|
var modes []string
|
||||||
|
if nil != themeConf.Modes {
|
||||||
|
modes = *themeConf.Modes
|
||||||
|
}
|
||||||
|
for _, mode := range modes {
|
||||||
t := &conf.AppearanceTheme{Name: name}
|
t := &conf.AppearanceTheme{Name: name}
|
||||||
if isBuiltInTheme(name) {
|
if isBuiltInTheme(name) {
|
||||||
t.Label = name + Conf.Language(281)
|
t.Label = name + Conf.Language(281)
|
||||||
|
|
@ -174,7 +178,7 @@ func LoadIcons() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name := iconDir.Name()
|
name := iconDir.Name()
|
||||||
iconConf, err := bazaar.IconJSON(name)
|
iconConf, err := bazaar.ParsePackageJSON(filepath.Join(util.IconsPath, name, "icon.json"))
|
||||||
if err != nil || nil == iconConf {
|
if err != nil || nil == iconConf {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,8 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -33,10 +32,55 @@ import (
|
||||||
"github.com/siyuan-note/siyuan/kernel/task"
|
"github.com/siyuan-note/siyuan/kernel/task"
|
||||||
"github.com/siyuan-note/siyuan/kernel/util"
|
"github.com/siyuan-note/siyuan/kernel/util"
|
||||||
"golang.org/x/mod/semver"
|
"golang.org/x/mod/semver"
|
||||||
|
"golang.org/x/sync/singleflight"
|
||||||
)
|
)
|
||||||
|
|
||||||
func BatchUpdateBazaarPackages(frontend string) {
|
// installedPackageInfo 描述了本地集市包的包与目录名信息
|
||||||
plugins, widgets, icons, themes, templates := UpdatedPackages(frontend)
|
type installedPackageInfo struct {
|
||||||
|
Pkg *bazaar.Package
|
||||||
|
DirName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPackageInstallPath(pkgType, packageName string) (string, string, error) {
|
||||||
|
switch pkgType {
|
||||||
|
case "plugins":
|
||||||
|
return filepath.Join(util.DataDir, "plugins", packageName), "plugin.json", nil
|
||||||
|
case "themes":
|
||||||
|
return filepath.Join(util.ThemesPath, packageName), "theme.json", nil
|
||||||
|
case "icons":
|
||||||
|
return filepath.Join(util.IconsPath, packageName), "icon.json", nil
|
||||||
|
case "templates":
|
||||||
|
return filepath.Join(util.DataDir, "templates", packageName), "template.json", nil
|
||||||
|
case "widgets":
|
||||||
|
return filepath.Join(util.DataDir, "widgets", packageName), "widget.json", nil
|
||||||
|
default:
|
||||||
|
logging.LogErrorf("invalid package type: %s", pkgType)
|
||||||
|
return "", "", errors.New("invalid package type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// updatePackages 更新一组集市包
|
||||||
|
func updatePackages(packages []*bazaar.Package, pkgType string, count *int, total int) bool {
|
||||||
|
for _, pkg := range packages {
|
||||||
|
installPath, _, err := getPackageInstallPath(pkgType, pkg.Name)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
err = bazaar.InstallPackage(pkg.RepoURL, pkg.RepoHash, installPath, Conf.System.ID, pkgType, pkg.Name)
|
||||||
|
if err != nil {
|
||||||
|
logging.LogErrorf("update %s [%s] failed: %s", pkgType, pkg.Name, err)
|
||||||
|
util.PushErrMsg(fmt.Sprintf(Conf.language(238), pkg.Name), 5000)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
*count++
|
||||||
|
util.PushEndlessProgress(fmt.Sprintf(Conf.language(236), *count, total, pkg.Name))
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchUpdatePackages 更新所有集市包
|
||||||
|
func BatchUpdatePackages(frontend string) {
|
||||||
|
plugins, widgets, icons, themes, templates := GetUpdatedPackages(frontend)
|
||||||
|
|
||||||
total := len(plugins) + len(widgets) + len(icons) + len(themes) + len(templates)
|
total := len(plugins) + len(widgets) + len(icons) + len(themes) + len(templates)
|
||||||
if 1 > total {
|
if 1 > total {
|
||||||
|
|
@ -46,544 +90,334 @@ func BatchUpdateBazaarPackages(frontend string) {
|
||||||
util.PushEndlessProgress(fmt.Sprintf(Conf.language(235), 1, total))
|
util.PushEndlessProgress(fmt.Sprintf(Conf.language(235), 1, total))
|
||||||
defer util.PushClearProgress()
|
defer util.PushClearProgress()
|
||||||
count := 1
|
count := 1
|
||||||
for _, plugin := range plugins {
|
|
||||||
err := bazaar.InstallPlugin(plugin.RepoURL, plugin.RepoHash, filepath.Join(util.DataDir, "plugins", plugin.Name), Conf.System.ID)
|
|
||||||
if err != nil {
|
|
||||||
logging.LogErrorf("update plugin [%s] failed: %s", plugin.Name, err)
|
|
||||||
util.PushErrMsg(fmt.Sprintf(Conf.language(238), plugin.Name), 5000)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
count++
|
if !updatePackages(plugins, "plugins", &count, total) {
|
||||||
util.PushEndlessProgress(fmt.Sprintf(Conf.language(236), count, total, plugin.Name))
|
return
|
||||||
}
|
}
|
||||||
|
if !updatePackages(themes, "themes", &count, total) {
|
||||||
for _, widget := range widgets {
|
return
|
||||||
err := bazaar.InstallWidget(widget.RepoURL, widget.RepoHash, filepath.Join(util.DataDir, "widgets", widget.Name), Conf.System.ID)
|
|
||||||
if err != nil {
|
|
||||||
logging.LogErrorf("update widget [%s] failed: %s", widget.Name, err)
|
|
||||||
util.PushErrMsg(fmt.Sprintf(Conf.language(238), widget.Name), 5000)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
count++
|
|
||||||
util.PushEndlessProgress(fmt.Sprintf(Conf.language(236), count, total, widget.Name))
|
|
||||||
}
|
}
|
||||||
|
if !updatePackages(icons, "icons", &count, total) {
|
||||||
for _, icon := range icons {
|
return
|
||||||
err := bazaar.InstallIcon(icon.RepoURL, icon.RepoHash, filepath.Join(util.IconsPath, icon.Name), Conf.System.ID)
|
|
||||||
if err != nil {
|
|
||||||
logging.LogErrorf("update icon [%s] failed: %s", icon.Name, err)
|
|
||||||
util.PushErrMsg(fmt.Sprintf(Conf.language(238), icon.Name), 5000)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
count++
|
|
||||||
util.PushEndlessProgress(fmt.Sprintf(Conf.language(236), count, total, icon.Name))
|
|
||||||
}
|
}
|
||||||
|
if !updatePackages(templates, "templates", &count, total) {
|
||||||
for _, template := range templates {
|
return
|
||||||
err := bazaar.InstallTemplate(template.RepoURL, template.RepoHash, filepath.Join(util.DataDir, "templates", template.Name), Conf.System.ID)
|
|
||||||
if err != nil {
|
|
||||||
logging.LogErrorf("update template [%s] failed: %s", template.Name, err)
|
|
||||||
util.PushErrMsg(fmt.Sprintf(Conf.language(238), template.Name), 5000)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
count++
|
|
||||||
util.PushEndlessProgress(fmt.Sprintf(Conf.language(236), count, total, template.Name))
|
|
||||||
}
|
}
|
||||||
|
if !updatePackages(widgets, "widgets", &count, total) {
|
||||||
for _, theme := range themes {
|
return
|
||||||
err := bazaar.InstallTheme(theme.RepoURL, theme.RepoHash, filepath.Join(util.ThemesPath, theme.Name), Conf.System.ID)
|
|
||||||
if err != nil {
|
|
||||||
logging.LogErrorf("update theme [%s] failed: %s", theme.Name, err)
|
|
||||||
util.PushErrMsg(fmt.Sprintf(Conf.language(238), theme.Name), 5000)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
count++
|
|
||||||
util.PushEndlessProgress(fmt.Sprintf(Conf.language(236), count, total, theme.Name))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
util.ReloadUI()
|
util.ReloadUI()
|
||||||
task.AppendAsyncTaskWithDelay(task.PushMsg, 3*time.Second, util.PushMsg, fmt.Sprintf(Conf.language(237), total), 5000)
|
task.AppendAsyncTaskWithDelay(task.PushMsg, 3*time.Second, util.PushMsg, fmt.Sprintf(Conf.language(237), total), 5000)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdatedPackages(frontend string) (plugins []*bazaar.Plugin, widgets []*bazaar.Widget, icons []*bazaar.Icon, themes []*bazaar.Theme, templates []*bazaar.Template) {
|
// GetUpdatedPackages 获取所有类型集市包的更新列表
|
||||||
|
func GetUpdatedPackages(frontend string) (plugins, widgets, icons, themes, templates []*bazaar.Package) {
|
||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
wg.Add(5)
|
wg.Add(5)
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
tmp := InstalledPlugins(frontend, "")
|
|
||||||
for _, plugin := range tmp {
|
|
||||||
if plugin.Outdated {
|
|
||||||
plugins = append(plugins, plugin)
|
|
||||||
}
|
|
||||||
plugin.PreferredReadme = "" // 清空这个字段,前端会请求在线的 README
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
tmp := InstalledWidgets("")
|
plugins = getUpdatedPackages("plugins", frontend, "")
|
||||||
for _, widget := range tmp {
|
|
||||||
if widget.Outdated {
|
|
||||||
widgets = append(widgets, widget)
|
|
||||||
}
|
|
||||||
widget.PreferredReadme = ""
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
tmp := InstalledIcons("")
|
themes = getUpdatedPackages("themes", "", "")
|
||||||
for _, icon := range tmp {
|
|
||||||
if icon.Outdated {
|
|
||||||
icons = append(icons, icon)
|
|
||||||
}
|
|
||||||
icon.PreferredReadme = ""
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
tmp := InstalledThemes("")
|
icons = getUpdatedPackages("icons", "", "")
|
||||||
for _, theme := range tmp {
|
|
||||||
if theme.Outdated {
|
|
||||||
themes = append(themes, theme)
|
|
||||||
}
|
|
||||||
theme.PreferredReadme = ""
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
tmp := InstalledTemplates("")
|
templates = getUpdatedPackages("templates", "", "")
|
||||||
for _, template := range tmp {
|
}()
|
||||||
if template.Outdated {
|
go func() {
|
||||||
templates = append(templates, template)
|
defer wg.Done()
|
||||||
}
|
widgets = getUpdatedPackages("widgets", "", "")
|
||||||
template.PreferredReadme = ""
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if 1 > len(plugins) {
|
// getUpdatedPackages 获取单个类型集市包的更新列表
|
||||||
plugins = []*bazaar.Plugin{}
|
func getUpdatedPackages(pkgType, frontend, keyword string) (updatedPackages []*bazaar.Package) {
|
||||||
}
|
installedPackages := GetInstalledPackages(pkgType, frontend, keyword)
|
||||||
|
updatedPackages = []*bazaar.Package{} // 确保返回空切片而非 nil
|
||||||
if 1 > len(widgets) {
|
for _, pkg := range installedPackages {
|
||||||
widgets = []*bazaar.Widget{}
|
if !pkg.Outdated {
|
||||||
}
|
continue
|
||||||
|
}
|
||||||
if 1 > len(icons) {
|
updatedPackages = append(updatedPackages, pkg)
|
||||||
icons = []*bazaar.Icon{}
|
pkg.PreferredReadme = "" // 清空这个字段,前端会请求在线的 README
|
||||||
}
|
|
||||||
|
|
||||||
if 1 > len(themes) {
|
|
||||||
themes = []*bazaar.Theme{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if 1 > len(templates) {
|
|
||||||
templates = []*bazaar.Template{}
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBazaarPackageREADME 获取集市包的在线 README。
|
// GetInstalledPackageInfos 获取本地集市包信息,并返回路径相关字段供调用方复用
|
||||||
func GetBazaarPackageREADME(ctx context.Context, repoURL, repoHash, packageType string) (ret string) {
|
func GetInstalledPackageInfos(pkgType string) (installedPackageInfos []installedPackageInfo, basePath, baseURLPathPrefix string, err error) {
|
||||||
ret = bazaar.GetBazaarPackageREADME(ctx, repoURL, repoHash, packageType)
|
var jsonFileName string
|
||||||
return
|
switch pkgType {
|
||||||
}
|
case "plugins":
|
||||||
|
basePath, jsonFileName, baseURLPathPrefix = filepath.Join(util.DataDir, "plugins"), "plugin.json", "/plugins/"
|
||||||
func BazaarPlugins(frontend, keyword string) (plugins []*bazaar.Plugin) {
|
case "themes":
|
||||||
plugins = bazaar.Plugins(frontend)
|
basePath, jsonFileName, baseURLPathPrefix = util.ThemesPath, "theme.json", "/appearance/themes/"
|
||||||
plugins = filterPlugins(plugins, keyword)
|
case "icons":
|
||||||
for _, plugin := range plugins {
|
basePath, jsonFileName, baseURLPathPrefix = util.IconsPath, "icon.json", "/appearance/icons/"
|
||||||
plugin.Installed = util.IsPathRegularDirOrSymlinkDir(filepath.Join(util.DataDir, "plugins", plugin.Name))
|
case "templates":
|
||||||
if plugin.Installed {
|
basePath, jsonFileName, baseURLPathPrefix = filepath.Join(util.DataDir, "templates"), "template.json", "/templates/"
|
||||||
if pluginConf, err := bazaar.PluginJSON(plugin.Name); err == nil && nil != plugin {
|
case "widgets":
|
||||||
plugin.Outdated = 0 > semver.Compare("v"+pluginConf.Version, "v"+plugin.Version)
|
basePath, jsonFileName, baseURLPathPrefix = filepath.Join(util.DataDir, "widgets"), "widget.json", "/widgets/"
|
||||||
}
|
default:
|
||||||
} else {
|
logging.LogErrorf("invalid package type: %s", pkgType)
|
||||||
plugin.Outdated = false
|
err = errors.New("invalid package type")
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterPlugins(plugins []*bazaar.Plugin, keyword string) (ret []*bazaar.Plugin) {
|
|
||||||
keywords := getSearchKeywords(keyword)
|
|
||||||
if 0 == len(keywords) {
|
|
||||||
return plugins
|
|
||||||
}
|
|
||||||
ret = []*bazaar.Plugin{}
|
|
||||||
for _, plugin := range plugins {
|
|
||||||
if matchPackage(keywords, plugin.Package) {
|
|
||||||
ret = append(ret, plugin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func InstalledPlugins(frontend, keyword string) (plugins []*bazaar.Plugin) {
|
|
||||||
plugins = bazaar.InstalledPlugins(frontend)
|
|
||||||
plugins = filterPlugins(plugins, keyword)
|
|
||||||
petals := getPetals()
|
|
||||||
for _, plugin := range plugins {
|
|
||||||
petal := getPetalByName(plugin.Name, petals)
|
|
||||||
if nil != petal {
|
|
||||||
plugin.Enabled = petal.Enabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func InstallBazaarPlugin(repoURL, repoHash, pluginName string) error {
|
|
||||||
installPath := filepath.Join(util.DataDir, "plugins", pluginName)
|
|
||||||
err := bazaar.InstallPlugin(repoURL, repoHash, installPath, Conf.System.ID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New(fmt.Sprintf(Conf.Language(46), pluginName, err))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func UninstallBazaarPlugin(pluginName, frontend string) error {
|
|
||||||
installPath := filepath.Join(util.DataDir, "plugins", pluginName)
|
|
||||||
err := bazaar.UninstallPlugin(installPath)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New(fmt.Sprintf(Conf.Language(47), err.Error()))
|
|
||||||
}
|
|
||||||
|
|
||||||
petals := getPetals()
|
|
||||||
var tmp []*Petal
|
|
||||||
for i, petal := range petals {
|
|
||||||
if petal.Name != pluginName {
|
|
||||||
tmp = append(tmp, petals[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
petals = tmp
|
|
||||||
savePetals(petals)
|
|
||||||
|
|
||||||
uninstallPluginSet := hashset.New(pluginName)
|
|
||||||
PushReloadPlugin(uninstallPluginSet, nil, nil, nil, "")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func BazaarWidgets(keyword string) (widgets []*bazaar.Widget) {
|
|
||||||
widgets = bazaar.Widgets()
|
|
||||||
widgets = filterWidgets(widgets, keyword)
|
|
||||||
for _, widget := range widgets {
|
|
||||||
widget.Installed = util.IsPathRegularDirOrSymlinkDir(filepath.Join(util.DataDir, "widgets", widget.Name))
|
|
||||||
if widget.Installed {
|
|
||||||
if widgetConf, err := bazaar.WidgetJSON(widget.Name); err == nil && nil != widget {
|
|
||||||
widget.Outdated = 0 > semver.Compare("v"+widgetConf.Version, "v"+widget.Version)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
widget.Outdated = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterWidgets(widgets []*bazaar.Widget, keyword string) (ret []*bazaar.Widget) {
|
|
||||||
keywords := getSearchKeywords(keyword)
|
|
||||||
if 0 == len(keywords) {
|
|
||||||
return widgets
|
|
||||||
}
|
|
||||||
ret = []*bazaar.Widget{}
|
|
||||||
for _, w := range widgets {
|
|
||||||
if matchPackage(keywords, w.Package) {
|
|
||||||
ret = append(ret, w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func InstalledWidgets(keyword string) (widgets []*bazaar.Widget) {
|
|
||||||
widgets = bazaar.InstalledWidgets()
|
|
||||||
widgets = filterWidgets(widgets, keyword)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func InstallBazaarWidget(repoURL, repoHash, widgetName string) error {
|
|
||||||
installPath := filepath.Join(util.DataDir, "widgets", widgetName)
|
|
||||||
err := bazaar.InstallWidget(repoURL, repoHash, installPath, Conf.System.ID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New(fmt.Sprintf(Conf.Language(46), widgetName, err))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func UninstallBazaarWidget(widgetName string) error {
|
|
||||||
installPath := filepath.Join(util.DataDir, "widgets", widgetName)
|
|
||||||
err := bazaar.UninstallWidget(installPath)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New(fmt.Sprintf(Conf.Language(47), err.Error()))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func BazaarIcons(keyword string) (icons []*bazaar.Icon) {
|
|
||||||
icons = bazaar.Icons()
|
|
||||||
icons = filterIcons(icons, keyword)
|
|
||||||
for _, installed := range Conf.Appearance.Icons {
|
|
||||||
for _, icon := range icons {
|
|
||||||
if installed == icon.Name {
|
|
||||||
icon.Installed = true
|
|
||||||
if iconConf, err := bazaar.IconJSON(icon.Name); err == nil {
|
|
||||||
icon.Outdated = 0 > semver.Compare("v"+iconConf.Version, "v"+icon.Version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
icon.Current = icon.Name == Conf.Appearance.Icon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterIcons(icons []*bazaar.Icon, keyword string) (ret []*bazaar.Icon) {
|
|
||||||
keywords := getSearchKeywords(keyword)
|
|
||||||
if 0 == len(keywords) {
|
|
||||||
return icons
|
|
||||||
}
|
|
||||||
ret = []*bazaar.Icon{}
|
|
||||||
for _, i := range icons {
|
|
||||||
if matchPackage(keywords, i.Package) {
|
|
||||||
ret = append(ret, i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func InstalledIcons(keyword string) (icons []*bazaar.Icon) {
|
|
||||||
icons = bazaar.InstalledIcons()
|
|
||||||
icons = filterIcons(icons, keyword)
|
|
||||||
for _, icon := range icons {
|
|
||||||
icon.Current = icon.Name == Conf.Appearance.Icon
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func InstallBazaarIcon(repoURL, repoHash, iconName string) error {
|
|
||||||
installPath := filepath.Join(util.IconsPath, iconName)
|
|
||||||
err := bazaar.InstallIcon(repoURL, repoHash, installPath, Conf.System.ID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New(fmt.Sprintf(Conf.Language(46), iconName, err))
|
|
||||||
}
|
|
||||||
Conf.Appearance.Icon = iconName
|
|
||||||
Conf.Save()
|
|
||||||
InitAppearance()
|
|
||||||
util.BroadcastByType("main", "setAppearance", 0, "", Conf.Appearance)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func UninstallBazaarIcon(iconName string) error {
|
|
||||||
installPath := filepath.Join(util.IconsPath, iconName)
|
|
||||||
err := bazaar.UninstallIcon(installPath)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New(fmt.Sprintf(Conf.Language(47), err.Error()))
|
|
||||||
}
|
|
||||||
|
|
||||||
InitAppearance()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func BazaarThemes(keyword string) (ret []*bazaar.Theme) {
|
|
||||||
ret = bazaar.Themes()
|
|
||||||
ret = filterThemes(ret, keyword)
|
|
||||||
installs := Conf.Appearance.DarkThemes
|
|
||||||
installs = append(installs, Conf.Appearance.LightThemes...)
|
|
||||||
for _, installed := range installs {
|
|
||||||
for _, theme := range ret {
|
|
||||||
if installed.Name == theme.Name {
|
|
||||||
theme.Installed = true
|
|
||||||
if themeConf, err := bazaar.ThemeJSON(theme.Name); err == nil {
|
|
||||||
theme.Outdated = 0 > semver.Compare("v"+themeConf.Version, "v"+theme.Version)
|
|
||||||
}
|
|
||||||
theme.Current = theme.Name == Conf.Appearance.ThemeDark || theme.Name == Conf.Appearance.ThemeLight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterThemes(themes []*bazaar.Theme, keyword string) (ret []*bazaar.Theme) {
|
|
||||||
keywords := getSearchKeywords(keyword)
|
|
||||||
if 0 == len(keywords) {
|
|
||||||
return themes
|
|
||||||
}
|
|
||||||
ret = []*bazaar.Theme{}
|
|
||||||
for _, t := range themes {
|
|
||||||
if matchPackage(keywords, t.Package) {
|
|
||||||
ret = append(ret, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func InstalledThemes(keyword string) (ret []*bazaar.Theme) {
|
|
||||||
ret = bazaar.InstalledThemes()
|
|
||||||
ret = filterThemes(ret, keyword)
|
|
||||||
for _, theme := range ret {
|
|
||||||
theme.Current = theme.Name == Conf.Appearance.ThemeDark || theme.Name == Conf.Appearance.ThemeLight
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func InstallBazaarTheme(repoURL, repoHash, themeName string, mode int, update bool) error {
|
|
||||||
CloseWatchThemes()
|
|
||||||
|
|
||||||
installPath := filepath.Join(util.ThemesPath, themeName)
|
|
||||||
err := bazaar.InstallTheme(repoURL, repoHash, installPath, Conf.System.ID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New(fmt.Sprintf(Conf.Language(46), themeName, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !update {
|
|
||||||
// 更新主题后不需要对该主题进行切换 https://github.com/siyuan-note/siyuan/issues/4966
|
|
||||||
if 0 == mode {
|
|
||||||
Conf.Appearance.ThemeLight = themeName
|
|
||||||
} else {
|
|
||||||
Conf.Appearance.ThemeDark = themeName
|
|
||||||
}
|
|
||||||
Conf.Appearance.Mode = mode
|
|
||||||
Conf.Appearance.ThemeJS = gulu.File.IsExist(filepath.Join(installPath, "theme.js"))
|
|
||||||
Conf.Save()
|
|
||||||
}
|
|
||||||
|
|
||||||
InitAppearance()
|
|
||||||
util.BroadcastByType("main", "setAppearance", 0, "", Conf.Appearance)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func UninstallBazaarTheme(themeName string) error {
|
|
||||||
CloseWatchThemes()
|
|
||||||
|
|
||||||
installPath := filepath.Join(util.ThemesPath, themeName)
|
|
||||||
err := bazaar.UninstallTheme(installPath)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New(fmt.Sprintf(Conf.Language(47), err.Error()))
|
|
||||||
}
|
|
||||||
|
|
||||||
InitAppearance()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func BazaarTemplates(keyword string) (templates []*bazaar.Template) {
|
|
||||||
templates = bazaar.Templates()
|
|
||||||
templates = filterTemplates(templates, keyword)
|
|
||||||
for _, template := range templates {
|
|
||||||
template.Installed = util.IsPathRegularDirOrSymlinkDir(filepath.Join(util.DataDir, "templates", template.Name))
|
|
||||||
if template.Installed {
|
|
||||||
if templateConf, err := bazaar.TemplateJSON(template.Name); err == nil && nil != templateConf {
|
|
||||||
template.Outdated = 0 > semver.Compare("v"+templateConf.Version, "v"+template.Version)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
template.Outdated = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterTemplates(templates []*bazaar.Template, keyword string) (ret []*bazaar.Template) {
|
|
||||||
keywords := getSearchKeywords(keyword)
|
|
||||||
if 0 == len(keywords) {
|
|
||||||
return templates
|
|
||||||
}
|
|
||||||
ret = []*bazaar.Template{}
|
|
||||||
for _, t := range templates {
|
|
||||||
if matchPackage(keywords, t.Package) {
|
|
||||||
ret = append(ret, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func InstalledTemplates(keyword string) (templates []*bazaar.Template) {
|
|
||||||
templates = bazaar.InstalledTemplates()
|
|
||||||
templates = filterTemplates(templates, keyword)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func InstallBazaarTemplate(repoURL, repoHash, templateName string) error {
|
|
||||||
installPath := filepath.Join(util.DataDir, "templates", templateName)
|
|
||||||
err := bazaar.InstallTemplate(repoURL, repoHash, installPath, Conf.System.ID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New(fmt.Sprintf(Conf.Language(46), templateName, err))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func UninstallBazaarTemplate(templateName string) error {
|
|
||||||
installPath := filepath.Join(util.DataDir, "templates", templateName)
|
|
||||||
err := bazaar.UninstallTemplate(installPath)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New(fmt.Sprintf(Conf.Language(47), err.Error()))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func matchPackage(keywords []string, pkg *bazaar.Package) bool {
|
|
||||||
if 1 > len(keywords) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if nil == pkg {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, kw := range keywords {
|
|
||||||
if !packageContainsKeyword(pkg, kw) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 全部关键词匹配
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func packageContainsKeyword(pkg *bazaar.Package, kw string) bool {
|
|
||||||
if strings.Contains(strings.ToLower(path.Base(pkg.RepoURL)), kw) ||
|
|
||||||
strings.Contains(strings.ToLower(pkg.Author), kw) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
for _, s := range pkg.DisplayName {
|
|
||||||
if strings.Contains(strings.ToLower(s), kw) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, s := range pkg.Description {
|
|
||||||
if strings.Contains(strings.ToLower(s), kw) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, s := range pkg.Keywords {
|
|
||||||
if strings.Contains(strings.ToLower(s), kw) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSearchKeywords(query string) (ret []string) {
|
|
||||||
query = strings.TrimSpace(query)
|
|
||||||
if "" == query {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
keywords := strings.Split(query, " ")
|
dirs, err := bazaar.ReadInstalledPackageDirs(basePath)
|
||||||
for _, k := range keywords {
|
if err != nil {
|
||||||
if "" != k {
|
logging.LogWarnf("read %s folder failed: %s", pkgType, err)
|
||||||
ret = append(ret, strings.ToLower(k))
|
return
|
||||||
|
}
|
||||||
|
if len(dirs) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤内置包
|
||||||
|
switch pkgType {
|
||||||
|
case "themes":
|
||||||
|
filtered := make([]os.DirEntry, 0, len(dirs))
|
||||||
|
for _, d := range dirs {
|
||||||
|
if isBuiltInTheme(d.Name()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, d)
|
||||||
|
}
|
||||||
|
dirs = filtered
|
||||||
|
case "icons":
|
||||||
|
filtered := make([]os.DirEntry, 0, len(dirs))
|
||||||
|
for _, d := range dirs {
|
||||||
|
if isBuiltInIcon(d.Name()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, d)
|
||||||
|
}
|
||||||
|
dirs = filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dir := range dirs {
|
||||||
|
dirName := dir.Name()
|
||||||
|
pkg, parseErr := bazaar.ParsePackageJSON(filepath.Join(basePath, dirName, jsonFileName))
|
||||||
|
if nil != parseErr || nil == pkg {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
installedPackageInfos = append(installedPackageInfos, installedPackageInfo{Pkg: pkg, DirName: dirName})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var getInstalledPackagesFlight singleflight.Group
|
||||||
|
|
||||||
|
// GetInstalledPackages 获取本地集市包列表
|
||||||
|
func GetInstalledPackages(pkgType, frontend, keyword string) (installedPackages []*bazaar.Package) {
|
||||||
|
key := "getInstalledPackages:" + pkgType + ":" + frontend + ":" + keyword
|
||||||
|
v, err, _ := getInstalledPackagesFlight.Do(key, func() (interface{}, error) {
|
||||||
|
return getInstalledPackages0(pkgType, frontend, keyword), nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return []*bazaar.Package{}
|
||||||
|
}
|
||||||
|
return v.([]*bazaar.Package)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getInstalledPackages0(pkgType, frontend, keyword string) (installedPackages []*bazaar.Package) {
|
||||||
|
installedPackages = []*bazaar.Package{}
|
||||||
|
|
||||||
|
installedInfos, basePath, baseURLPathPrefix, err := GetInstalledPackageInfos(pkgType)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 本地没有该类型的集市包时,直接返回,避免请求云端数据
|
||||||
|
if len(installedInfos) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bazaarPackages := bazaar.GetBazaarPackages(pkgType, frontend)
|
||||||
|
bazaarPackagesMap := make(map[string]*bazaar.Package, len(bazaarPackages))
|
||||||
|
for _, pkg := range bazaarPackages {
|
||||||
|
if "" != pkg.Name {
|
||||||
|
bazaarPackagesMap[pkg.Name] = pkg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, info := range installedInfos {
|
||||||
|
pkg := info.Pkg
|
||||||
|
installPath := filepath.Join(basePath, info.DirName)
|
||||||
|
baseURLPath := baseURLPathPrefix + info.DirName + "/"
|
||||||
|
// 设置本地集市包的通用元数据
|
||||||
|
if !bazaar.SetInstalledPackageMetadata(pkg, installPath, baseURLPath, pkgType, bazaarPackagesMap) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
installedPackages = append(installedPackages, pkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
installedPackages = bazaar.FilterPackages(installedPackages, keyword)
|
||||||
|
|
||||||
|
// 设置本地集市包的额外元数据
|
||||||
|
var petals []*Petal
|
||||||
|
if pkgType == "plugins" {
|
||||||
|
petals = getPetals()
|
||||||
|
}
|
||||||
|
for _, pkg := range installedPackages {
|
||||||
|
switch pkgType {
|
||||||
|
case "plugins":
|
||||||
|
incompatible := bazaar.IsIncompatiblePlugin(pkg, frontend)
|
||||||
|
pkg.Incompatible = &incompatible
|
||||||
|
petal := getPetalByName(pkg.Name, petals)
|
||||||
|
if nil != petal {
|
||||||
|
enabled := petal.Enabled
|
||||||
|
pkg.Enabled = &enabled
|
||||||
|
}
|
||||||
|
case "themes":
|
||||||
|
pkg.Current = pkg.Name == Conf.Appearance.ThemeDark || pkg.Name == Conf.Appearance.ThemeLight
|
||||||
|
case "icons":
|
||||||
|
pkg.Current = pkg.Name == Conf.Appearance.Icon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetBazaarPackages 获取在线集市包列表
|
||||||
|
func GetBazaarPackages(pkgType, frontend, keyword string) (bazaarPackages []*bazaar.Package) {
|
||||||
|
bazaarPackages = bazaar.GetBazaarPackages(pkgType, frontend)
|
||||||
|
bazaarPackages = bazaar.FilterPackages(bazaarPackages, keyword)
|
||||||
|
installedInfos, _, _, err := GetInstalledPackageInfos(pkgType)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
installedMap := make(map[string]*bazaar.Package, len(installedInfos))
|
||||||
|
for _, info := range installedInfos {
|
||||||
|
installedMap[info.Pkg.Name] = info.Pkg
|
||||||
|
}
|
||||||
|
for _, pkg := range bazaarPackages {
|
||||||
|
installedPkg, ok := installedMap[pkg.Name]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pkg.Installed = true
|
||||||
|
pkg.Outdated = 0 > semver.Compare("v"+installedPkg.Version, "v"+pkg.Version)
|
||||||
|
switch pkgType {
|
||||||
|
case "themes":
|
||||||
|
pkg.Current = pkg.Name == Conf.Appearance.ThemeDark || pkg.Name == Conf.Appearance.ThemeLight
|
||||||
|
case "icons":
|
||||||
|
pkg.Current = pkg.Name == Conf.Appearance.Icon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetBazaarPackageREADME(ctx context.Context, repoURL, repoHash, pkgType string) (ret string) {
|
||||||
|
ret = bazaar.GetBazaarPackageREADME(ctx, repoURL, repoHash, pkgType)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func InstallBazaarPackage(pkgType, repoURL, repoHash, packageName string, themeMode int) error {
|
||||||
|
installPath, jsonFileName, err := getPackageInstallPath(pkgType, packageName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
installedPkg, parseErr := bazaar.ParsePackageJSON(filepath.Join(installPath, jsonFileName))
|
||||||
|
update := parseErr == nil && installedPkg != nil && installedPkg.Name == packageName
|
||||||
|
|
||||||
|
err = bazaar.InstallPackage(repoURL, repoHash, installPath, Conf.System.ID, pkgType, packageName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(Conf.Language(46), packageName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch pkgType {
|
||||||
|
case "plugins":
|
||||||
|
if update {
|
||||||
|
// 已启用的插件更新之后需要重载
|
||||||
|
petals := getPetals()
|
||||||
|
petal := getPetalByName(packageName, petals)
|
||||||
|
if nil != petal && petal.Enabled {
|
||||||
|
reloadPluginSet := hashset.New(packageName)
|
||||||
|
PushReloadPlugin(nil, nil, reloadPluginSet, nil, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "themes":
|
||||||
|
if !update {
|
||||||
|
// 更新主题后不需要切换到该主题 https://github.com/siyuan-note/siyuan/issues/4966
|
||||||
|
if 0 == themeMode {
|
||||||
|
Conf.Appearance.ThemeLight = packageName
|
||||||
|
} else {
|
||||||
|
Conf.Appearance.ThemeDark = packageName
|
||||||
|
}
|
||||||
|
Conf.Appearance.Mode = themeMode
|
||||||
|
Conf.Appearance.ThemeJS = gulu.File.IsExist(filepath.Join(util.ThemesPath, packageName, "theme.js"))
|
||||||
|
Conf.Save()
|
||||||
|
}
|
||||||
|
InitAppearance()
|
||||||
|
WatchThemes()
|
||||||
|
util.BroadcastByType("main", "setAppearance", 0, "", Conf.Appearance)
|
||||||
|
case "icons":
|
||||||
|
if !update {
|
||||||
|
// 更新图标后不需要切换到该图标
|
||||||
|
Conf.Appearance.Icon = packageName
|
||||||
|
Conf.Save()
|
||||||
|
}
|
||||||
|
InitAppearance()
|
||||||
|
util.BroadcastByType("main", "setAppearance", 0, "", Conf.Appearance)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UninstallPackage(pkgType, packageName string) error {
|
||||||
|
installPath, _, err := getPackageInstallPath(pkgType, packageName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bazaar.UninstallPackage(installPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(Conf.Language(47), err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除集市包的持久化信息
|
||||||
|
bazaar.RemovePackageInfo(pkgType, packageName)
|
||||||
|
|
||||||
|
switch pkgType {
|
||||||
|
case "plugins":
|
||||||
|
petals := getPetals()
|
||||||
|
var tmp []*Petal
|
||||||
|
for i, petal := range petals {
|
||||||
|
if petal.Name != packageName {
|
||||||
|
tmp = append(tmp, petals[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
petals = tmp
|
||||||
|
savePetals(petals)
|
||||||
|
|
||||||
|
uninstallPluginSet := hashset.New(packageName)
|
||||||
|
PushReloadPlugin(uninstallPluginSet, nil, nil, nil, "")
|
||||||
|
case "themes":
|
||||||
|
InitAppearance()
|
||||||
|
WatchThemes()
|
||||||
|
case "icons":
|
||||||
|
InitAppearance()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// isBuiltInTheme 通过包名或目录名判断是否为内置主题
|
// isBuiltInTheme 通过包名或目录名判断是否为内置主题
|
||||||
func isBuiltInTheme(name string) bool {
|
func isBuiltInTheme(name string) bool {
|
||||||
return "daylight" == name || "midnight" == name
|
return "daylight" == name || "midnight" == name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isBuiltInIcon 通过包名或目录名判断是否为内置图标
|
||||||
|
func isBuiltInIcon(name string) bool {
|
||||||
|
return "ant" == name || "material" == name
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,24 +34,24 @@ type WidgetSearchResult struct {
|
||||||
|
|
||||||
func SearchWidget(keyword string) (ret []*WidgetSearchResult) {
|
func SearchWidget(keyword string) (ret []*WidgetSearchResult) {
|
||||||
ret = []*WidgetSearchResult{}
|
ret = []*WidgetSearchResult{}
|
||||||
widgetsDir := filepath.Join(util.DataDir, "widgets")
|
widgetsDirPath := filepath.Join(util.DataDir, "widgets")
|
||||||
entries, err := os.ReadDir(widgetsDir)
|
widgetsDir, err := os.ReadDir(widgetsDirPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.LogErrorf("read dir [%s] failed: %s", widgetsDir, err)
|
logging.LogErrorf("read dir [%s] failed: %s", widgetsDirPath, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
k := strings.ToLower(keyword)
|
var widgets []*bazaar.Package
|
||||||
var widgets []*bazaar.Widget
|
for _, dir := range widgetsDir {
|
||||||
for _, entry := range entries {
|
if !util.IsDirRegularOrSymlink(dir) {
|
||||||
if !util.IsDirRegularOrSymlink(entry) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(entry.Name(), ".") {
|
dirName := dir.Name()
|
||||||
|
if strings.HasPrefix(dirName, ".") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
widget, _ := bazaar.WidgetJSON(entry.Name())
|
widget, _ := bazaar.ParsePackageJSON(filepath.Join(widgetsDirPath, dirName, "widget.json"))
|
||||||
if nil == widget {
|
if nil == widget {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -59,10 +59,10 @@ func SearchWidget(keyword string) (ret []*WidgetSearchResult) {
|
||||||
widgets = append(widgets, widget)
|
widgets = append(widgets, widget)
|
||||||
}
|
}
|
||||||
|
|
||||||
widgets = filterWidgets(widgets, k)
|
widgets = bazaar.FilterPackages(widgets, keyword)
|
||||||
for _, widget := range widgets {
|
for _, widget := range widgets {
|
||||||
b := &WidgetSearchResult{
|
b := &WidgetSearchResult{
|
||||||
Name: bazaar.GetPreferredName(widget.Package),
|
Name: bazaar.GetPreferredLocaleString(widget.DisplayName, widget.Name),
|
||||||
Content: widget.Name,
|
Content: widget.Name,
|
||||||
}
|
}
|
||||||
ret = append(ret, b)
|
ret = append(ret, b)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue