chore: initial commit

This commit is contained in:
Seth 2023-08-19 10:25:15 +08:00
commit 39eda568e9
20 changed files with 1994 additions and 0 deletions

67
.github/workflows/main.yml vendored Normal file
View 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
View 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
View File

@ -0,0 +1,7 @@
{
"arrowParens": "always",
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"trailingComma": "all"
}

21
LICENSE Normal file
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

26
src/comps/FavArrow.tsx Normal file
View 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
View 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
View File

@ -0,0 +1,3 @@
declare global {}
export {}

176
src/libs/utils.ts Normal file
View 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
View 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
View File

@ -0,0 +1 @@
import JSX = preact.JSX

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

@ -0,0 +1 @@
/// <reference types="vite/client" />

33
tsconfig.json Normal file
View 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
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

29
vite.config.ts Normal file
View 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()],
}
})