Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
Vanessa 2026-03-05 10:57:38 +08:00
commit 2d47cb3684
24 changed files with 1191 additions and 796 deletions

View file

@ -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;
}

View file

@ -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");

View file

@ -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") ||

View file

@ -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) {

View file

@ -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;

View file

@ -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()

View file

@ -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,

View file

@ -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)
}
}

View file

@ -76,5 +76,5 @@ func reloadIcon(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
model.ReloadIcon()
model.LoadIcons()
}

View file

@ -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
View 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
View 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
}

View file

@ -55,5 +55,6 @@ func main() {
model.WatchAssets()
model.WatchEmojis()
model.WatchThemes()
model.HandleSignal()
}

View file

@ -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()),
})
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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) {

View file

@ -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 {

View 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
}
}

View 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
}
}

View file

@ -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