mirror of
https://github.com/siyuan-note/siyuan.git
synced 2026-03-07 21:22:34 +01:00
Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
commit
2d47cb3684
24 changed files with 1191 additions and 796 deletions
|
|
@ -771,7 +771,7 @@ type="checkbox">
|
|||
app.plugins.find((item: Plugin) => {
|
||||
if (item.name === dataObj.name) {
|
||||
reloadPlugin(app, {
|
||||
upsertCodePlugins: [dataObj.name],
|
||||
reloadPlugins: [dataObj.name],
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -537,14 +537,14 @@ export const setTitle = (title: string) => {
|
|||
};
|
||||
|
||||
export const downloadProgress = (data: { id: string, percent: number }) => {
|
||||
const bazzarSideElement = document.querySelector("#configBazaarReadme .item__side");
|
||||
if (!bazzarSideElement) {
|
||||
const bazaarSideElement = document.querySelector("#configBazaarReadme .item__side");
|
||||
if (!bazaarSideElement) {
|
||||
return;
|
||||
}
|
||||
if (data.id !== JSON.parse(bazzarSideElement.getAttribute("data-obj")).repoURL) {
|
||||
if (data.id !== JSON.parse(bazaarSideElement.getAttribute("data-obj")).repoURL) {
|
||||
return;
|
||||
}
|
||||
const btnElement = bazzarSideElement.querySelector('[data-type="install"]') as HTMLElement;
|
||||
const btnElement = bazaarSideElement.querySelector('[data-type="install"]') as HTMLElement;
|
||||
if (btnElement) {
|
||||
if (data.percent >= 1) {
|
||||
btnElement.parentElement.classList.add("fn__none");
|
||||
|
|
|
|||
|
|
@ -26,8 +26,12 @@ export const onMessage = (app: App, data: IWebSocketData) => {
|
|||
break;
|
||||
case "sendDeviceNotification":
|
||||
if (window.JSAndroid.sendNotification) {
|
||||
window.JSAndroid.sendNotification(data.data.title, data.data.body, data.data.delayInSeconds);
|
||||
window.JSAndroid.sendNotification(data.data.channel, data.data.title, data.data.body, data.data.delayInSeconds);
|
||||
}
|
||||
if (window.JSHarmony.sendNotification) {
|
||||
window.JSHarmony.sendNotification(data.data.channel, data.data.title, data.data.body, data.data.delayInSeconds);
|
||||
}
|
||||
|
||||
break;
|
||||
case "backgroundtask":
|
||||
if (!document.querySelector("#keyboardToolbar").classList.contains("fn__none") ||
|
||||
|
|
|
|||
|
|
@ -223,12 +223,12 @@ export const afterLoadPlugin = (plugin: Plugin) => {
|
|||
};
|
||||
|
||||
export const reloadPlugin = async (app: App, data: {
|
||||
upsertCodePlugins?: string[],
|
||||
upsertDataPlugins?: string[],
|
||||
unloadPlugins?: string[],
|
||||
uninstallPlugins?: string[],
|
||||
uninstallPlugins?: string[], // 插件卸载
|
||||
unloadPlugins?: string[], // 插件禁用
|
||||
reloadPlugins?: string[], // 插件启用,或插件代码变更
|
||||
dataChangePlugins?: string[], // 插件存储数据变更
|
||||
} = {}) => {
|
||||
const {upsertCodePlugins = [], upsertDataPlugins = [], unloadPlugins = [], uninstallPlugins = []} = data;
|
||||
const {uninstallPlugins = [], unloadPlugins = [], reloadPlugins = [], dataChangePlugins = []} = data;
|
||||
// 禁用
|
||||
unloadPlugins.forEach((item) => {
|
||||
uninstall(app, item, true);
|
||||
|
|
@ -237,12 +237,12 @@ export const reloadPlugin = async (app: App, data: {
|
|||
uninstallPlugins.forEach((item) => {
|
||||
uninstall(app, item, false);
|
||||
});
|
||||
upsertCodePlugins.forEach((item) => {
|
||||
reloadPlugins.forEach((item) => {
|
||||
uninstall(app, item, true);
|
||||
});
|
||||
loadPlugins(app, upsertCodePlugins, false).then(() => {
|
||||
loadPlugins(app, reloadPlugins, false).then(() => {
|
||||
app.plugins.forEach(item => {
|
||||
if (upsertCodePlugins.includes(item.name)) {
|
||||
if (reloadPlugins.includes(item.name)) {
|
||||
afterLoadPlugin(item);
|
||||
getAllEditor().forEach(editor => {
|
||||
editor.protyle.toolbar.update(editor.protyle);
|
||||
|
|
@ -251,7 +251,7 @@ export const reloadPlugin = async (app: App, data: {
|
|||
});
|
||||
});
|
||||
app.plugins.forEach(item => {
|
||||
if (upsertDataPlugins.includes(item.name)) {
|
||||
if (dataChangePlugins.includes(item.name)) {
|
||||
try {
|
||||
item.onDataChanged();
|
||||
} catch (e) {
|
||||
|
|
|
|||
3
app/src/types/index.d.ts
vendored
3
app/src/types/index.d.ts
vendored
|
|
@ -255,7 +255,7 @@ interface Window {
|
|||
getScreenWidthPx(): number
|
||||
exit(): void
|
||||
setWebViewFocusable(enable: boolean): void
|
||||
sendNotification(title: string, body: string, delayInSeconds: number): void
|
||||
sendNotification(channel: string, title: string, body: string, delayInSeconds: number): void
|
||||
};
|
||||
JSHarmony: {
|
||||
showKeyboard(): void
|
||||
|
|
@ -274,6 +274,7 @@ interface Window {
|
|||
getScreenWidthPx(): number
|
||||
exit(): void
|
||||
setWebViewFocusable(enable: boolean): void
|
||||
sendNotification(channel: string, title: string, body: string, delayInSeconds: number): void
|
||||
};
|
||||
|
||||
Protyle: import("../protyle/method").default;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,14 @@ import (
|
|||
"github.com/siyuan-note/siyuan/kernel/util"
|
||||
)
|
||||
|
||||
var validPackageTypes = map[string]bool{
|
||||
"plugins": true,
|
||||
"themes": true,
|
||||
"icons": true,
|
||||
"templates": true,
|
||||
"widgets": true,
|
||||
}
|
||||
|
||||
func batchUpdatePackage(c *gin.Context) {
|
||||
ret := gulu.Ret.NewResult()
|
||||
defer c.JSON(http.StatusOK, ret)
|
||||
|
|
@ -34,7 +42,10 @@ func batchUpdatePackage(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
frontend := arg["frontend"].(string)
|
||||
var frontend string
|
||||
if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("frontend", true, &frontend)) {
|
||||
return
|
||||
}
|
||||
model.BatchUpdateBazaarPackages(frontend)
|
||||
}
|
||||
|
||||
|
|
@ -47,7 +58,10 @@ func getUpdatedPackage(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
frontend := arg["frontend"].(string)
|
||||
var frontend string
|
||||
if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("frontend", true, &frontend)) {
|
||||
return
|
||||
}
|
||||
plugins, widgets, icons, themes, templates := model.UpdatedPackages(frontend)
|
||||
ret.Data = map[string]interface{}{
|
||||
"plugins": plugins,
|
||||
|
|
@ -67,11 +81,21 @@ func getBazaarPackageREADME(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
repoURL := arg["repoURL"].(string)
|
||||
repoHash := arg["repoHash"].(string)
|
||||
packageType := arg["packageType"].(string)
|
||||
var repoURL, repoHash, pkgType string
|
||||
if !util.ParseJsonArgs(arg, ret,
|
||||
util.BindJsonArg("repoURL", true, &repoURL),
|
||||
util.BindJsonArg("repoHash", true, &repoHash),
|
||||
util.BindJsonArg("packageType", true, &pkgType),
|
||||
) {
|
||||
return
|
||||
}
|
||||
if !validPackageTypes[pkgType] {
|
||||
ret.Code = -1
|
||||
ret.Msg = "Invalid package type"
|
||||
return
|
||||
}
|
||||
ret.Data = map[string]interface{}{
|
||||
"html": model.GetPackageREADME(repoURL, repoHash, packageType),
|
||||
"html": model.GetPackageREADME(repoURL, repoHash, pkgType),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -84,10 +108,12 @@ func getBazaarPlugin(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
frontend := arg["frontend"].(string)
|
||||
var keyword string
|
||||
if keywordArg := arg["keyword"]; nil != keywordArg {
|
||||
keyword = keywordArg.(string)
|
||||
var frontend, keyword string
|
||||
if !util.ParseJsonArgs(arg, ret,
|
||||
util.BindJsonArg("frontend", true, &frontend),
|
||||
util.BindJsonArg("keyword", false, &keyword),
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
ret.Data = map[string]interface{}{
|
||||
|
|
@ -104,10 +130,12 @@ func getInstalledPlugin(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
frontend := arg["frontend"].(string)
|
||||
var keyword string
|
||||
if keywordArg := arg["keyword"]; nil != keywordArg {
|
||||
keyword = keywordArg.(string)
|
||||
var frontend, keyword string
|
||||
if !util.ParseJsonArgs(arg, ret,
|
||||
util.BindJsonArg("frontend", true, &frontend),
|
||||
util.BindJsonArg("keyword", false, &keyword),
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
ret.Data = map[string]interface{}{
|
||||
|
|
@ -124,14 +152,16 @@ func installBazaarPlugin(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
var keyword string
|
||||
if keywordArg := arg["keyword"]; nil != keywordArg {
|
||||
keyword = keywordArg.(string)
|
||||
var frontend, keyword, repoURL, repoHash, packageName string
|
||||
if !util.ParseJsonArgs(arg, ret,
|
||||
util.BindJsonArg("frontend", true, &frontend),
|
||||
util.BindJsonArg("keyword", false, &keyword),
|
||||
util.BindJsonArg("repoURL", true, &repoURL),
|
||||
util.BindJsonArg("repoHash", true, &repoHash),
|
||||
util.BindJsonArg("packageName", true, &packageName),
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
repoURL := arg["repoURL"].(string)
|
||||
repoHash := arg["repoHash"].(string)
|
||||
packageName := arg["packageName"].(string)
|
||||
err := model.InstallBazaarPlugin(repoURL, repoHash, packageName)
|
||||
if err != nil {
|
||||
ret.Code = 1
|
||||
|
|
@ -139,8 +169,6 @@ func installBazaarPlugin(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
frontend := arg["frontend"].(string)
|
||||
|
||||
util.PushMsg(model.Conf.Language(69), 3000)
|
||||
ret.Data = map[string]interface{}{
|
||||
"packages": model.BazaarPlugins(frontend, keyword),
|
||||
|
|
@ -156,13 +184,14 @@ func uninstallBazaarPlugin(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
var keyword string
|
||||
if keywordArg := arg["keyword"]; nil != keywordArg {
|
||||
keyword = keywordArg.(string)
|
||||
var frontend, keyword, packageName string
|
||||
if !util.ParseJsonArgs(arg, ret,
|
||||
util.BindJsonArg("frontend", true, &frontend),
|
||||
util.BindJsonArg("keyword", false, &keyword),
|
||||
util.BindJsonArg("packageName", true, &packageName),
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
frontend := arg["frontend"].(string)
|
||||
packageName := arg["packageName"].(string)
|
||||
err := model.UninstallBazaarPlugin(packageName, frontend)
|
||||
if err != nil {
|
||||
ret.Code = -1
|
||||
|
|
@ -185,8 +214,8 @@ func getBazaarWidget(c *gin.Context) {
|
|||
}
|
||||
|
||||
var keyword string
|
||||
if keywordArg := arg["keyword"]; nil != keywordArg {
|
||||
keyword = keywordArg.(string)
|
||||
if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("keyword", false, &keyword)) {
|
||||
return
|
||||
}
|
||||
|
||||
ret.Data = map[string]interface{}{
|
||||
|
|
@ -204,8 +233,8 @@ func getInstalledWidget(c *gin.Context) {
|
|||
}
|
||||
|
||||
var keyword string
|
||||
if keywordArg := arg["keyword"]; nil != keywordArg {
|
||||
keyword = keywordArg.(string)
|
||||
if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("keyword", false, &keyword)) {
|
||||
return
|
||||
}
|
||||
|
||||
ret.Data = map[string]interface{}{
|
||||
|
|
@ -222,14 +251,15 @@ func installBazaarWidget(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
var keyword string
|
||||
if keywordArg := arg["keyword"]; nil != keywordArg {
|
||||
keyword = keywordArg.(string)
|
||||
var keyword, repoURL, repoHash, packageName string
|
||||
if !util.ParseJsonArgs(arg, ret,
|
||||
util.BindJsonArg("keyword", false, &keyword),
|
||||
util.BindJsonArg("repoURL", true, &repoURL),
|
||||
util.BindJsonArg("repoHash", true, &repoHash),
|
||||
util.BindJsonArg("packageName", true, &packageName),
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
repoURL := arg["repoURL"].(string)
|
||||
repoHash := arg["repoHash"].(string)
|
||||
packageName := arg["packageName"].(string)
|
||||
err := model.InstallBazaarWidget(repoURL, repoHash, packageName)
|
||||
if err != nil {
|
||||
ret.Code = 1
|
||||
|
|
@ -252,12 +282,13 @@ func uninstallBazaarWidget(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
var keyword string
|
||||
if keywordArg := arg["keyword"]; nil != keywordArg {
|
||||
keyword = keywordArg.(string)
|
||||
var keyword, packageName string
|
||||
if !util.ParseJsonArgs(arg, ret,
|
||||
util.BindJsonArg("keyword", false, &keyword),
|
||||
util.BindJsonArg("packageName", true, &packageName),
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
packageName := arg["packageName"].(string)
|
||||
err := model.UninstallBazaarWidget(packageName)
|
||||
if err != nil {
|
||||
ret.Code = -1
|
||||
|
|
@ -280,8 +311,8 @@ func getBazaarIcon(c *gin.Context) {
|
|||
}
|
||||
|
||||
var keyword string
|
||||
if keywordArg := arg["keyword"]; nil != keywordArg {
|
||||
keyword = keywordArg.(string)
|
||||
if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("keyword", false, &keyword)) {
|
||||
return
|
||||
}
|
||||
|
||||
ret.Data = map[string]interface{}{
|
||||
|
|
@ -299,8 +330,8 @@ func getInstalledIcon(c *gin.Context) {
|
|||
}
|
||||
|
||||
var keyword string
|
||||
if keywordArg := arg["keyword"]; nil != keywordArg {
|
||||
keyword = keywordArg.(string)
|
||||
if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("keyword", false, &keyword)) {
|
||||
return
|
||||
}
|
||||
|
||||
ret.Data = map[string]interface{}{
|
||||
|
|
@ -317,14 +348,15 @@ func installBazaarIcon(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
var keyword string
|
||||
if keywordArg := arg["keyword"]; nil != keywordArg {
|
||||
keyword = keywordArg.(string)
|
||||
var keyword, repoURL, repoHash, packageName string
|
||||
if !util.ParseJsonArgs(arg, ret,
|
||||
util.BindJsonArg("keyword", false, &keyword),
|
||||
util.BindJsonArg("repoURL", true, &repoURL),
|
||||
util.BindJsonArg("repoHash", true, &repoHash),
|
||||
util.BindJsonArg("packageName", true, &packageName),
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
repoURL := arg["repoURL"].(string)
|
||||
repoHash := arg["repoHash"].(string)
|
||||
packageName := arg["packageName"].(string)
|
||||
err := model.InstallBazaarIcon(repoURL, repoHash, packageName)
|
||||
if err != nil {
|
||||
ret.Code = 1
|
||||
|
|
@ -348,12 +380,13 @@ func uninstallBazaarIcon(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
var keyword string
|
||||
if keywordArg := arg["keyword"]; nil != keywordArg {
|
||||
keyword = keywordArg.(string)
|
||||
var keyword, packageName string
|
||||
if !util.ParseJsonArgs(arg, ret,
|
||||
util.BindJsonArg("keyword", false, &keyword),
|
||||
util.BindJsonArg("packageName", true, &packageName),
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
packageName := arg["packageName"].(string)
|
||||
err := model.UninstallBazaarIcon(packageName)
|
||||
if err != nil {
|
||||
ret.Code = -1
|
||||
|
|
@ -377,8 +410,8 @@ func getBazaarTemplate(c *gin.Context) {
|
|||
}
|
||||
|
||||
var keyword string
|
||||
if keywordArg := arg["keyword"]; nil != keywordArg {
|
||||
keyword = keywordArg.(string)
|
||||
if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("keyword", false, &keyword)) {
|
||||
return
|
||||
}
|
||||
|
||||
ret.Data = map[string]interface{}{
|
||||
|
|
@ -396,8 +429,8 @@ func getInstalledTemplate(c *gin.Context) {
|
|||
}
|
||||
|
||||
var keyword string
|
||||
if keywordArg := arg["keyword"]; nil != keywordArg {
|
||||
keyword = keywordArg.(string)
|
||||
if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("keyword", false, &keyword)) {
|
||||
return
|
||||
}
|
||||
|
||||
ret.Data = map[string]interface{}{
|
||||
|
|
@ -510,15 +543,17 @@ func installBazaarTheme(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
var keyword string
|
||||
if keywordArg := arg["keyword"]; nil != keywordArg {
|
||||
keyword = keywordArg.(string)
|
||||
var keyword, repoURL, repoHash, packageName string
|
||||
var mode float64
|
||||
if !util.ParseJsonArgs(arg, ret,
|
||||
util.BindJsonArg("keyword", false, &keyword),
|
||||
util.BindJsonArg("repoURL", true, &repoURL),
|
||||
util.BindJsonArg("repoHash", true, &repoHash),
|
||||
util.BindJsonArg("packageName", true, &packageName),
|
||||
util.BindJsonArg("mode", true, &mode),
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
repoURL := arg["repoURL"].(string)
|
||||
repoHash := arg["repoHash"].(string)
|
||||
packageName := arg["packageName"].(string)
|
||||
mode := arg["mode"].(float64)
|
||||
update := false
|
||||
if nil != arg["update"] {
|
||||
update = arg["update"].(bool)
|
||||
|
|
@ -530,6 +565,7 @@ func installBazaarTheme(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// TODO 安装新主题之后,不应该始终取消外观模式“跟随系统” https://github.com/siyuan-note/siyuan/issues/16990
|
||||
// 安装集市主题后不跟随系统切换外观模式
|
||||
model.Conf.Appearance.ModeOS = false
|
||||
model.Conf.Save()
|
||||
|
|
|
|||
|
|
@ -34,12 +34,19 @@ func sendDeviceNotification(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
if util.ContainerAndroid != util.Container {
|
||||
if util.ContainerAndroid != util.Container && util.ContainerHarmony != util.Container {
|
||||
ret.Code = -1
|
||||
ret.Msg = "Just support Android"
|
||||
ret.Msg = "Just support Android and HarmonyOS"
|
||||
return
|
||||
}
|
||||
|
||||
var channel string
|
||||
if nil != arg["channel"] {
|
||||
channel = strings.TrimSpace(arg["channel"].(string))
|
||||
} else {
|
||||
channel = "SiYuan Notifications"
|
||||
}
|
||||
|
||||
var title string
|
||||
if nil != arg["title"] {
|
||||
title = strings.TrimSpace(arg["title"].(string))
|
||||
|
|
@ -66,6 +73,7 @@ func sendDeviceNotification(c *gin.Context) {
|
|||
}
|
||||
|
||||
util.BroadcastByType("main", "sendDeviceNotification", 0, "", map[string]interface{}{
|
||||
"channel": channel,
|
||||
"title": title,
|
||||
"body": body,
|
||||
"delayInSeconds": delayInSeconds,
|
||||
|
|
|
|||
|
|
@ -67,10 +67,10 @@ func setPetalEnabled(c *gin.Context) {
|
|||
app = arg["app"].(string)
|
||||
}
|
||||
if enabled {
|
||||
upsertPluginCodeSet := hashset.New(packageName)
|
||||
model.PushReloadPlugin(upsertPluginCodeSet, nil, nil, nil, app)
|
||||
reloadPluginSet := hashset.New(packageName)
|
||||
model.PushReloadPlugin(nil, nil, reloadPluginSet, nil, app)
|
||||
} else {
|
||||
unloadPluginSet := hashset.New(packageName)
|
||||
model.PushReloadPlugin(nil, nil, unloadPluginSet, nil, app)
|
||||
model.PushReloadPlugin(nil, unloadPluginSet, nil, nil, app)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,5 +76,5 @@ func reloadIcon(c *gin.Context) {
|
|||
ret := gulu.Ret.NewResult()
|
||||
defer c.JSON(http.StatusOK, ret)
|
||||
|
||||
model.ReloadIcon()
|
||||
model.LoadIcons()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ package bazaar
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
|
@ -28,7 +27,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/88250/gulu"
|
||||
"github.com/88250/lute"
|
||||
"github.com/araddon/dateparse"
|
||||
"github.com/imroc/req/v3"
|
||||
gcache "github.com/patrickmn/go-cache"
|
||||
|
|
@ -37,9 +35,6 @@ import (
|
|||
"github.com/siyuan-note/logging"
|
||||
"github.com/siyuan-note/siyuan/kernel/util"
|
||||
"golang.org/x/mod/semver"
|
||||
"golang.org/x/sync/singleflight"
|
||||
textUnicode "golang.org/x/text/encoding/unicode"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
// LocaleStrings 表示按语种 key 的字符串表,key 为语种如 "default"、"en_US"、"zh_CN" 等
|
||||
|
|
@ -274,388 +269,6 @@ func ThemeJSON(themeDirName string) (ret *Theme, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
var cachedStageIndex = map[string]*StageIndex{}
|
||||
var stageIndexCacheTime int64
|
||||
var stageIndexLock = sync.RWMutex{}
|
||||
|
||||
type StageBazaarResult struct {
|
||||
StageIndex *StageIndex // stage 索引
|
||||
BazaarIndex map[string]*bazaarPackage // bazaar 索引
|
||||
Online bool // online 状态
|
||||
StageErr error // stage 错误
|
||||
}
|
||||
|
||||
var stageBazaarFlight singleflight.Group
|
||||
var onlineCheckFlight singleflight.Group
|
||||
|
||||
// getStageAndBazaar 获取 stage 索引和 bazaar 索引,相同 pkgType 的并发调用会合并为一次实际请求 (single-flight)
|
||||
func getStageAndBazaar(pkgType string) (result StageBazaarResult) {
|
||||
key := "stageBazaar:" + pkgType
|
||||
v, err, _ := stageBazaarFlight.Do(key, func() (interface{}, error) {
|
||||
return getStageAndBazaar0(pkgType), nil
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
result = v.(StageBazaarResult)
|
||||
return
|
||||
}
|
||||
|
||||
// getStageAndBazaar0 执行一次 stage 和 bazaar 索引拉取
|
||||
func getStageAndBazaar0(pkgType string) (result StageBazaarResult) {
|
||||
stageIndex, stageErr := getStageIndexFromCache(pkgType)
|
||||
bazaarIndex := getBazaarIndexFromCache()
|
||||
if nil != stageIndex && nil != bazaarIndex {
|
||||
// 两者都从缓存返回,不需要 online 检查
|
||||
return StageBazaarResult{
|
||||
StageIndex: stageIndex,
|
||||
BazaarIndex: bazaarIndex,
|
||||
Online: true,
|
||||
StageErr: stageErr,
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
var onlineResult bool
|
||||
onlineDone := make(chan bool, 1)
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(3)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
onlineResult = isBazzarOnline()
|
||||
onlineDone <- true
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
stageIndex, stageErr = getStageIndex(ctx, pkgType)
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
bazaarIndex = getBazaarIndex(ctx)
|
||||
}()
|
||||
|
||||
<-onlineDone
|
||||
if !onlineResult {
|
||||
// 不在线时立即取消其他请求并返回结果,避免等待 HTTP 请求超时
|
||||
cancel()
|
||||
return StageBazaarResult{
|
||||
StageIndex: stageIndex,
|
||||
BazaarIndex: bazaarIndex,
|
||||
Online: false,
|
||||
StageErr: stageErr,
|
||||
}
|
||||
}
|
||||
|
||||
// 在线时等待所有请求完成
|
||||
wg.Wait()
|
||||
|
||||
return StageBazaarResult{
|
||||
StageIndex: stageIndex,
|
||||
BazaarIndex: bazaarIndex,
|
||||
Online: onlineResult,
|
||||
StageErr: stageErr,
|
||||
}
|
||||
}
|
||||
|
||||
// getStageIndexFromCache 仅从缓存获取 stage 索引,过期或无缓存时返回 nil
|
||||
func getStageIndexFromCache(pkgType string) (ret *StageIndex, err error) {
|
||||
stageIndexLock.RLock()
|
||||
cacheTime := stageIndexCacheTime
|
||||
cached := cachedStageIndex[pkgType]
|
||||
stageIndexLock.RUnlock()
|
||||
if util.RhyCacheDuration >= time.Now().Unix()-cacheTime && nil != cached {
|
||||
ret = cached
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// getStageIndex 获取 stage 索引
|
||||
func getStageIndex(ctx context.Context, pkgType string) (ret *StageIndex, err error) {
|
||||
if cached, cacheErr := getStageIndexFromCache(pkgType); nil != cached {
|
||||
ret = cached
|
||||
err = cacheErr
|
||||
return
|
||||
}
|
||||
|
||||
var rhyRet map[string]interface{}
|
||||
rhyRet, err = util.GetRhyResult(ctx, false)
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
|
||||
stageIndexLock.Lock()
|
||||
defer stageIndexLock.Unlock()
|
||||
|
||||
bazaarHash := rhyRet["bazaar"].(string)
|
||||
ret = &StageIndex{}
|
||||
request := httpclient.NewBrowserRequest()
|
||||
u := util.BazaarOSSServer + "/bazaar@" + bazaarHash + "/stage/" + pkgType + ".json"
|
||||
resp, reqErr := request.SetContext(ctx).SetSuccessResult(ret).Get(u)
|
||||
if nil != reqErr {
|
||||
logging.LogErrorf("get community stage index [%s] failed: %s", u, reqErr)
|
||||
err = reqErr
|
||||
return
|
||||
}
|
||||
if 200 != resp.StatusCode {
|
||||
logging.LogErrorf("get community stage index [%s] failed: %d", u, resp.StatusCode)
|
||||
err = errors.New("get stage index failed")
|
||||
return
|
||||
}
|
||||
|
||||
stageIndexCacheTime = time.Now().Unix()
|
||||
cachedStageIndex[pkgType] = ret
|
||||
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 {
|
||||
v, err, _ := onlineCheckFlight.Do("bazaarOnline", func() (interface{}, error) {
|
||||
return isBazzarOnline0(), nil
|
||||
})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return v.(bool)
|
||||
}
|
||||
|
||||
func isBazzarOnline0() (ret bool) {
|
||||
// Improve marketplace loading when offline https://github.com/siyuan-note/siyuan/issues/12050
|
||||
ret = util.IsOnline(util.BazaarOSSServer+"/204", true, 3000)
|
||||
if !ret {
|
||||
util.PushErrMsg(util.Langs[util.Lang][24], 5000)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GetPackageREADME(repoURL, repoHash, packageType string) (ret string) {
|
||||
repoURLHash := repoURL + "@" + repoHash
|
||||
|
||||
stageIndexLock.RLock()
|
||||
stageIndex := cachedStageIndex[packageType]
|
||||
stageIndexLock.RUnlock()
|
||||
if nil == stageIndex {
|
||||
return
|
||||
}
|
||||
|
||||
url := strings.TrimPrefix(repoURLHash, "https://github.com/")
|
||||
var repo *StageRepo
|
||||
for _, r := range stageIndex.Repos {
|
||||
if r.URL == url {
|
||||
repo = r
|
||||
break
|
||||
}
|
||||
}
|
||||
if nil == repo || nil == repo.Package {
|
||||
return
|
||||
}
|
||||
|
||||
readme := getPreferredReadme(repo.Package.Readme)
|
||||
|
||||
data, err := downloadPackage(repoURLHash+"/"+readme, false, "")
|
||||
if err != nil {
|
||||
ret = fmt.Sprintf("Load bazaar package's preferred README(%s) failed: %s", readme, err.Error())
|
||||
// 回退到 Default README
|
||||
var defaultReadme string
|
||||
if len(repo.Package.Readme) > 0 {
|
||||
defaultReadme = repo.Package.Readme["default"]
|
||||
}
|
||||
if "" == strings.TrimSpace(defaultReadme) {
|
||||
defaultReadme = "README.md"
|
||||
}
|
||||
if readme != defaultReadme {
|
||||
data, err = downloadPackage(repoURLHash+"/"+defaultReadme, false, "")
|
||||
if err != nil {
|
||||
ret += fmt.Sprintf("<br>Load bazaar package's default README(%s) failed: %s", defaultReadme, err.Error())
|
||||
}
|
||||
}
|
||||
// 回退到 README.md
|
||||
if err != nil && readme != "README.md" && defaultReadme != "README.md" {
|
||||
data, err = downloadPackage(repoURLHash+"/README.md", false, "")
|
||||
if err != nil {
|
||||
ret += fmt.Sprintf("<br>Load bazaar package's README.md failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
} else if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if 2 < len(data) {
|
||||
if 255 == data[0] && 254 == data[1] {
|
||||
data, _, err = transform.Bytes(textUnicode.UTF16(textUnicode.LittleEndian, textUnicode.ExpectBOM).NewDecoder(), data)
|
||||
} else if 254 == data[0] && 255 == data[1] {
|
||||
data, _, err = transform.Bytes(textUnicode.UTF16(textUnicode.BigEndian, textUnicode.ExpectBOM).NewDecoder(), data)
|
||||
}
|
||||
}
|
||||
|
||||
ret, err = renderREADME(repoURL, data)
|
||||
return
|
||||
}
|
||||
|
||||
func loadInstalledReadme(installPath, basePath string, readme LocaleStrings) (ret string) {
|
||||
readmeFilename := getPreferredReadme(readme)
|
||||
readmeData, readErr := os.ReadFile(filepath.Join(installPath, readmeFilename))
|
||||
if nil == readErr {
|
||||
ret, _ = renderLocalREADME(basePath, readmeData)
|
||||
return
|
||||
}
|
||||
|
||||
logging.LogWarnf("read installed %s failed: %s", readmeFilename, readErr)
|
||||
ret = fmt.Sprintf("File %s not found", readmeFilename)
|
||||
// 回退到 Default README
|
||||
var defaultReadme string
|
||||
if len(readme) > 0 {
|
||||
defaultReadme = strings.TrimSpace(readme["default"])
|
||||
}
|
||||
if "" == defaultReadme {
|
||||
defaultReadme = "README.md"
|
||||
}
|
||||
if readmeFilename != defaultReadme {
|
||||
readmeData, readErr = os.ReadFile(filepath.Join(installPath, defaultReadme))
|
||||
if nil == readErr {
|
||||
ret, _ = renderLocalREADME(basePath, readmeData)
|
||||
return
|
||||
}
|
||||
logging.LogWarnf("read installed %s failed: %s", defaultReadme, readErr)
|
||||
ret += fmt.Sprintf("<br>File %s not found", defaultReadme)
|
||||
}
|
||||
// 回退到 README.md
|
||||
if nil != readErr && readmeFilename != "README.md" && defaultReadme != "README.md" {
|
||||
readmeData, readErr = os.ReadFile(filepath.Join(installPath, "README.md"))
|
||||
if nil == readErr {
|
||||
ret, _ = renderLocalREADME(basePath, readmeData)
|
||||
return
|
||||
}
|
||||
logging.LogWarnf("read installed README.md failed: %s", readErr)
|
||||
ret += "<br>File README.md not found"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func renderREADME(repoURL string, mdData []byte) (ret string, err error) {
|
||||
mdData = bytes.TrimPrefix(mdData, []byte("\xef\xbb\xbf"))
|
||||
luteEngine := lute.New()
|
||||
luteEngine.SetSoftBreak2HardBreak(false)
|
||||
luteEngine.SetCodeSyntaxHighlight(false)
|
||||
linkBase := "https://cdn.jsdelivr.net/gh/" + strings.TrimPrefix(repoURL, "https://github.com/")
|
||||
luteEngine.SetLinkBase(linkBase)
|
||||
ret = luteEngine.Md2HTML(string(mdData))
|
||||
ret = util.LinkTarget(ret, linkBase)
|
||||
return
|
||||
}
|
||||
|
||||
func renderLocalREADME(basePath string, mdData []byte) (ret string, err error) {
|
||||
mdData = bytes.TrimPrefix(mdData, []byte("\xef\xbb\xbf"))
|
||||
luteEngine := lute.New()
|
||||
luteEngine.SetSoftBreak2HardBreak(false)
|
||||
luteEngine.SetCodeSyntaxHighlight(false)
|
||||
linkBase := basePath
|
||||
luteEngine.SetLinkBase(linkBase)
|
||||
ret = luteEngine.Md2HTML(string(mdData))
|
||||
ret = util.LinkTarget(ret, linkBase)
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
packageLocks = map[string]*sync.Mutex{}
|
||||
packageLocksLock = sync.Mutex{}
|
||||
|
|
@ -779,54 +392,6 @@ func formatUpdated(updated string) (ret string) {
|
|||
return
|
||||
}
|
||||
|
||||
type bazaarPackage struct {
|
||||
Name string `json:"name"`
|
||||
Downloads int `json:"downloads"`
|
||||
}
|
||||
|
||||
var cachedBazaarIndex = map[string]*bazaarPackage{}
|
||||
var bazaarIndexCacheTime int64
|
||||
var bazaarIndexLock = sync.RWMutex{}
|
||||
|
||||
// getBazaarIndexFromCache 仅从缓存获取 bazaar 索引,过期或无缓存时返回 nil
|
||||
func getBazaarIndexFromCache() (ret map[string]*bazaarPackage) {
|
||||
bazaarIndexLock.RLock()
|
||||
cacheTime := bazaarIndexCacheTime
|
||||
cached := cachedBazaarIndex
|
||||
hasData := 0 < len(cached)
|
||||
bazaarIndexLock.RUnlock()
|
||||
if util.RhyCacheDuration >= time.Now().Unix()-cacheTime && hasData {
|
||||
ret = cached
|
||||
} else {
|
||||
ret = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// getBazaarIndex 获取 bazaar 索引
|
||||
func getBazaarIndex(ctx context.Context) map[string]*bazaarPackage {
|
||||
if cached := getBazaarIndexFromCache(); nil != cached {
|
||||
return cached
|
||||
}
|
||||
|
||||
bazaarIndexLock.Lock()
|
||||
defer bazaarIndexLock.Unlock()
|
||||
|
||||
request := httpclient.NewBrowserRequest()
|
||||
u := util.BazaarStatServer + "/bazaar/index.json"
|
||||
resp, reqErr := request.SetContext(ctx).SetSuccessResult(&cachedBazaarIndex).Get(u)
|
||||
if nil != reqErr {
|
||||
logging.LogErrorf("get bazaar index [%s] failed: %s", u, reqErr)
|
||||
return cachedBazaarIndex
|
||||
}
|
||||
if 200 != resp.StatusCode {
|
||||
logging.LogErrorf("get bazaar index [%s] failed: %d", u, resp.StatusCode)
|
||||
return cachedBazaarIndex
|
||||
}
|
||||
bazaarIndexCacheTime = time.Now().Unix()
|
||||
return cachedBazaarIndex
|
||||
}
|
||||
|
||||
// Add marketplace package config item `minAppVersion` https://github.com/siyuan-note/siyuan/issues/8330
|
||||
func disallowInstallBazaarPackage(pkg *Package) bool {
|
||||
// 如果包没有指定 minAppVersion,则允许安装
|
||||
|
|
|
|||
160
kernel/bazaar/readme.go
Normal file
160
kernel/bazaar/readme.go
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
// 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"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/88250/lute"
|
||||
"github.com/siyuan-note/logging"
|
||||
"github.com/siyuan-note/siyuan/kernel/util"
|
||||
textUnicode "golang.org/x/text/encoding/unicode"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
func GetPackageREADME(repoURL, repoHash, packageType string) (ret string) {
|
||||
repoURLHash := repoURL + "@" + repoHash
|
||||
|
||||
stageIndexLock.RLock()
|
||||
stageIndex := cachedStageIndex[packageType]
|
||||
stageIndexLock.RUnlock()
|
||||
if nil == stageIndex {
|
||||
return
|
||||
}
|
||||
|
||||
url := strings.TrimPrefix(repoURLHash, "https://github.com/")
|
||||
var repo *StageRepo
|
||||
for _, r := range stageIndex.Repos {
|
||||
if r.URL == url {
|
||||
repo = r
|
||||
break
|
||||
}
|
||||
}
|
||||
if nil == repo || nil == repo.Package {
|
||||
return
|
||||
}
|
||||
|
||||
readme := getPreferredReadme(repo.Package.Readme)
|
||||
|
||||
data, err := downloadPackage(repoURLHash+"/"+readme, false, "")
|
||||
if err != nil {
|
||||
ret = fmt.Sprintf("Load bazaar package's preferred README(%s) failed: %s", readme, err.Error())
|
||||
// 回退到 Default README
|
||||
var defaultReadme string
|
||||
if len(repo.Package.Readme) > 0 {
|
||||
defaultReadme = repo.Package.Readme["default"]
|
||||
}
|
||||
if "" == strings.TrimSpace(defaultReadme) {
|
||||
defaultReadme = "README.md"
|
||||
}
|
||||
if readme != defaultReadme {
|
||||
data, err = downloadPackage(repoURLHash+"/"+defaultReadme, false, "")
|
||||
if err != nil {
|
||||
ret += fmt.Sprintf("<br>Load bazaar package's default README(%s) failed: %s", defaultReadme, err.Error())
|
||||
}
|
||||
}
|
||||
// 回退到 README.md
|
||||
if err != nil && readme != "README.md" && defaultReadme != "README.md" {
|
||||
data, err = downloadPackage(repoURLHash+"/README.md", false, "")
|
||||
if err != nil {
|
||||
ret += fmt.Sprintf("<br>Load bazaar package's README.md failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
} else if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if 2 < len(data) {
|
||||
if 255 == data[0] && 254 == data[1] {
|
||||
data, _, err = transform.Bytes(textUnicode.UTF16(textUnicode.LittleEndian, textUnicode.ExpectBOM).NewDecoder(), data)
|
||||
} else if 254 == data[0] && 255 == data[1] {
|
||||
data, _, err = transform.Bytes(textUnicode.UTF16(textUnicode.BigEndian, textUnicode.ExpectBOM).NewDecoder(), data)
|
||||
}
|
||||
}
|
||||
|
||||
ret, err = renderREADME(repoURL, data)
|
||||
return
|
||||
}
|
||||
|
||||
func loadInstalledReadme(installPath, basePath string, readme LocaleStrings) (ret string) {
|
||||
readmeFilename := getPreferredReadme(readme)
|
||||
readmeData, readErr := os.ReadFile(filepath.Join(installPath, readmeFilename))
|
||||
if nil == readErr {
|
||||
ret, _ = renderLocalREADME(basePath, readmeData)
|
||||
return
|
||||
}
|
||||
|
||||
logging.LogWarnf("read installed %s failed: %s", readmeFilename, readErr)
|
||||
ret = fmt.Sprintf("File %s not found", readmeFilename)
|
||||
// 回退到 Default README
|
||||
var defaultReadme string
|
||||
if len(readme) > 0 {
|
||||
defaultReadme = strings.TrimSpace(readme["default"])
|
||||
}
|
||||
if "" == defaultReadme {
|
||||
defaultReadme = "README.md"
|
||||
}
|
||||
if readmeFilename != defaultReadme {
|
||||
readmeData, readErr = os.ReadFile(filepath.Join(installPath, defaultReadme))
|
||||
if nil == readErr {
|
||||
ret, _ = renderLocalREADME(basePath, readmeData)
|
||||
return
|
||||
}
|
||||
logging.LogWarnf("read installed %s failed: %s", defaultReadme, readErr)
|
||||
ret += fmt.Sprintf("<br>File %s not found", defaultReadme)
|
||||
}
|
||||
// 回退到 README.md
|
||||
if nil != readErr && readmeFilename != "README.md" && defaultReadme != "README.md" {
|
||||
readmeData, readErr = os.ReadFile(filepath.Join(installPath, "README.md"))
|
||||
if nil == readErr {
|
||||
ret, _ = renderLocalREADME(basePath, readmeData)
|
||||
return
|
||||
}
|
||||
logging.LogWarnf("read installed README.md failed: %s", readErr)
|
||||
ret += "<br>File README.md not found"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func renderREADME(repoURL string, mdData []byte) (ret string, err error) {
|
||||
mdData = bytes.TrimPrefix(mdData, []byte("\xef\xbb\xbf"))
|
||||
luteEngine := lute.New()
|
||||
luteEngine.SetSoftBreak2HardBreak(false)
|
||||
luteEngine.SetCodeSyntaxHighlight(false)
|
||||
linkBase := "https://cdn.jsdelivr.net/gh/" + strings.TrimPrefix(repoURL, "https://github.com/")
|
||||
luteEngine.SetLinkBase(linkBase)
|
||||
ret = luteEngine.Md2HTML(string(mdData))
|
||||
ret = util.LinkTarget(ret, linkBase)
|
||||
return
|
||||
}
|
||||
|
||||
func renderLocalREADME(basePath string, mdData []byte) (ret string, err error) {
|
||||
mdData = bytes.TrimPrefix(mdData, []byte("\xef\xbb\xbf"))
|
||||
luteEngine := lute.New()
|
||||
luteEngine.SetSoftBreak2HardBreak(false)
|
||||
luteEngine.SetCodeSyntaxHighlight(false)
|
||||
linkBase := basePath
|
||||
luteEngine.SetLinkBase(linkBase)
|
||||
ret = luteEngine.Md2HTML(string(mdData))
|
||||
ret = util.LinkTarget(ret, linkBase)
|
||||
return
|
||||
}
|
||||
332
kernel/bazaar/stage.go
Normal file
332
kernel/bazaar/stage.go
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
// 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 (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/siyuan-note/httpclient"
|
||||
"github.com/siyuan-note/logging"
|
||||
"github.com/siyuan-note/siyuan/kernel/util"
|
||||
"golang.org/x/mod/semver"
|
||||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
|
||||
var cachedStageIndex = map[string]*StageIndex{}
|
||||
var stageIndexCacheTime int64
|
||||
var stageIndexLock = sync.RWMutex{}
|
||||
|
||||
type StageBazaarResult struct {
|
||||
StageIndex *StageIndex // stage 索引
|
||||
BazaarIndex map[string]*bazaarPackage // bazaar 索引
|
||||
Online bool // online 状态
|
||||
StageErr error // stage 错误
|
||||
}
|
||||
|
||||
var stageBazaarFlight singleflight.Group
|
||||
var onlineCheckFlight singleflight.Group
|
||||
|
||||
// getStageAndBazaar 获取 stage 索引和 bazaar 索引,相同 pkgType 的并发调用会合并为一次实际请求 (single-flight)
|
||||
func getStageAndBazaar(pkgType string) (result StageBazaarResult) {
|
||||
key := "stageBazaar:" + pkgType
|
||||
v, err, _ := stageBazaarFlight.Do(key, func() (interface{}, error) {
|
||||
return getStageAndBazaar0(pkgType), nil
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
result = v.(StageBazaarResult)
|
||||
return
|
||||
}
|
||||
|
||||
// getStageAndBazaar0 执行一次 stage 和 bazaar 索引拉取
|
||||
func getStageAndBazaar0(pkgType string) (result StageBazaarResult) {
|
||||
stageIndex, stageErr := getStageIndexFromCache(pkgType)
|
||||
bazaarIndex := getBazaarIndexFromCache()
|
||||
if nil != stageIndex && nil != bazaarIndex {
|
||||
// 两者都从缓存返回,不需要 online 检查
|
||||
return StageBazaarResult{
|
||||
StageIndex: stageIndex,
|
||||
BazaarIndex: bazaarIndex,
|
||||
Online: true,
|
||||
StageErr: stageErr,
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
var onlineResult bool
|
||||
onlineDone := make(chan bool, 1)
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(3)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
onlineResult = isBazzarOnline()
|
||||
onlineDone <- true
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
stageIndex, stageErr = getStageIndex(ctx, pkgType)
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
bazaarIndex = getBazaarIndex(ctx)
|
||||
}()
|
||||
|
||||
<-onlineDone
|
||||
if !onlineResult {
|
||||
// 不在线时立即取消其他请求并返回结果,避免等待 HTTP 请求超时
|
||||
cancel()
|
||||
return StageBazaarResult{
|
||||
StageIndex: stageIndex,
|
||||
BazaarIndex: bazaarIndex,
|
||||
Online: false,
|
||||
StageErr: stageErr,
|
||||
}
|
||||
}
|
||||
|
||||
// 在线时等待所有请求完成
|
||||
wg.Wait()
|
||||
|
||||
return StageBazaarResult{
|
||||
StageIndex: stageIndex,
|
||||
BazaarIndex: bazaarIndex,
|
||||
Online: onlineResult,
|
||||
StageErr: stageErr,
|
||||
}
|
||||
}
|
||||
|
||||
// getStageIndexFromCache 仅从缓存获取 stage 索引,过期或无缓存时返回 nil
|
||||
func getStageIndexFromCache(pkgType string) (ret *StageIndex, err error) {
|
||||
stageIndexLock.RLock()
|
||||
cacheTime := stageIndexCacheTime
|
||||
cached := cachedStageIndex[pkgType]
|
||||
stageIndexLock.RUnlock()
|
||||
if util.RhyCacheDuration >= time.Now().Unix()-cacheTime && nil != cached {
|
||||
ret = cached
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// getStageIndex 获取 stage 索引
|
||||
func getStageIndex(ctx context.Context, pkgType string) (ret *StageIndex, err error) {
|
||||
if cached, cacheErr := getStageIndexFromCache(pkgType); nil != cached {
|
||||
ret = cached
|
||||
err = cacheErr
|
||||
return
|
||||
}
|
||||
|
||||
var rhyRet map[string]interface{}
|
||||
rhyRet, err = util.GetRhyResult(ctx, false)
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
|
||||
stageIndexLock.Lock()
|
||||
defer stageIndexLock.Unlock()
|
||||
|
||||
bazaarHash := rhyRet["bazaar"].(string)
|
||||
ret = &StageIndex{}
|
||||
request := httpclient.NewBrowserRequest()
|
||||
u := util.BazaarOSSServer + "/bazaar@" + bazaarHash + "/stage/" + pkgType + ".json"
|
||||
resp, reqErr := request.SetContext(ctx).SetSuccessResult(ret).Get(u)
|
||||
if nil != reqErr {
|
||||
logging.LogErrorf("get community stage index [%s] failed: %s", u, reqErr)
|
||||
err = reqErr
|
||||
return
|
||||
}
|
||||
if 200 != resp.StatusCode {
|
||||
logging.LogErrorf("get community stage index [%s] failed: %d", u, resp.StatusCode)
|
||||
err = errors.New("get stage index failed")
|
||||
return
|
||||
}
|
||||
|
||||
stageIndexCacheTime = time.Now().Unix()
|
||||
cachedStageIndex[pkgType] = ret
|
||||
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 {
|
||||
v, err, _ := onlineCheckFlight.Do("bazaarOnline", func() (interface{}, error) {
|
||||
return isBazzarOnline0(), nil
|
||||
})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return v.(bool)
|
||||
}
|
||||
|
||||
func isBazzarOnline0() (ret bool) {
|
||||
// Improve marketplace loading when offline https://github.com/siyuan-note/siyuan/issues/12050
|
||||
ret = util.IsOnline(util.BazaarOSSServer+"/204", true, 3000)
|
||||
if !ret {
|
||||
util.PushErrMsg(util.Langs[util.Lang][24], 5000)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type bazaarPackage struct {
|
||||
Name string `json:"name"`
|
||||
Downloads int `json:"downloads"`
|
||||
}
|
||||
|
||||
var cachedBazaarIndex = map[string]*bazaarPackage{}
|
||||
var bazaarIndexCacheTime int64
|
||||
var bazaarIndexLock = sync.RWMutex{}
|
||||
|
||||
// getBazaarIndexFromCache 仅从缓存获取 bazaar 索引,过期或无缓存时返回 nil
|
||||
func getBazaarIndexFromCache() (ret map[string]*bazaarPackage) {
|
||||
bazaarIndexLock.RLock()
|
||||
cacheTime := bazaarIndexCacheTime
|
||||
cached := cachedBazaarIndex
|
||||
hasData := 0 < len(cached)
|
||||
bazaarIndexLock.RUnlock()
|
||||
if util.RhyCacheDuration >= time.Now().Unix()-cacheTime && hasData {
|
||||
ret = cached
|
||||
} else {
|
||||
ret = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// getBazaarIndex 获取 bazaar 索引
|
||||
func getBazaarIndex(ctx context.Context) map[string]*bazaarPackage {
|
||||
if cached := getBazaarIndexFromCache(); nil != cached {
|
||||
return cached
|
||||
}
|
||||
|
||||
bazaarIndexLock.Lock()
|
||||
defer bazaarIndexLock.Unlock()
|
||||
|
||||
request := httpclient.NewBrowserRequest()
|
||||
u := util.BazaarStatServer + "/bazaar/index.json"
|
||||
resp, reqErr := request.SetContext(ctx).SetSuccessResult(&cachedBazaarIndex).Get(u)
|
||||
if nil != reqErr {
|
||||
logging.LogErrorf("get bazaar index [%s] failed: %s", u, reqErr)
|
||||
return cachedBazaarIndex
|
||||
}
|
||||
if 200 != resp.StatusCode {
|
||||
logging.LogErrorf("get bazaar index [%s] failed: %d", u, resp.StatusCode)
|
||||
return cachedBazaarIndex
|
||||
}
|
||||
bazaarIndexCacheTime = time.Now().Unix()
|
||||
return cachedBazaarIndex
|
||||
}
|
||||
|
|
@ -55,5 +55,6 @@ func main() {
|
|||
|
||||
model.WatchAssets()
|
||||
model.WatchEmojis()
|
||||
model.WatchThemes()
|
||||
model.HandleSignal()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,11 +21,9 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/88250/gulu"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/siyuan-note/filelock"
|
||||
"github.com/siyuan-note/logging"
|
||||
"github.com/siyuan-note/siyuan/kernel/bazaar"
|
||||
|
|
@ -41,15 +39,17 @@ func InitAppearance() {
|
|||
return
|
||||
}
|
||||
|
||||
unloadThemes()
|
||||
from := filepath.Join(util.WorkingDir, "appearance")
|
||||
if err := filelock.Copy(from, util.AppearancePath); err != nil {
|
||||
logging.LogErrorf("copy appearance resources from [%s] to [%s] failed: %s", from, util.AppearancePath, err)
|
||||
util.ReportFileSysFatalError(err)
|
||||
return
|
||||
}
|
||||
loadThemes()
|
||||
|
||||
loadThemes()
|
||||
LoadIcons()
|
||||
|
||||
Conf.m.Lock()
|
||||
if !containTheme(Conf.Appearance.ThemeDark, Conf.Appearance.DarkThemes) {
|
||||
Conf.Appearance.ThemeDark = "midnight"
|
||||
Conf.Appearance.ThemeJS = false
|
||||
|
|
@ -58,11 +58,10 @@ func InitAppearance() {
|
|||
Conf.Appearance.ThemeLight = "daylight"
|
||||
Conf.Appearance.ThemeJS = false
|
||||
}
|
||||
|
||||
loadIcons()
|
||||
if !gulu.Str.Contains(Conf.Appearance.Icon, Conf.Appearance.Icons) {
|
||||
Conf.Appearance.Icon = "material"
|
||||
}
|
||||
Conf.m.Unlock()
|
||||
|
||||
Conf.Save()
|
||||
|
||||
|
|
@ -78,36 +77,6 @@ func containTheme(name string, themes []*conf.AppearanceTheme) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
var themeWatchers = sync.Map{} // [string]*fsnotify.Watcher{}
|
||||
|
||||
func closeThemeWatchers() {
|
||||
themeWatchers.Range(func(key, value interface{}) bool {
|
||||
if err := value.(*fsnotify.Watcher).Close(); err != nil {
|
||||
logging.LogErrorf("close file watcher failed: %s", err)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func unloadThemes() {
|
||||
if !util.IsPathRegularDirOrSymlinkDir(util.ThemesPath) {
|
||||
return
|
||||
}
|
||||
|
||||
themeDirs, err := os.ReadDir(util.ThemesPath)
|
||||
if err != nil {
|
||||
logging.LogErrorf("read appearance themes folder failed: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, themeDir := range themeDirs {
|
||||
if !util.IsDirRegularOrSymlink(themeDir) {
|
||||
continue
|
||||
}
|
||||
unwatchTheme(filepath.Join(util.ThemesPath, themeDir.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
func loadThemes() {
|
||||
themeDirs, err := os.ReadDir(util.ThemesPath)
|
||||
if err != nil {
|
||||
|
|
@ -116,9 +85,13 @@ func loadThemes() {
|
|||
return
|
||||
}
|
||||
|
||||
Conf.Appearance.DarkThemes = nil
|
||||
Conf.Appearance.LightThemes = nil
|
||||
var darkThemes, lightThemes []*conf.AppearanceTheme
|
||||
var daylightTheme, midnightTheme *conf.AppearanceTheme
|
||||
var themeVer string
|
||||
var themeJS bool
|
||||
mode := Conf.Appearance.Mode
|
||||
themeLight := Conf.Appearance.ThemeLight
|
||||
themeDark := Conf.Appearance.ThemeDark
|
||||
for _, themeDir := range themeDirs {
|
||||
if !util.IsDirRegularOrSymlink(themeDir) {
|
||||
continue
|
||||
|
|
@ -129,10 +102,9 @@ func loadThemes() {
|
|||
continue
|
||||
}
|
||||
|
||||
modes := themeConf.Modes
|
||||
for _, mode := range modes {
|
||||
for _, mode := range themeConf.Modes {
|
||||
t := &conf.AppearanceTheme{Name: name}
|
||||
if "midnight" == name || "daylight" == name {
|
||||
if isBuiltInTheme(name) {
|
||||
t.Label = name + Conf.Language(281)
|
||||
} else {
|
||||
t.Label = name
|
||||
|
|
@ -156,32 +128,37 @@ func loadThemes() {
|
|||
}
|
||||
|
||||
if "dark" == mode {
|
||||
Conf.Appearance.DarkThemes = append(Conf.Appearance.DarkThemes, t)
|
||||
darkThemes = append(darkThemes, t)
|
||||
} else if "light" == mode {
|
||||
Conf.Appearance.LightThemes = append(Conf.Appearance.LightThemes, t)
|
||||
lightThemes = append(lightThemes, t)
|
||||
}
|
||||
}
|
||||
|
||||
if 0 == Conf.Appearance.Mode {
|
||||
if Conf.Appearance.ThemeLight == name {
|
||||
Conf.Appearance.ThemeVer = themeConf.Version
|
||||
Conf.Appearance.ThemeJS = gulu.File.IsExist(filepath.Join(util.ThemesPath, name, "theme.js"))
|
||||
if 0 == mode {
|
||||
if themeLight == name {
|
||||
themeVer = themeConf.Version
|
||||
themeJS = gulu.File.IsExist(filepath.Join(util.ThemesPath, name, "theme.js"))
|
||||
}
|
||||
} else {
|
||||
if Conf.Appearance.ThemeDark == name {
|
||||
Conf.Appearance.ThemeVer = themeConf.Version
|
||||
Conf.Appearance.ThemeJS = gulu.File.IsExist(filepath.Join(util.ThemesPath, name, "theme.js"))
|
||||
if themeDark == name {
|
||||
themeVer = themeConf.Version
|
||||
themeJS = gulu.File.IsExist(filepath.Join(util.ThemesPath, name, "theme.js"))
|
||||
}
|
||||
}
|
||||
|
||||
go watchTheme(filepath.Join(util.ThemesPath, name))
|
||||
}
|
||||
|
||||
Conf.Appearance.LightThemes = append([]*conf.AppearanceTheme{daylightTheme}, Conf.Appearance.LightThemes...)
|
||||
Conf.Appearance.DarkThemes = append([]*conf.AppearanceTheme{midnightTheme}, Conf.Appearance.DarkThemes...)
|
||||
lightThemes = append([]*conf.AppearanceTheme{daylightTheme}, lightThemes...)
|
||||
darkThemes = append([]*conf.AppearanceTheme{midnightTheme}, darkThemes...)
|
||||
|
||||
Conf.m.Lock()
|
||||
Conf.Appearance.DarkThemes = darkThemes
|
||||
Conf.Appearance.LightThemes = lightThemes
|
||||
Conf.Appearance.ThemeVer = themeVer
|
||||
Conf.Appearance.ThemeJS = themeJS
|
||||
Conf.m.Unlock()
|
||||
}
|
||||
|
||||
func loadIcons() {
|
||||
func LoadIcons() {
|
||||
iconDirs, err := os.ReadDir(util.IconsPath)
|
||||
if err != nil {
|
||||
logging.LogErrorf("read appearance icons folder failed: %s", err)
|
||||
|
|
@ -189,7 +166,9 @@ func loadIcons() {
|
|||
return
|
||||
}
|
||||
|
||||
Conf.Appearance.Icons = nil
|
||||
var icons []string
|
||||
var iconVer string
|
||||
currentIcon := Conf.Appearance.Icon
|
||||
for _, iconDir := range iconDirs {
|
||||
if !util.IsDirRegularOrSymlink(iconDir) {
|
||||
continue
|
||||
|
|
@ -199,77 +178,15 @@ func loadIcons() {
|
|||
if err != nil || nil == iconConf {
|
||||
continue
|
||||
}
|
||||
Conf.Appearance.Icons = append(Conf.Appearance.Icons, name)
|
||||
if Conf.Appearance.Icon == name {
|
||||
Conf.Appearance.IconVer = iconConf.Version
|
||||
icons = append(icons, name)
|
||||
if currentIcon == name {
|
||||
iconVer = iconConf.Version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ReloadIcon() {
|
||||
loadIcons()
|
||||
}
|
||||
|
||||
func unwatchTheme(folder string) {
|
||||
val, _ := themeWatchers.Load(folder)
|
||||
if nil != val {
|
||||
themeWatcher := val.(*fsnotify.Watcher)
|
||||
themeWatcher.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func watchTheme(folder string) {
|
||||
val, _ := themeWatchers.Load(folder)
|
||||
var themeWatcher *fsnotify.Watcher
|
||||
if nil != val {
|
||||
themeWatcher = val.(*fsnotify.Watcher)
|
||||
themeWatcher.Close()
|
||||
}
|
||||
|
||||
var err error
|
||||
if themeWatcher, err = fsnotify.NewWatcher(); err != nil {
|
||||
logging.LogErrorf("add theme file watcher for folder [%s] failed: %s", folder, err)
|
||||
return
|
||||
}
|
||||
themeWatchers.Store(folder, themeWatcher)
|
||||
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-themeWatcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
//logging.LogInfof(event.String())
|
||||
if event.Op&fsnotify.Write == fsnotify.Write && (strings.HasSuffix(event.Name, "theme.css")) {
|
||||
var themeName string
|
||||
if themeName = isCurrentUseTheme(event.Name); "" == themeName {
|
||||
break
|
||||
}
|
||||
|
||||
if strings.HasSuffix(event.Name, "theme.css") {
|
||||
util.BroadcastByType("main", "refreshtheme", 0, "", map[string]interface{}{
|
||||
"theme": "/appearance/themes/" + themeName + "/theme.css?" + fmt.Sprintf("%d", time.Now().Unix()),
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
case err, ok := <-themeWatcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
logging.LogErrorf("watch theme file failed: %s", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
//logging.LogInfof("add file watcher [%s]", folder)
|
||||
if err := themeWatcher.Add(folder); err != nil {
|
||||
logging.LogErrorf("add theme files watcher for folder [%s] failed: %s", folder, err)
|
||||
}
|
||||
<-done
|
||||
Conf.m.Lock()
|
||||
Conf.Appearance.Icons = icons
|
||||
Conf.Appearance.IconVer = iconVer
|
||||
Conf.m.Unlock()
|
||||
}
|
||||
|
||||
func isCurrentUseTheme(themePath string) string {
|
||||
|
|
@ -285,3 +202,22 @@ func isCurrentUseTheme(themePath string) string {
|
|||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func broadcastRefreshThemeIfCurrent(themeCssPath string) {
|
||||
if !strings.HasSuffix(themeCssPath, "theme.css") {
|
||||
return
|
||||
}
|
||||
// 只处理主题根目录中的 theme.css
|
||||
themeDir := filepath.Clean(filepath.Dir(themeCssPath))
|
||||
themesRoot := filepath.Clean(util.ThemesPath)
|
||||
if themeDir != filepath.Join(themesRoot, filepath.Base(themeDir)) {
|
||||
return
|
||||
}
|
||||
themeName := isCurrentUseTheme(themeCssPath)
|
||||
if themeName == "" {
|
||||
return
|
||||
}
|
||||
util.BroadcastByType("main", "refreshtheme", 0, "", map[string]interface{}{
|
||||
"theme": "/appearance/themes/" + themeName + "/theme.css?" + fmt.Sprintf("%d", time.Now().Unix()),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,23 +37,30 @@ func WatchAssets() {
|
|||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
watchAssets()
|
||||
}()
|
||||
go watchAssets()
|
||||
}
|
||||
|
||||
func watchAssets() {
|
||||
CloseWatchAssets()
|
||||
assetsDir := filepath.Join(util.DataDir, "assets")
|
||||
if nil != assetsWatcher {
|
||||
assetsWatcher.Close()
|
||||
}
|
||||
|
||||
var err error
|
||||
if assetsWatcher, err = fsnotify.NewWatcher(); err != nil {
|
||||
assetsWatcher, err = fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
logging.LogErrorf("add assets watcher for folder [%s] failed: %s", assetsDir, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !gulu.File.IsDir(assetsDir) {
|
||||
os.MkdirAll(assetsDir, 0755)
|
||||
}
|
||||
|
||||
if err = assetsWatcher.Add(assetsDir); err != nil {
|
||||
logging.LogErrorf("add assets watcher for folder [%s] failed: %s", assetsDir, err)
|
||||
CloseWatchAssets()
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer logging.Recover()
|
||||
|
||||
|
|
@ -95,19 +102,11 @@ func watchAssets() {
|
|||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if !gulu.File.IsDir(assetsDir) {
|
||||
os.MkdirAll(assetsDir, 0755)
|
||||
}
|
||||
|
||||
if err = assetsWatcher.Add(assetsDir); err != nil {
|
||||
logging.LogErrorf("add assets watcher for folder [%s] failed: %s", assetsDir, err)
|
||||
}
|
||||
//logging.LogInfof("added file watcher [%s]", assetsDir)
|
||||
}
|
||||
|
||||
func CloseWatchAssets() {
|
||||
if nil != assetsWatcher {
|
||||
assetsWatcher.Close()
|
||||
assetsWatcher = nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,9 +19,11 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/88250/gulu"
|
||||
"github.com/radovskyb/watcher"
|
||||
"github.com/siyuan-note/logging"
|
||||
"github.com/siyuan-note/siyuan/kernel/cache"
|
||||
|
|
@ -31,20 +33,31 @@ import (
|
|||
var assetsWatcher *watcher.Watcher
|
||||
|
||||
func WatchAssets() {
|
||||
go func() {
|
||||
watchAssets()
|
||||
}()
|
||||
if !isFileWatcherAvailable() {
|
||||
return
|
||||
}
|
||||
|
||||
go watchAssets()
|
||||
}
|
||||
|
||||
func watchAssets() {
|
||||
if nil != assetsWatcher {
|
||||
assetsWatcher.Close()
|
||||
}
|
||||
assetsWatcher = watcher.New()
|
||||
|
||||
CloseWatchAssets()
|
||||
assetsDir := filepath.Join(util.DataDir, "assets")
|
||||
|
||||
assetsWatcher = watcher.New()
|
||||
|
||||
if !gulu.File.IsDir(assetsDir) {
|
||||
os.MkdirAll(assetsDir, 0755)
|
||||
}
|
||||
|
||||
if err := assetsWatcher.Add(assetsDir); err != nil {
|
||||
logging.LogErrorf("add assets watcher for folder [%s] failed: %s", assetsDir, err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer logging.Recover()
|
||||
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-assetsWatcher.Event:
|
||||
|
|
@ -75,13 +88,6 @@ func watchAssets() {
|
|||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err := assetsWatcher.Add(assetsDir); err != nil {
|
||||
logging.LogErrorf("add assets watcher for folder [%s] failed: %s", assetsDir, err)
|
||||
return
|
||||
}
|
||||
|
||||
//logging.LogInfof("added file watcher [%s]", assetsDir)
|
||||
if err := assetsWatcher.Start(10 * time.Second); err != nil {
|
||||
logging.LogErrorf("start assets watcher for folder [%s] failed: %s", assetsDir, err)
|
||||
return
|
||||
|
|
@ -91,5 +97,6 @@ func watchAssets() {
|
|||
func CloseWatchAssets() {
|
||||
if nil != assetsWatcher {
|
||||
assetsWatcher.Close()
|
||||
assetsWatcher = nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -267,7 +267,7 @@ func UninstallBazaarPlugin(pluginName, frontend string) error {
|
|||
savePetals(petals)
|
||||
|
||||
uninstallPluginSet := hashset.New(pluginName)
|
||||
PushReloadPlugin(nil, nil, nil, uninstallPluginSet, "")
|
||||
PushReloadPlugin(uninstallPluginSet, nil, nil, nil, "")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -432,7 +432,7 @@ func InstalledThemes(keyword string) (ret []*bazaar.Theme) {
|
|||
}
|
||||
|
||||
func InstallBazaarTheme(repoURL, repoHash, themeName string, mode int, update bool) error {
|
||||
closeThemeWatchers()
|
||||
CloseWatchThemes()
|
||||
|
||||
installPath := filepath.Join(util.ThemesPath, themeName)
|
||||
err := bazaar.InstallTheme(repoURL, repoHash, installPath, Conf.System.ID)
|
||||
|
|
@ -458,7 +458,7 @@ func InstallBazaarTheme(repoURL, repoHash, themeName string, mode int, update bo
|
|||
}
|
||||
|
||||
func UninstallBazaarTheme(themeName string) error {
|
||||
closeThemeWatchers()
|
||||
CloseWatchThemes()
|
||||
|
||||
installPath := filepath.Join(util.ThemesPath, themeName)
|
||||
err := bazaar.UninstallTheme(installPath)
|
||||
|
|
@ -580,3 +580,8 @@ func getSearchKeywords(query string) (ret []string) {
|
|||
}
|
||||
return
|
||||
}
|
||||
|
||||
// isBuiltInTheme 通过包名或目录名判断是否为内置主题
|
||||
func isBuiltInTheme(name string) bool {
|
||||
return "daylight" == name || "midnight" == name
|
||||
}
|
||||
|
|
@ -32,27 +32,34 @@ import (
|
|||
var emojisWatcher *fsnotify.Watcher
|
||||
|
||||
func WatchEmojis() {
|
||||
if util.ContainerAndroid == util.Container || util.ContainerIOS == util.Container || util.ContainerHarmony == util.Container {
|
||||
if !isFileWatcherAvailable() {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
watchEmojis()
|
||||
}()
|
||||
go watchEmojis()
|
||||
}
|
||||
|
||||
func watchEmojis() {
|
||||
CloseWatchEmojis()
|
||||
emojisDir := filepath.Join(util.DataDir, "emojis")
|
||||
if nil != emojisWatcher {
|
||||
emojisWatcher.Close()
|
||||
}
|
||||
|
||||
var err error
|
||||
if emojisWatcher, err = fsnotify.NewWatcher(); err != nil {
|
||||
emojisWatcher, err = fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
logging.LogErrorf("add emojis watcher for folder [%s] failed: %s", emojisDir, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !gulu.File.IsDir(emojisDir) {
|
||||
os.MkdirAll(emojisDir, 0755)
|
||||
}
|
||||
|
||||
if err = emojisWatcher.Add(emojisDir); err != nil {
|
||||
logging.LogErrorf("add emojis watcher for folder [%s] failed: %s", emojisDir, err)
|
||||
CloseWatchEmojis()
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer logging.Recover()
|
||||
|
||||
|
|
@ -77,19 +84,11 @@ func watchEmojis() {
|
|||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if !gulu.File.IsDir(emojisDir) {
|
||||
os.MkdirAll(emojisDir, 0755)
|
||||
}
|
||||
|
||||
if err = emojisWatcher.Add(emojisDir); err != nil {
|
||||
logging.LogErrorf("add emojis watcher for folder [%s] failed: %s", emojisDir, err)
|
||||
}
|
||||
//logging.LogInfof("added file watcher [%s]", emojisDir)
|
||||
}
|
||||
|
||||
func CloseWatchEmojis() {
|
||||
if nil != emojisWatcher {
|
||||
emojisWatcher.Close()
|
||||
emojisWatcher = nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,9 +19,11 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/88250/gulu"
|
||||
"github.com/radovskyb/watcher"
|
||||
"github.com/siyuan-note/logging"
|
||||
"github.com/siyuan-note/siyuan/kernel/util"
|
||||
|
|
@ -30,20 +32,31 @@ import (
|
|||
var emojisWatcher *watcher.Watcher
|
||||
|
||||
func WatchEmojis() {
|
||||
go func() {
|
||||
watchEmojis()
|
||||
}()
|
||||
if !isFileWatcherAvailable() {
|
||||
return
|
||||
}
|
||||
|
||||
go watchEmojis()
|
||||
}
|
||||
|
||||
func watchEmojis() {
|
||||
if nil != emojisWatcher {
|
||||
emojisWatcher.Close()
|
||||
}
|
||||
emojisWatcher = watcher.New()
|
||||
|
||||
emojisDir := filepath.Join(util.DataDir, "emojis")
|
||||
|
||||
CloseWatchEmojis()
|
||||
emojisWatcher = watcher.New()
|
||||
|
||||
if !gulu.File.IsDir(emojisDir) {
|
||||
os.MkdirAll(emojisDir, 0755)
|
||||
}
|
||||
|
||||
if err := emojisWatcher.Add(emojisDir); err != nil {
|
||||
logging.LogErrorf("add emojis watcher for folder [%s] failed: %s", emojisDir, err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer logging.Recover()
|
||||
|
||||
for {
|
||||
select {
|
||||
case _, ok := <-emojisWatcher.Event:
|
||||
|
|
@ -61,13 +74,6 @@ func watchEmojis() {
|
|||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err := emojisWatcher.Add(emojisDir); err != nil {
|
||||
logging.LogErrorf("add emojis watcher for folder [%s] failed: %s", emojisDir, err)
|
||||
return
|
||||
}
|
||||
|
||||
//logging.LogInfof("added file watcher [%s]", emojisDir)
|
||||
if err := emojisWatcher.Start(10 * time.Second); err != nil {
|
||||
logging.LogErrorf("start emojis watcher for folder [%s] failed: %s", emojisDir, err)
|
||||
return
|
||||
|
|
@ -77,5 +83,6 @@ func watchEmojis() {
|
|||
func CloseWatchEmojis() {
|
||||
if nil != emojisWatcher {
|
||||
emojisWatcher.Close()
|
||||
emojisWatcher = nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,85 +43,50 @@ func PushReloadSnippet(snippet *conf.Snpt) {
|
|||
util.BroadcastByType("main", "setSnippet", 0, "", snippet)
|
||||
}
|
||||
|
||||
func PushReloadPlugin(upsertCodePluginSet, upsertDataPluginSet, unloadPluginNameSet, uninstallPluginNameSet *hashset.Set, excludeApp string) {
|
||||
// 集合去重
|
||||
if nil != uninstallPluginNameSet {
|
||||
for _, n := range uninstallPluginNameSet.Values() {
|
||||
pluginName := n.(string)
|
||||
if nil != upsertCodePluginSet {
|
||||
upsertCodePluginSet.Remove(pluginName)
|
||||
}
|
||||
if nil != upsertDataPluginSet {
|
||||
upsertDataPluginSet.Remove(pluginName)
|
||||
}
|
||||
if nil != unloadPluginNameSet {
|
||||
unloadPluginNameSet.Remove(pluginName)
|
||||
func PushReloadPlugin(uninstallPluginNameSet, unloadPluginNameSet, reloadPluginSet, dataChangePluginSet *hashset.Set, excludeApp string) {
|
||||
// 按优先级从高到低排列,同一插件只保留在优先级最高的集合中
|
||||
orderedSets := []*hashset.Set{uninstallPluginNameSet, unloadPluginNameSet, reloadPluginSet, dataChangePluginSet}
|
||||
slices := make([][]string, len(orderedSets))
|
||||
// 按顺序遍历所有集合
|
||||
for i, set := range orderedSets {
|
||||
if nil != set {
|
||||
// 遍历当前集合的所有插件名称
|
||||
for _, n := range set.Values() {
|
||||
name := n.(string)
|
||||
// 将该插件从所有后续集合中移除
|
||||
for _, lowerSet := range orderedSets[i+1:] {
|
||||
if nil != lowerSet {
|
||||
lowerSet.Remove(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if nil != unloadPluginNameSet {
|
||||
for _, n := range unloadPluginNameSet.Values() {
|
||||
pluginName := n.(string)
|
||||
if nil != upsertCodePluginSet {
|
||||
upsertCodePluginSet.Remove(pluginName)
|
||||
}
|
||||
if nil != upsertDataPluginSet {
|
||||
upsertDataPluginSet.Remove(pluginName)
|
||||
}
|
||||
}
|
||||
}
|
||||
if nil != upsertCodePluginSet {
|
||||
for _, n := range upsertCodePluginSet.Values() {
|
||||
pluginName := n.(string)
|
||||
if nil != upsertDataPluginSet {
|
||||
upsertDataPluginSet.Remove(pluginName)
|
||||
|
||||
// 将当前集合转换为字符串切片
|
||||
if nil == set {
|
||||
slices[i] = []string{}
|
||||
} else {
|
||||
strs := make([]string, 0, set.Size())
|
||||
for _, n := range set.Values() {
|
||||
strs = append(strs, n.(string))
|
||||
}
|
||||
slices[i] = strs
|
||||
}
|
||||
}
|
||||
|
||||
upsertCodePlugins, upsertDataPlugins, unloadPlugins, uninstallPlugins := []string{}, []string{}, []string{}, []string{}
|
||||
if nil != upsertCodePluginSet {
|
||||
for _, n := range upsertCodePluginSet.Values() {
|
||||
upsertCodePlugins = append(upsertCodePlugins, n.(string))
|
||||
}
|
||||
}
|
||||
if nil != upsertDataPluginSet {
|
||||
for _, n := range upsertDataPluginSet.Values() {
|
||||
upsertDataPlugins = append(upsertDataPlugins, n.(string))
|
||||
}
|
||||
}
|
||||
if nil != unloadPluginNameSet {
|
||||
for _, n := range unloadPluginNameSet.Values() {
|
||||
unloadPlugins = append(unloadPlugins, n.(string))
|
||||
}
|
||||
}
|
||||
if nil != uninstallPluginNameSet {
|
||||
for _, n := range uninstallPluginNameSet.Values() {
|
||||
uninstallPlugins = append(uninstallPlugins, n.(string))
|
||||
}
|
||||
logging.LogInfof("reload plugins, uninstalls=%v, unloads=%v, reloads=%v, dataChanges=%v", slices[0], slices[1], slices[2], slices[3])
|
||||
payload := map[string]interface{}{
|
||||
"uninstallPlugins": slices[0], // 插件卸载
|
||||
"unloadPlugins": slices[1], // 插件禁用
|
||||
"reloadPlugins": slices[2], // 插件启用,或插件代码变更
|
||||
"dataChangePlugins": slices[3], // 插件存储数据变更
|
||||
}
|
||||
|
||||
pushReloadPlugin0(upsertCodePlugins, upsertDataPlugins, unloadPlugins, uninstallPlugins, excludeApp)
|
||||
}
|
||||
|
||||
func pushReloadPlugin0(upsertCodePlugins, upsertDataPlugins, unloadPlugins, uninstallPlugins []string, excludeApp string) {
|
||||
logging.LogInfof("reload plugins [codeChanges=%v, dataChanges=%v, unloads=%v, uninstalls=%v]", upsertCodePlugins, upsertDataPlugins, unloadPlugins, uninstallPlugins)
|
||||
if "" == excludeApp {
|
||||
util.BroadcastByType("main", "reloadPlugin", 0, "", map[string]interface{}{
|
||||
"upsertCodePlugins": upsertCodePlugins,
|
||||
"upsertDataPlugins": upsertDataPlugins,
|
||||
"unloadPlugins": unloadPlugins,
|
||||
"uninstallPlugins": uninstallPlugins,
|
||||
})
|
||||
util.BroadcastByType("main", "reloadPlugin", 0, "", payload)
|
||||
return
|
||||
}
|
||||
|
||||
util.BroadcastByTypeAndExcludeApp(excludeApp, "main", "reloadPlugin", 0, "", map[string]interface{}{
|
||||
"upsertCodePlugins": upsertCodePlugins,
|
||||
"upsertDataPlugins": upsertDataPlugins,
|
||||
"unloadPlugins": unloadPlugins,
|
||||
"uninstallPlugins": uninstallPlugins,
|
||||
})
|
||||
util.BroadcastByTypeAndExcludeApp(excludeApp, "main", "reloadPlugin", 0, "", payload)
|
||||
}
|
||||
|
||||
func refreshDocInfo(tree *parse.Tree) {
|
||||
|
|
|
|||
|
|
@ -1596,8 +1596,8 @@ func processSyncMergeResult(exit, byHand bool, mergeResult *dejavu.MergeResult,
|
|||
var upsertTrees int
|
||||
// 可能需要重新加载部分功能
|
||||
var needReloadFlashcard, needReloadOcrTexts, needReloadPlugin, needReloadSnippet bool
|
||||
upsertCodePluginSet := hashset.New() // 插件代码变更 data/plugins/
|
||||
upsertDataPluginSet := hashset.New() // 插件存储数据变更 data/storage/petal/
|
||||
reloadPluginSet := hashset.New() // 插件代码变更 data/plugins/
|
||||
dataChangePluginSet := hashset.New() // 插件存储数据变更 data/storage/petal/
|
||||
needUnindexBoxes, needIndexBoxes := map[string]bool{}, map[string]bool{}
|
||||
for _, file := range mergeResult.Upserts {
|
||||
upserts = append(upserts, file.Path)
|
||||
|
|
@ -1620,7 +1620,7 @@ func processSyncMergeResult(exit, byHand bool, mergeResult *dejavu.MergeResult,
|
|||
needReloadPlugin = true
|
||||
if parts := strings.Split(file.Path, "/"); 3 < len(parts) {
|
||||
if pluginName := parts[3]; "petals.json" != pluginName {
|
||||
upsertDataPluginSet.Add(pluginName)
|
||||
dataChangePluginSet.Add(pluginName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1628,7 +1628,7 @@ func processSyncMergeResult(exit, byHand bool, mergeResult *dejavu.MergeResult,
|
|||
if strings.HasPrefix(file.Path, "/plugins/") {
|
||||
if parts := strings.Split(file.Path, "/"); 2 < len(parts) {
|
||||
needReloadPlugin = true
|
||||
upsertCodePluginSet.Add(parts[2])
|
||||
reloadPluginSet.Add(parts[2])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1667,7 +1667,7 @@ func processSyncMergeResult(exit, byHand bool, mergeResult *dejavu.MergeResult,
|
|||
needReloadPlugin = true
|
||||
if parts := strings.Split(file.Path, "/"); 3 < len(parts) {
|
||||
if pluginName := parts[3]; "petals.json" != pluginName {
|
||||
upsertDataPluginSet.Add(pluginName)
|
||||
dataChangePluginSet.Add(pluginName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1698,7 +1698,7 @@ func processSyncMergeResult(exit, byHand bool, mergeResult *dejavu.MergeResult,
|
|||
|
||||
for _, upsertPetal := range mergeResult.UpsertPetals {
|
||||
needReloadPlugin = true
|
||||
upsertCodePluginSet.Add(upsertPetal)
|
||||
reloadPluginSet.Add(upsertPetal)
|
||||
}
|
||||
for _, removePetal := range mergeResult.RemovePetals {
|
||||
needReloadPlugin = true
|
||||
|
|
@ -1715,7 +1715,7 @@ func processSyncMergeResult(exit, byHand bool, mergeResult *dejavu.MergeResult,
|
|||
}
|
||||
|
||||
if needReloadPlugin {
|
||||
PushReloadPlugin(upsertCodePluginSet, upsertDataPluginSet, unloadPluginSet, uninstallPluginSet, "")
|
||||
PushReloadPlugin(uninstallPluginSet, unloadPluginSet, reloadPluginSet, dataChangePluginSet, "")
|
||||
}
|
||||
|
||||
if needReloadSnippet {
|
||||
|
|
|
|||
161
kernel/model/themes_watcher.go
Normal file
161
kernel/model/themes_watcher.go
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
// 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/>.
|
||||
|
||||
//go:build !darwin
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/88250/gulu"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/siyuan-note/logging"
|
||||
"github.com/siyuan-note/siyuan/kernel/util"
|
||||
)
|
||||
|
||||
var themesWatcher *fsnotify.Watcher
|
||||
|
||||
func WatchThemes() {
|
||||
if !isFileWatcherAvailable() {
|
||||
return
|
||||
}
|
||||
|
||||
go watchThemes()
|
||||
}
|
||||
|
||||
func watchThemes() {
|
||||
CloseWatchThemes()
|
||||
themesDir := util.ThemesPath
|
||||
|
||||
var err error
|
||||
themesWatcher, err = fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
logging.LogErrorf("add themes watcher for folder [%s] failed: %s", themesDir, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !gulu.File.IsDir(themesDir) {
|
||||
os.MkdirAll(themesDir, 0755)
|
||||
}
|
||||
|
||||
if err = themesWatcher.Add(themesDir); err != nil {
|
||||
logging.LogErrorf("add themes root watcher for folder [%s] failed: %s", themesDir, err)
|
||||
CloseWatchThemes()
|
||||
return
|
||||
}
|
||||
|
||||
// 为每个子目录添加监听,以便收到 theme.css 的变更
|
||||
addThemesSubdirs(themesWatcher, themesDir)
|
||||
|
||||
go func() {
|
||||
defer logging.Recover()
|
||||
|
||||
var (
|
||||
timer *time.Timer
|
||||
lastEvent fsnotify.Event
|
||||
)
|
||||
timer = time.NewTimer(100 * time.Millisecond)
|
||||
<-timer.C // timer should be expired at first
|
||||
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-themesWatcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 新目录创建时加入监听
|
||||
if event.Op&fsnotify.Create == fsnotify.Create {
|
||||
if isThemesDirectSubdir(event.Name) {
|
||||
if addErr := themesWatcher.Add(event.Name); addErr != nil {
|
||||
logging.LogWarnf("add themes watcher for new folder [%s] failed: %s", event.Name, addErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastEvent = event
|
||||
timer.Reset(time.Millisecond * 100)
|
||||
case err, ok := <-themesWatcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
logging.LogErrorf("watch themes failed: %s", err)
|
||||
case <-timer.C:
|
||||
handleThemesEvent(lastEvent)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// addThemesSubdirs 为 themes 下每个子目录添加监听
|
||||
func addThemesSubdirs(w *fsnotify.Watcher, themesDir string) {
|
||||
entries, err := os.ReadDir(themesDir)
|
||||
if err != nil {
|
||||
logging.LogErrorf("read themes folder failed: %s", err)
|
||||
return
|
||||
}
|
||||
for _, e := range entries {
|
||||
if !util.IsDirRegularOrSymlink(e) {
|
||||
continue
|
||||
}
|
||||
subdir := filepath.Join(themesDir, e.Name())
|
||||
if addErr := w.Add(subdir); addErr != nil {
|
||||
logging.LogWarnf("add themes watcher for folder [%s] failed: %s", subdir, addErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isThemesDirectSubdir 判断 path 是否为 themes 下的直接子目录
|
||||
func isThemesDirectSubdir(path string) bool {
|
||||
if !gulu.File.IsDir(path) {
|
||||
return false
|
||||
}
|
||||
rel, err := filepath.Rel(util.ThemesPath, path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if filepath.Base(path) != rel {
|
||||
return false
|
||||
}
|
||||
entries, err := os.ReadDir(util.ThemesPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
name := filepath.Base(path)
|
||||
for _, e := range entries {
|
||||
if e.Name() == name {
|
||||
return util.IsDirRegularOrSymlink(e)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func handleThemesEvent(event fsnotify.Event) {
|
||||
if event.Op&fsnotify.Write != fsnotify.Write {
|
||||
return
|
||||
}
|
||||
broadcastRefreshThemeIfCurrent(event.Name)
|
||||
}
|
||||
|
||||
func CloseWatchThemes() {
|
||||
if nil != themesWatcher {
|
||||
themesWatcher.Close()
|
||||
themesWatcher = nil
|
||||
}
|
||||
}
|
||||
156
kernel/model/themes_watcher_darwin.go
Normal file
156
kernel/model/themes_watcher_darwin.go
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
// 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/>.
|
||||
|
||||
//go:build darwin
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/88250/gulu"
|
||||
"github.com/radovskyb/watcher"
|
||||
"github.com/siyuan-note/logging"
|
||||
"github.com/siyuan-note/siyuan/kernel/util"
|
||||
)
|
||||
|
||||
var themesWatcher *watcher.Watcher
|
||||
|
||||
func WatchThemes() {
|
||||
if !isFileWatcherAvailable() {
|
||||
return
|
||||
}
|
||||
|
||||
go watchThemes()
|
||||
}
|
||||
|
||||
func watchThemes() {
|
||||
CloseWatchThemes()
|
||||
themesDir := util.ThemesPath
|
||||
|
||||
themesWatcher = watcher.New()
|
||||
|
||||
if !gulu.File.IsDir(themesDir) {
|
||||
os.MkdirAll(themesDir, 0755)
|
||||
}
|
||||
|
||||
if err := themesWatcher.Add(themesDir); err != nil {
|
||||
logging.LogErrorf("add themes watcher for folder [%s] failed: %s", themesDir, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 为每个子目录添加监听,以便收到 theme.css 的变更
|
||||
addThemesSubdirs(themesWatcher, themesDir)
|
||||
|
||||
go func() {
|
||||
defer logging.Recover()
|
||||
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-themesWatcher.Event:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 新目录创建时加入监听
|
||||
if watcher.Create == event.Op {
|
||||
if isThemesDirectSubdir(event.Path) {
|
||||
if addErr := themesWatcher.Add(event.Path); addErr != nil {
|
||||
logging.LogWarnf("add themes watcher for new folder [%s] failed: %s", event.Path, addErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleThemesEvent(event)
|
||||
case err, ok := <-themesWatcher.Error:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
logging.LogErrorf("watch themes failed: %s", err)
|
||||
case <-themesWatcher.Closed:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err := themesWatcher.Start(10 * time.Second); err != nil {
|
||||
logging.LogErrorf("start themes watcher for folder [%s] failed: %s", themesDir, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// addThemesSubdirs 为 themes 下每个子目录添加监听
|
||||
func addThemesSubdirs(w *watcher.Watcher, themesDir string) {
|
||||
entries, err := os.ReadDir(themesDir)
|
||||
if err != nil {
|
||||
logging.LogErrorf("read themes folder failed: %s", err)
|
||||
return
|
||||
}
|
||||
for _, e := range entries {
|
||||
if !util.IsDirRegularOrSymlink(e) {
|
||||
continue
|
||||
}
|
||||
subdir := filepath.Join(themesDir, e.Name())
|
||||
if addErr := w.Add(subdir); addErr != nil {
|
||||
logging.LogWarnf("add themes watcher for folder [%s] failed: %s", subdir, addErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isThemesDirectSubdir 判断 path 是否为 themes 下的直接子目录
|
||||
func isThemesDirectSubdir(path string) bool {
|
||||
if !gulu.File.IsDir(path) {
|
||||
return false
|
||||
}
|
||||
rel, err := filepath.Rel(util.ThemesPath, path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if filepath.Base(path) != rel {
|
||||
return false
|
||||
}
|
||||
entries, err := os.ReadDir(util.ThemesPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
name := filepath.Base(path)
|
||||
for _, e := range entries {
|
||||
if e.Name() == name {
|
||||
return util.IsDirRegularOrSymlink(e)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func handleThemesEvent(event watcher.Event) {
|
||||
if watcher.Write != event.Op {
|
||||
return
|
||||
}
|
||||
if !strings.HasSuffix(event.Path, "theme.css") {
|
||||
return
|
||||
}
|
||||
broadcastRefreshThemeIfCurrent(event.Path)
|
||||
}
|
||||
|
||||
func CloseWatchThemes() {
|
||||
if nil != themesWatcher {
|
||||
themesWatcher.Close()
|
||||
themesWatcher = nil
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
|
@ -213,6 +214,58 @@ func JsonArg(c *gin.Context, result *gulu.Result) (arg map[string]interface{}, o
|
|||
return
|
||||
}
|
||||
|
||||
// ParseJsonArg 使用泛型从 JSON 参数中提取指定键的值。
|
||||
// - 如果 required 为 true 但参数缺失,则会在 ret.Msg 中写入 “[key] is required”
|
||||
// - 如果参数存在但类型不匹配,则会在 ret.Msg 中写入 “[key] should be [T]”
|
||||
// - 返回值 ok 为 false 时,表示提取失败或类型不匹配
|
||||
func ParseJsonArg[T any](key string, required bool, arg map[string]interface{}, ret *gulu.Result) (value T, ok bool) {
|
||||
raw, exists := arg[key]
|
||||
if !exists || raw == nil {
|
||||
if required {
|
||||
ret.Code = -1
|
||||
ret.Msg = key + " is required"
|
||||
} else {
|
||||
ok = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
value, ok = raw.(T)
|
||||
if !ok {
|
||||
var zero T
|
||||
ret.Code = -1
|
||||
ret.Msg = fmt.Sprintf("%s should be %T", key, zero)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// JsonArgParseFunc 为单次提取函数,用于 ParseJsonArgs 批量提取。
|
||||
type JsonArgParseFunc func(arg map[string]interface{}, ret *gulu.Result) bool
|
||||
|
||||
// BindJsonArg 创建一个提取函数:从 arg 取 key 并写入 dest,供 ParseJsonArgs 使用。
|
||||
func BindJsonArg[T any](key string, required bool, dest *T) JsonArgParseFunc {
|
||||
return func(arg map[string]interface{}, ret *gulu.Result) bool {
|
||||
v, ok := ParseJsonArg[T](key, required, arg, ret)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
*dest = v
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// ParseJsonArgs 按顺序执行多个提取函数。
|
||||
// - 任一失败返回 false 并在 ret 中写入错误信息
|
||||
// - 全部成功返回 true
|
||||
func ParseJsonArgs(arg map[string]interface{}, ret *gulu.Result, extractors ...JsonArgParseFunc) bool {
|
||||
for _, ext := range extractors {
|
||||
if !ext(arg, ret) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func InvalidIDPattern(idArg string, result *gulu.Result) bool {
|
||||
if ast.IsNodeIDPattern(idArg) {
|
||||
return false
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue