chore: initial commit
This commit is contained in:
commit
39eda568e9
67
.github/workflows/main.yml
vendored
Normal file
67
.github/workflows/main.yml
vendored
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# This is a basic workflow to help you get started with Actions
|
||||||
|
|
||||||
|
name: Release
|
||||||
|
|
||||||
|
env:
|
||||||
|
PLUGIN_NAME: logseq-plugin-favorite-tree
|
||||||
|
|
||||||
|
# Controls when the workflow will run
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*" # Push events to matching any tag format, i.e. 1.0, 20.15.10
|
||||||
|
|
||||||
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
# The type of runner that the job will run on
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||||
|
steps:
|
||||||
|
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: "16.x"
|
||||||
|
- uses: pnpm/action-setup@v2.2.4
|
||||||
|
with:
|
||||||
|
version: 6.0.2
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
id: build
|
||||||
|
run: |
|
||||||
|
pnpm install
|
||||||
|
pnpm build
|
||||||
|
mkdir ${{ env.PLUGIN_NAME }}
|
||||||
|
cp README.md package.json icon.png ${{ env.PLUGIN_NAME }}
|
||||||
|
mv dist ${{ env.PLUGIN_NAME }}
|
||||||
|
zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }}
|
||||||
|
ls
|
||||||
|
echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)"
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
id: create_release
|
||||||
|
uses: actions/create-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
VERSION: ${{ github.ref }}
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref }}
|
||||||
|
release_name: ${{ github.ref }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
|
||||||
|
- name: Upload zip file
|
||||||
|
id: upload_zip
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./${{ env.PLUGIN_NAME }}.zip
|
||||||
|
asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip
|
||||||
|
asset_content_type: application/zip
|
||||||
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
*.zip
|
||||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"arrowParens": "always",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Seth Yuan
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
15
README.md
Normal file
15
README.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
English | [中文](README.zh.md)
|
||||||
|
|
||||||
|
# logseq-plugin-favorite-tree
|
||||||
|
|
||||||
|
## Feature Highlights
|
||||||
|
|
||||||
|
- Tree structure for "Favorites" and "Recents" based on namespaces and/or page properties. You can affect the order of display by writing a `fixed` property on the page you want to adjust, e.g, `fixed:: 100`. Smaller the number, closer to the top it will be.
|
||||||
|
- Set combination of filters through page property and have them displayed in the tree structure. Please refer the demo video below.
|
||||||
|
- Slider to adjust the left sidebar's width.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
https://github.com/sethyuan/logseq-plugin-another-embed/assets/3410293/32b2a19e-19b3-4113-8fee-f2a445d151cc
|
||||||
|
|
||||||
|
https://github.com/sethyuan/logseq-plugin-another-embed/assets/3410293/d586158a-6781-44fd-931b-1eca8c4df780
|
||||||
15
README.zh.md
Normal file
15
README.zh.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
[English](README.md) | 中文
|
||||||
|
|
||||||
|
# logseq-plugin-favorite-tree
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
|
- 基于 namespace 或者页面属性实现树形“收藏”与“最近使用”。可通过`fixed`页面属性来调整展示顺序,例如 `fixed:: 100`,数值越小位置越靠前。
|
||||||
|
- 可在页面上通过属性设置组合过滤器,会在树形收藏上展示。可参见下方演示视频。
|
||||||
|
- 通过拖拽来调整左侧边栏宽度。
|
||||||
|
|
||||||
|
## 使用展示
|
||||||
|
|
||||||
|
https://github.com/sethyuan/logseq-plugin-another-embed/assets/3410293/32b2a19e-19b3-4113-8fee-f2a445d151cc
|
||||||
|
|
||||||
|
https://github.com/sethyuan/logseq-plugin-another-embed/assets/3410293/d586158a-6781-44fd-931b-1eca8c4df780
|
||||||
15
index.html
Normal file
15
index.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="data:" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Favorite Tree</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<script type="module" src="/src/plugin.tsx"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
29
package.json
Normal file
29
package.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "logseq-plugin-favorite-tree",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "dist/index.html",
|
||||||
|
"logseq": {
|
||||||
|
"id": "_sethyuan-logseq-favorite-tree",
|
||||||
|
"icon": "./icon.png"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@logseq/libs": "^0.0.15",
|
||||||
|
"immer": "^9.0.19",
|
||||||
|
"logseq-l10n": "^0.2.0",
|
||||||
|
"preact": "^10.14.1",
|
||||||
|
"rambdax": "^10.0.0",
|
||||||
|
"reactutils": "^5.13.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@preact/preset-vite": "^2.5.0",
|
||||||
|
"@types/node": "^18.15.11",
|
||||||
|
"typescript": "^5.1.6",
|
||||||
|
"vite": "^4.4.8",
|
||||||
|
"vite-plugin-logseq": "^1.1.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
1043
pnpm-lock.yaml
generated
Normal file
1043
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
src/comps/FavArrow.tsx
Normal file
26
src/comps/FavArrow.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { cls } from "reactutils"
|
||||||
|
|
||||||
|
export default function FavArrow({
|
||||||
|
expanded,
|
||||||
|
onToggle,
|
||||||
|
}: {
|
||||||
|
expanded: boolean
|
||||||
|
onToggle: (e: Event) => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
class={cls("kef-ae-fav-arrow", expanded && "kef-ae-fav-arrow-expanded")}
|
||||||
|
onClick={onToggle}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 128 128"
|
||||||
|
fill="currentColor"
|
||||||
|
display="inline-block"
|
||||||
|
class="h-3 w-3"
|
||||||
|
>
|
||||||
|
<path d="M64.177 100.069a7.889 7.889 0 01-5.6-2.316l-55.98-55.98a7.92 7.92 0 010-11.196c3.086-3.085 8.105-3.092 11.196 0l50.382 50.382 50.382-50.382a7.92 7.92 0 0111.195 0c3.086 3.086 3.092 8.104 0 11.196l-55.98 55.98a7.892 7.892 0 01-5.595 2.316z"></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
164
src/comps/FavList.tsx
Normal file
164
src/comps/FavList.tsx
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import { produce } from "immer"
|
||||||
|
import { createPortal } from "preact/compat"
|
||||||
|
import { useEffect, useState } from "preact/hooks"
|
||||||
|
import { cls } from "reactutils"
|
||||||
|
import { queryForSubItems } from "../libs/utils"
|
||||||
|
import FavArrow from "./FavArrow"
|
||||||
|
|
||||||
|
export default function FavList({
|
||||||
|
items,
|
||||||
|
arrowContainer,
|
||||||
|
}: {
|
||||||
|
items: any[]
|
||||||
|
arrowContainer: HTMLElement
|
||||||
|
}) {
|
||||||
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
|
||||||
|
function toggleList(e: Event) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
setExpanded((v) => !v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{createPortal(
|
||||||
|
<FavArrow expanded={expanded} onToggle={toggleList} />,
|
||||||
|
arrowContainer,
|
||||||
|
)}
|
||||||
|
<SubList items={items} shown={expanded} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SubList({ items, shown }: { items: any[]; shown: boolean }) {
|
||||||
|
const [childrenData, setChildrenData] = useState<any>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setChildrenData(null)
|
||||||
|
}, [items])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (shown && childrenData == null) {
|
||||||
|
;(async () => {
|
||||||
|
const data: any = {}
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.filters) {
|
||||||
|
if (item.subitems) {
|
||||||
|
data[item.displayName] = {
|
||||||
|
expanded: false,
|
||||||
|
items: Object.values(item.subitems),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const subitems = await queryForSubItems(item["original-name"])
|
||||||
|
if (subitems?.length > 0) {
|
||||||
|
data[item.name] = { expanded: false, items: subitems }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setChildrenData(data)
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
}, [shown, childrenData, items])
|
||||||
|
|
||||||
|
async function openPage(e: MouseEvent, item: any) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
if (item.filters) {
|
||||||
|
let content = (await logseq.Editor.getBlock(
|
||||||
|
item.blockUUID,
|
||||||
|
))!.content.replace(/\n*^filters:: .*\n*/m, "")
|
||||||
|
content += `\nfilters:: ${`{${item.filters
|
||||||
|
.map((filter: string) => `"${filter.toLowerCase()}" true`)
|
||||||
|
.join(", ")}}`}`
|
||||||
|
await logseq.Editor.updateBlock(item.blockUUID, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(`http://localhost${parent.location.hash.substring(6)}`)
|
||||||
|
const isAlreadyOnPage =
|
||||||
|
decodeURIComponent(url.pathname.substring(1)) === item.name
|
||||||
|
if (!isAlreadyOnPage) {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
logseq.Editor.openInRightSidebar(item.uuid ?? item.pageUUID)
|
||||||
|
} else {
|
||||||
|
;(logseq.Editor.scrollToBlockInPage as any)(item.name)
|
||||||
|
}
|
||||||
|
} else if (item.filters) {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
// NOTE: right sidebar refreshing is not possible yet.
|
||||||
|
logseq.Editor.openInRightSidebar(item.uuid ?? item.pageUUID)
|
||||||
|
} else {
|
||||||
|
// HACK: remove this hack later when Logseq's responsive refresh is fixed.
|
||||||
|
;(logseq.Editor.scrollToBlockInPage as any)(item.blockUUID)
|
||||||
|
setTimeout(() => {
|
||||||
|
;(logseq.Editor.scrollToBlockInPage as any)(item.name)
|
||||||
|
}, 50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleChild(e: Event, itemName: string) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
const newChildrenData = produce(childrenData, (draft: any) => {
|
||||||
|
draft[itemName].expanded = !draft[itemName].expanded
|
||||||
|
})
|
||||||
|
setChildrenData(newChildrenData)
|
||||||
|
}
|
||||||
|
|
||||||
|
function preventSideEffect(e: Event) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={cls("kef-ae-fav-list", shown && "kef-ae-fav-expanded")}
|
||||||
|
onMouseDown={preventSideEffect}
|
||||||
|
>
|
||||||
|
{items.map((item) => {
|
||||||
|
const displayName = item.displayName ?? item["original-name"]
|
||||||
|
const data = item.filters
|
||||||
|
? childrenData?.[item.displayName]
|
||||||
|
: childrenData?.[item.name]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={item.name}>
|
||||||
|
<div class="kef-ae-fav-item" onClick={(e) => openPage(e, item)}>
|
||||||
|
{item.filters ? (
|
||||||
|
<div class="kef-ae-fav-item-icon">
|
||||||
|
{logseq.settings?.filterIcon ?? "🔎"}
|
||||||
|
</div>
|
||||||
|
) : item.properties?.icon ? (
|
||||||
|
<div class="kef-ae-fav-item-icon">{item.properties?.icon}</div>
|
||||||
|
) : (
|
||||||
|
<span class="ui__icon tie tie-page kef-ae-fav-item-icon"></span>
|
||||||
|
)}
|
||||||
|
<div class="kef-ae-fav-item-name" title={displayName}>
|
||||||
|
{item.filters &&
|
||||||
|
displayName.toLowerCase().startsWith(`${item.name}/`)
|
||||||
|
? displayName.substring(item.name.length + 1)
|
||||||
|
: displayName}
|
||||||
|
</div>
|
||||||
|
{data && (
|
||||||
|
<FavArrow
|
||||||
|
expanded={data.expanded}
|
||||||
|
onToggle={(e: Event) =>
|
||||||
|
item.filters
|
||||||
|
? toggleChild(e, item.displayName)
|
||||||
|
: toggleChild(e, item.name)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{data?.items?.length > 0 && (
|
||||||
|
<SubList items={data.items} shown={data.expanded} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
3
src/global.d.ts
vendored
Normal file
3
src/global.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
declare global {}
|
||||||
|
|
||||||
|
export {}
|
||||||
176
src/libs/utils.ts
Normal file
176
src/libs/utils.ts
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
import { partition } from "rambdax"
|
||||||
|
|
||||||
|
let language: string
|
||||||
|
|
||||||
|
export function setLanguage(val: string) {
|
||||||
|
language = val
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function hash(text: string) {
|
||||||
|
if (!text) return ""
|
||||||
|
|
||||||
|
const bytes = new TextEncoder().encode(text)
|
||||||
|
const hashedArray = Array.from(
|
||||||
|
new Uint8Array(await crypto.subtle.digest("SHA-1", bytes)),
|
||||||
|
)
|
||||||
|
const hashed = hashedArray
|
||||||
|
.map((b) => b.toString(16).padStart(2, "0"))
|
||||||
|
.join("")
|
||||||
|
return hashed
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function queryForSubItems(name: string) {
|
||||||
|
name = name.toLowerCase()
|
||||||
|
|
||||||
|
const namespaceChildren = (
|
||||||
|
await logseq.DB.datascriptQuery(
|
||||||
|
`[:find (pull ?p [:block/name :block/original-name :block/uuid :block/properties])
|
||||||
|
:in $ ?name
|
||||||
|
:where
|
||||||
|
[?t :block/name ?name]
|
||||||
|
[?p :block/namespace ?t]]`,
|
||||||
|
`"${name}"`,
|
||||||
|
)
|
||||||
|
).flat()
|
||||||
|
namespaceChildren.forEach((p: any) => {
|
||||||
|
const originalName = p["original-name"]
|
||||||
|
const trimStart = originalName.lastIndexOf("/")
|
||||||
|
p.displayName =
|
||||||
|
trimStart > -1 ? originalName.substring(trimStart + 1) : originalName
|
||||||
|
})
|
||||||
|
|
||||||
|
const hierarchyProperty = logseq.settings?.hierarchyProperty ?? "tags"
|
||||||
|
const taggedPages = (
|
||||||
|
await logseq.DB.datascriptQuery(
|
||||||
|
hierarchyProperty === "tags"
|
||||||
|
? `[:find (pull ?p [:block/name :block/original-name :block/uuid :block/properties])
|
||||||
|
:in $ ?name
|
||||||
|
:where
|
||||||
|
[?t :block/name ?name]
|
||||||
|
[?p :block/tags ?t]]`
|
||||||
|
: `[:find (pull ?p [:block/name :block/original-name :block/uuid :block/properties])
|
||||||
|
:in $ ?name
|
||||||
|
:where
|
||||||
|
[?p :block/original-name]
|
||||||
|
[?p :block/properties ?props]
|
||||||
|
[(get ?props :${hierarchyProperty}) ?v]
|
||||||
|
(or [(= ?v ?name)] [(contains? ?v ?name)])]`,
|
||||||
|
`"${name}"`,
|
||||||
|
)
|
||||||
|
).flat()
|
||||||
|
|
||||||
|
const quickFilters = await getQuickFilters(name)
|
||||||
|
|
||||||
|
if (
|
||||||
|
namespaceChildren.length === 0 &&
|
||||||
|
taggedPages.length === 0 &&
|
||||||
|
quickFilters.length === 0
|
||||||
|
)
|
||||||
|
return namespaceChildren
|
||||||
|
|
||||||
|
const list = namespaceChildren.concat(taggedPages).concat(quickFilters)
|
||||||
|
const [fixed, dynamic] = partition(
|
||||||
|
(p: any) => p.properties?.fixed != null,
|
||||||
|
list,
|
||||||
|
)
|
||||||
|
fixed.sort((a, b) => a.properties.fixed - b.properties.fixed)
|
||||||
|
dynamic.sort((a, b) =>
|
||||||
|
(a.displayName ?? a["original-name"]).localeCompare(
|
||||||
|
b.displayName ?? b["original-name"],
|
||||||
|
language,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
const result = fixed
|
||||||
|
.concat(dynamic)
|
||||||
|
.slice(0, logseq.settings?.taggedPageLimit ?? 30)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getQuickFilters(name: string) {
|
||||||
|
const [{ uuid: blockUUID }, { uuid: pageUUID }] = (
|
||||||
|
await logseq.DB.datascriptQuery(
|
||||||
|
`[:find (pull ?b [:block/uuid]) (pull ?p [:block/uuid])
|
||||||
|
:in $ ?name
|
||||||
|
:where
|
||||||
|
[?p :block/name ?name]
|
||||||
|
[?b :block/page ?p]
|
||||||
|
[?b :block/pre-block? true]]`,
|
||||||
|
`"${name}"`,
|
||||||
|
)
|
||||||
|
)[0] ?? [{}, {}]
|
||||||
|
if (blockUUID == null || pageUUID == null) return []
|
||||||
|
|
||||||
|
let quickFiltersStr
|
||||||
|
try {
|
||||||
|
quickFiltersStr = JSON.parse(
|
||||||
|
(await logseq.Editor.getBlockProperty(blockUUID, "quick-filters")) ??
|
||||||
|
'""',
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
if (!quickFiltersStr) return []
|
||||||
|
|
||||||
|
const groups = quickFiltersStr.match(/(?:\d+\s+)?(?:\[\[[^\]]+\]\]\s*)+/g)
|
||||||
|
if (groups == null) return []
|
||||||
|
|
||||||
|
const quickFilters = groups
|
||||||
|
.map((filterStr: string) => {
|
||||||
|
const matches = Array.from(
|
||||||
|
filterStr.matchAll(/\[\[([^\]]+)\]\]\s*|(\d+)/g),
|
||||||
|
)
|
||||||
|
const fixed = matches[0][2] ? +matches[0][2] : null
|
||||||
|
const tags = (fixed == null ? matches : matches.slice(1)).map((m) => m[1])
|
||||||
|
return [tags, fixed]
|
||||||
|
})
|
||||||
|
.filter(([tags, fixed]: any) => tags.length > 0)
|
||||||
|
.reduce((filter: any, [tags, fixed]: any) => {
|
||||||
|
if (filter[tags[0]] == null) {
|
||||||
|
filter[tags[0]] = {}
|
||||||
|
if (fixed != null) {
|
||||||
|
filter[tags[0]].properties = { fixed }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
constructFilter(filter[tags[0]], name, blockUUID, pageUUID, tags, [])
|
||||||
|
return filter
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
return Object.values(quickFilters)
|
||||||
|
}
|
||||||
|
|
||||||
|
function constructFilter(
|
||||||
|
obj: Record<string, any>,
|
||||||
|
name: string,
|
||||||
|
blockUUID: string,
|
||||||
|
pageUUID: string,
|
||||||
|
tags: string[],
|
||||||
|
path: string[],
|
||||||
|
) {
|
||||||
|
if (obj.displayName == null) {
|
||||||
|
obj.name = name
|
||||||
|
obj.blockUUID = blockUUID
|
||||||
|
obj.pageUUID = pageUUID
|
||||||
|
obj.displayName = tags[0]
|
||||||
|
obj.filters = [...path, tags[0]]
|
||||||
|
}
|
||||||
|
|
||||||
|
tags = tags.slice(1)
|
||||||
|
if (tags.length === 0) return
|
||||||
|
|
||||||
|
if (obj.subitems == null) {
|
||||||
|
obj.subitems = {}
|
||||||
|
}
|
||||||
|
if (obj.subitems[tags[0]] == null) {
|
||||||
|
obj.subitems[tags[0]] = {}
|
||||||
|
}
|
||||||
|
constructFilter(
|
||||||
|
obj.subitems[tags[0]],
|
||||||
|
name,
|
||||||
|
blockUUID,
|
||||||
|
pageUUID,
|
||||||
|
tags,
|
||||||
|
obj.filters,
|
||||||
|
)
|
||||||
|
}
|
||||||
307
src/plugin.tsx
Normal file
307
src/plugin.tsx
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
import "@logseq/libs"
|
||||||
|
import { setup, t } from "logseq-l10n"
|
||||||
|
import { render } from "preact"
|
||||||
|
import { throttle } from "rambdax"
|
||||||
|
import FavList from "./comps/FavList"
|
||||||
|
import { hash, queryForSubItems, setLanguage } from "./libs/utils"
|
||||||
|
import zhCN from "./translations/zh-CN.json"
|
||||||
|
|
||||||
|
let dragHandle: HTMLElement | null = null
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const { preferredLanguage: lang } = await logseq.App.getUserConfigs()
|
||||||
|
setLanguage(logseq.settings?.sortingLocale || lang)
|
||||||
|
|
||||||
|
await setup({ builtinTranslations: { "zh-CN": zhCN } })
|
||||||
|
|
||||||
|
provideStyles()
|
||||||
|
|
||||||
|
logseq.useSettingsSchema([
|
||||||
|
{
|
||||||
|
key: "hierarchyProperty",
|
||||||
|
title: "",
|
||||||
|
type: "string",
|
||||||
|
default: "tags",
|
||||||
|
description: t(
|
||||||
|
"It controls which property is used to decide a tag's hierarchy.",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "filterIcon",
|
||||||
|
title: "",
|
||||||
|
type: "string",
|
||||||
|
default: "🔍",
|
||||||
|
description: t("Define an icon for quick filters."),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "hoverArrow",
|
||||||
|
title: "",
|
||||||
|
type: "boolean",
|
||||||
|
default: false,
|
||||||
|
description: t("Show arrows only when hovered."),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "taggedPageLimit",
|
||||||
|
title: "",
|
||||||
|
type: "number",
|
||||||
|
default: 30,
|
||||||
|
description: t(
|
||||||
|
"Maximum number of tagged pages to display on each level for favorites.",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "sortingLocale",
|
||||||
|
title: "",
|
||||||
|
type: "string",
|
||||||
|
default: "",
|
||||||
|
description: t(
|
||||||
|
"Locale used in sorting hierarchical favorites. E.g, zh-CN. Keep it empty to use Logseq's language setting.",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const favoritesObserver = new MutationObserver(async (mutationList) => {
|
||||||
|
const mutation = mutationList[0]
|
||||||
|
if (
|
||||||
|
(mutation?.target as any).classList?.contains("bd") ||
|
||||||
|
(mutation?.target as any).classList?.contains("favorites")
|
||||||
|
) {
|
||||||
|
await processFavorites()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const favoritesEl = parent.document.querySelector("#left-sidebar .favorites")
|
||||||
|
if (favoritesEl != null) {
|
||||||
|
favoritesObserver.observe(favoritesEl, { childList: true, subtree: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const transactionOff = logseq.DB.onChanged(onTransaction)
|
||||||
|
|
||||||
|
await processFavorites()
|
||||||
|
|
||||||
|
const graph = (await logseq.App.getCurrentGraph())!
|
||||||
|
const storedWidth = parent.localStorage.getItem(`kef-ae-lsw-${graph.name}`)
|
||||||
|
if (storedWidth) {
|
||||||
|
parent.document.documentElement.style.setProperty(
|
||||||
|
"--ls-left-sidebar-width",
|
||||||
|
`${+storedWidth}px`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
logseq.provideUI({
|
||||||
|
key: "kef-ae-drag-handle",
|
||||||
|
path: "#left-sidebar",
|
||||||
|
template: `<div class="kef-ae-drag-handle"></div>`,
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
dragHandle = parent.document.querySelector(
|
||||||
|
"#left-sidebar .kef-ae-drag-handle",
|
||||||
|
)!
|
||||||
|
dragHandle.addEventListener("pointerdown", onPointerDown)
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
logseq.beforeunload(async () => {
|
||||||
|
transactionOff()
|
||||||
|
favoritesObserver.disconnect()
|
||||||
|
dragHandle?.removeEventListener("pointerdown", onPointerDown)
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log("#favorite-tree loaded")
|
||||||
|
}
|
||||||
|
|
||||||
|
function provideStyles() {
|
||||||
|
logseq.provideStyle({
|
||||||
|
key: "kef-ae-fav",
|
||||||
|
style: `
|
||||||
|
.kef-ae-fav-list {
|
||||||
|
padding-left: 24px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.kef-ae-fav-expanded {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.kef-ae-fav-arrow {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 4px 20px 4px 10px;
|
||||||
|
margin-right: -20px;
|
||||||
|
opacity: ${logseq.settings?.hoverArrow ? 0 : 1};
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
:is(.favorite-item, .recent-item):hover > a > .kef-ae-fav-arrow,
|
||||||
|
.kef-ae-fav-item:hover > .kef-ae-fav-arrow {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.kef-ae-fav-arrow svg {
|
||||||
|
transform: rotate(90deg) scale(0.8);
|
||||||
|
transition: transform 0.04s linear;
|
||||||
|
}
|
||||||
|
.kef-ae-fav-arrow-expanded svg {
|
||||||
|
transform: rotate(0deg) scale(0.8);
|
||||||
|
}
|
||||||
|
.kef-ae-fav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 24px;
|
||||||
|
line-height: 28px;
|
||||||
|
color: var(--ls-header-button-background);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.kef-ae-fav-item:hover {
|
||||||
|
background-color: var(--ls-quaternary-background-color);
|
||||||
|
}
|
||||||
|
.kef-ae-fav-item-icon {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-right: 5px;
|
||||||
|
width: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.kef-ae-fav-item-name {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kef-ae-drag-handle {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 4px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.kef-ae-drag-handle:hover,
|
||||||
|
.kef-ae-dragging .kef-ae-drag-handle {
|
||||||
|
cursor: col-resize;
|
||||||
|
background: var(--ls-active-primary-color);
|
||||||
|
}
|
||||||
|
.kef-ae-dragging {
|
||||||
|
cursor: col-resize;
|
||||||
|
}
|
||||||
|
.kef-ae-dragging :is(#left-sidebar, #main-content-container) {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processFavorites() {
|
||||||
|
const favorites = parent.document.querySelectorAll<HTMLElement>(
|
||||||
|
`#left-sidebar .favorite-item`,
|
||||||
|
)
|
||||||
|
for (const fav of favorites) {
|
||||||
|
const items = await queryForSubItems(fav.dataset.ref!)
|
||||||
|
if (items?.length > 0) {
|
||||||
|
injectList(fav, items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function injectList(el: HTMLElement, items: any[]) {
|
||||||
|
const isFav = el.classList.contains("favorite-item")
|
||||||
|
const key = `kef-ae-${isFav ? "f" : "r"}-${await hash(el.dataset.ref!)}`
|
||||||
|
|
||||||
|
const arrowContainer = el.querySelector("a")!
|
||||||
|
const arrow = arrowContainer.querySelector(".kef-ae-fav-arrow")
|
||||||
|
if (arrow != null) {
|
||||||
|
arrow.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent.document.getElementById(key) == null) {
|
||||||
|
logseq.provideUI({
|
||||||
|
key,
|
||||||
|
path: `.${isFav ? "favorite" : "recent"}-item[data-ref="${
|
||||||
|
el.dataset.ref
|
||||||
|
}"]`,
|
||||||
|
template: `<div id="${key}"></div>`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
renderList(key, items, arrowContainer)
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderList(key: string, items: any[], arrowContainer: HTMLElement) {
|
||||||
|
const el = parent.document.getElementById(key)!
|
||||||
|
render(<FavList items={items} arrowContainer={arrowContainer} />, el)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onTransaction({ blocks, txData, txMeta }: any) {
|
||||||
|
if (needsProcessing(txData)) {
|
||||||
|
await processFavorites()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function needsProcessing(txData: any[]) {
|
||||||
|
const hierarchyProperty = logseq.settings?.hierarchyProperty ?? "tags"
|
||||||
|
let oldProperty, newProperty
|
||||||
|
let oldQuickFilters, newQuickFilters
|
||||||
|
for (const [_e, attr, val, _tx, added] of txData) {
|
||||||
|
if (attr === "originalName") return true
|
||||||
|
if (hierarchyProperty === "tags" && attr === "tags") return true
|
||||||
|
if (attr === "properties") {
|
||||||
|
if (val[hierarchyProperty]) {
|
||||||
|
if (added) {
|
||||||
|
newProperty = val[hierarchyProperty]
|
||||||
|
} else {
|
||||||
|
oldProperty = val[hierarchyProperty]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (val.quickFilters) {
|
||||||
|
if (added) {
|
||||||
|
newQuickFilters = val.quickFilters
|
||||||
|
} else {
|
||||||
|
oldQuickFilters = val.quickFilters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(!oldProperty && !newProperty && !oldQuickFilters && !newQuickFilters) ||
|
||||||
|
(oldProperty?.toString() === newProperty?.toString() &&
|
||||||
|
oldQuickFilters === newQuickFilters)
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerDown(e: Event) {
|
||||||
|
e.preventDefault()
|
||||||
|
parent.document.documentElement.classList.add("kef-ae-dragging")
|
||||||
|
parent.document.addEventListener("pointermove", onPointerMove)
|
||||||
|
parent.document.addEventListener("pointerup", onPointerUp)
|
||||||
|
parent.document.addEventListener("pointercancel", onPointerUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerUp(e: MouseEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
parent.document.removeEventListener("pointermove", onPointerMove)
|
||||||
|
parent.document.removeEventListener("pointerup", onPointerUp)
|
||||||
|
parent.document.removeEventListener("pointercancel", onPointerUp)
|
||||||
|
parent.document.documentElement.classList.remove("kef-ae-dragging")
|
||||||
|
|
||||||
|
const pos = e.clientX
|
||||||
|
parent.document.documentElement.style.setProperty(
|
||||||
|
"--ls-left-sidebar-width",
|
||||||
|
`${pos}px`,
|
||||||
|
)
|
||||||
|
;(async () => {
|
||||||
|
const graph = (await logseq.App.getCurrentGraph())!
|
||||||
|
parent.localStorage.setItem(`kef-ae-lsw-${graph.name}`, `${pos}`)
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerMove(e: MouseEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
move(e.clientX)
|
||||||
|
}
|
||||||
|
|
||||||
|
const move = throttle((pos) => {
|
||||||
|
parent.document.documentElement.style.setProperty(
|
||||||
|
"--ls-left-sidebar-width",
|
||||||
|
`${pos}px`,
|
||||||
|
)
|
||||||
|
}, 12)
|
||||||
|
|
||||||
|
logseq.ready(main).catch(console.error)
|
||||||
1
src/preact.d.ts
vendored
Normal file
1
src/preact.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
import JSX = preact.JSX
|
||||||
7
src/translations/zh-CN.json
Normal file
7
src/translations/zh-CN.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"It controls which property is used to decide a tag's hierarchy.": "使用哪个属性作为判断标签结构的依据。",
|
||||||
|
"Define an icon for quick filters.": "给快速过滤器设置一个图标。",
|
||||||
|
"Show arrows only when hovered.": "仅在鼠标移上去时显示箭头。",
|
||||||
|
"Maximum number of tagged pages to display on each level for favorites.": "收藏中每级显示的标记页面的最大数量。",
|
||||||
|
"Locale used in sorting hierarchical favorites. E.g, zh-CN. Keep it empty to use Logseq's language setting.": "在结构化收藏排序中所使用到的locale。例如 zh-CN。留空会使用Logseq的语言设置。"
|
||||||
|
}
|
||||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
33
tsconfig.json
Normal file
33
tsconfig.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": [
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable",
|
||||||
|
"ESNext"
|
||||||
|
],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxFactory": "h",
|
||||||
|
"jsxFragmentFactory": "Fragment",
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
9
tsconfig.node.json
Normal file
9
tsconfig.node.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
29
vite.config.ts
Normal file
29
vite.config.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import preact from "@preact/preset-vite"
|
||||||
|
import { resolve } from "path"
|
||||||
|
import { defineConfig } from "vite"
|
||||||
|
import logseqPlugin from "vite-plugin-logseq"
|
||||||
|
|
||||||
|
const PORT = 3003
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig(({ mode }) => {
|
||||||
|
return {
|
||||||
|
server: {
|
||||||
|
strictPort: true,
|
||||||
|
port: PORT,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
cssCodeSplit: false,
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
plugin: resolve(__dirname, "index.html"),
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
entryFileNames: "[name]-[hash].js",
|
||||||
|
assetFileNames: "[name][extname]",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [preact(), logseqPlugin()],
|
||||||
|
}
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user