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