@hanology/cham-browser 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pipeline.d.ts +2 -14
- package/dist/pipeline.js +1 -376
- package/dist/pipeline.js.map +1 -1
- package/package.json +4 -2
- package/template/__tests__/annotationRenderer.test.ts +140 -0
- package/template/__tests__/annotationTooltip.test.ts +176 -0
- package/template/src/components/AnnotationControlBar.vue +124 -0
- package/template/src/components/AnnotationTooltip.vue +42 -31
- package/template/src/components/ReadingToolbar.vue +18 -4
- package/template/src/components/SideNav.vue +20 -6
- package/template/src/composables/useAnnotationInteraction.ts +60 -0
- package/template/src/composables/useI18n.ts +174 -0
- package/template/src/views/AboutView.vue +173 -0
- package/template/src/views/PieceView.vue +67 -78
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { ref, computed, watch } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type Locale = 'zh-Hant' | 'zh-Hans' | 'en'
|
|
4
|
+
|
|
5
|
+
export const LOCALE_LABELS: Record<Locale, string> = {
|
|
6
|
+
'zh-Hant': '繁',
|
|
7
|
+
'zh-Hans': '简',
|
|
8
|
+
'en': 'EN',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const messages: Record<Locale, Record<string, string>> = {
|
|
12
|
+
'zh-Hant': {
|
|
13
|
+
'site.title': '古典詩文圖書館',
|
|
14
|
+
'site.subtitle': 'Classical Chinese Text Library',
|
|
15
|
+
'nav.back': '返回',
|
|
16
|
+
'nav.home': '首頁',
|
|
17
|
+
'settings.layout': '版面',
|
|
18
|
+
'settings.vertical': '直排',
|
|
19
|
+
'settings.horizontal': '橫排',
|
|
20
|
+
'settings.theme': '主題',
|
|
21
|
+
'settings.language': '語言',
|
|
22
|
+
'settings.mainFontSize': '正文字號',
|
|
23
|
+
'settings.bodyFontSize': '內文字號',
|
|
24
|
+
'settings.reading': '閱讀設定',
|
|
25
|
+
'settings.close': '關閉設定',
|
|
26
|
+
'piece.stanzas': '段',
|
|
27
|
+
'piece.notes': '注',
|
|
28
|
+
'piece.noNotes': '無注',
|
|
29
|
+
'piece.previous': '上一篇',
|
|
30
|
+
'piece.next': '下一篇',
|
|
31
|
+
'piece.enterLibrary': '進 入 文 庫',
|
|
32
|
+
'piece.loading': '載入中…',
|
|
33
|
+
'catalog.title': '篇 章 目 錄',
|
|
34
|
+
'catalog.total': '共 {count} 篇',
|
|
35
|
+
'catalog.search': '搜索詩題、作者…',
|
|
36
|
+
'author.biography': '作者簡介',
|
|
37
|
+
'author.collectedWorks': '收錄作品',
|
|
38
|
+
'author.worksCount': '{count} 篇收錄作品',
|
|
39
|
+
'author.unknownDynasty': '未知朝代',
|
|
40
|
+
'annotation.pronunciation': '音',
|
|
41
|
+
'annotation.semantic': '義',
|
|
42
|
+
'annotation.notes': '注釋',
|
|
43
|
+
'genre.poetry': '詩歌',
|
|
44
|
+
'genre.prose': '散文',
|
|
45
|
+
'genre.mixed': '綜合',
|
|
46
|
+
'genre.drama': '戲曲',
|
|
47
|
+
'genre.classicalText': '古典文本',
|
|
48
|
+
'genre.fourTreasuries': '四庫全書',
|
|
49
|
+
'genre.textbooks': '教材',
|
|
50
|
+
'stat.books': '部',
|
|
51
|
+
'stat.pieces': '篇',
|
|
52
|
+
'stat.pieceCount': '{count} 篇',
|
|
53
|
+
'stat.authorCount': '{count} 位作者',
|
|
54
|
+
'stat.piecePoems': '篇詩文',
|
|
55
|
+
},
|
|
56
|
+
'zh-Hans': {
|
|
57
|
+
'site.title': '古典诗文图书馆',
|
|
58
|
+
'site.subtitle': 'Classical Chinese Text Library',
|
|
59
|
+
'nav.back': '返回',
|
|
60
|
+
'nav.home': '首页',
|
|
61
|
+
'settings.layout': '版面',
|
|
62
|
+
'settings.vertical': '直排',
|
|
63
|
+
'settings.horizontal': '横排',
|
|
64
|
+
'settings.theme': '主题',
|
|
65
|
+
'settings.language': '语言',
|
|
66
|
+
'settings.mainFontSize': '正文字号',
|
|
67
|
+
'settings.bodyFontSize': '内文字号',
|
|
68
|
+
'settings.reading': '阅读设定',
|
|
69
|
+
'settings.close': '关闭设定',
|
|
70
|
+
'piece.stanzas': '段',
|
|
71
|
+
'piece.notes': '注',
|
|
72
|
+
'piece.noNotes': '无注',
|
|
73
|
+
'piece.previous': '上一篇',
|
|
74
|
+
'piece.next': '下一篇',
|
|
75
|
+
'piece.enterLibrary': '进 入 文 库',
|
|
76
|
+
'piece.loading': '载入中…',
|
|
77
|
+
'catalog.title': '篇 章 目 录',
|
|
78
|
+
'catalog.total': '共 {count} 篇',
|
|
79
|
+
'catalog.search': '搜索诗题、作者…',
|
|
80
|
+
'author.biography': '作者简介',
|
|
81
|
+
'author.collectedWorks': '收录作品',
|
|
82
|
+
'author.worksCount': '{count} 篇收录作品',
|
|
83
|
+
'author.unknownDynasty': '未知朝代',
|
|
84
|
+
'annotation.pronunciation': '音',
|
|
85
|
+
'annotation.semantic': '义',
|
|
86
|
+
'annotation.notes': '注释',
|
|
87
|
+
'genre.poetry': '诗歌',
|
|
88
|
+
'genre.prose': '散文',
|
|
89
|
+
'genre.mixed': '综合',
|
|
90
|
+
'genre.drama': '戏曲',
|
|
91
|
+
'genre.classicalText': '古典文本',
|
|
92
|
+
'genre.fourTreasuries': '四库全书',
|
|
93
|
+
'genre.textbooks': '教材',
|
|
94
|
+
'stat.books': '部',
|
|
95
|
+
'stat.pieces': '篇',
|
|
96
|
+
'stat.pieceCount': '{count} 篇',
|
|
97
|
+
'stat.authorCount': '{count} 位作者',
|
|
98
|
+
'stat.piecePoems': '篇诗文',
|
|
99
|
+
},
|
|
100
|
+
'en': {
|
|
101
|
+
'site.title': 'Classical Chinese Text Library',
|
|
102
|
+
'site.subtitle': 'Classical Chinese Text Library',
|
|
103
|
+
'nav.back': 'Back',
|
|
104
|
+
'nav.home': 'Home',
|
|
105
|
+
'settings.layout': 'Layout',
|
|
106
|
+
'settings.vertical': 'Vertical',
|
|
107
|
+
'settings.horizontal': 'Horizontal',
|
|
108
|
+
'settings.theme': 'Theme',
|
|
109
|
+
'settings.language': 'Language',
|
|
110
|
+
'settings.mainFontSize': 'Main Size',
|
|
111
|
+
'settings.bodyFontSize': 'Body Size',
|
|
112
|
+
'settings.reading': 'Reading Settings',
|
|
113
|
+
'settings.close': 'Close Settings',
|
|
114
|
+
'piece.stanzas': 'stanzas',
|
|
115
|
+
'piece.notes': 'notes',
|
|
116
|
+
'piece.noNotes': 'No notes',
|
|
117
|
+
'piece.previous': 'Previous',
|
|
118
|
+
'piece.next': 'Next',
|
|
119
|
+
'piece.enterLibrary': 'Enter Library',
|
|
120
|
+
'piece.loading': 'Loading…',
|
|
121
|
+
'catalog.title': 'Catalogue',
|
|
122
|
+
'catalog.total': '{count} works',
|
|
123
|
+
'catalog.search': 'Search titles, authors…',
|
|
124
|
+
'author.biography': 'Biography',
|
|
125
|
+
'author.collectedWorks': 'Collected Works',
|
|
126
|
+
'author.worksCount': '{count} collected works',
|
|
127
|
+
'author.unknownDynasty': 'Unknown dynasty',
|
|
128
|
+
'annotation.pronunciation': 'Pron',
|
|
129
|
+
'annotation.semantic': 'Def',
|
|
130
|
+
'annotation.notes': 'Notes',
|
|
131
|
+
'genre.poetry': 'Poetry',
|
|
132
|
+
'genre.prose': 'Prose',
|
|
133
|
+
'genre.mixed': 'Mixed',
|
|
134
|
+
'genre.drama': 'Drama',
|
|
135
|
+
'genre.classicalText': 'Classical Texts',
|
|
136
|
+
'genre.fourTreasuries': 'Four Treasuries',
|
|
137
|
+
'genre.textbooks': 'Textbooks',
|
|
138
|
+
'stat.books': 'books',
|
|
139
|
+
'stat.pieces': 'works',
|
|
140
|
+
'stat.pieceCount': '{count} works',
|
|
141
|
+
'stat.authorCount': '{count} authors',
|
|
142
|
+
'stat.piecePoems': 'works',
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const AVAILABLE_LOCALES: Locale[] = ['zh-Hant', 'zh-Hans', 'en']
|
|
147
|
+
|
|
148
|
+
const locale = ref<Locale>('zh-Hant')
|
|
149
|
+
|
|
150
|
+
if (!import.meta.env.SSR) {
|
|
151
|
+
const saved = localStorage.getItem('cham-locale') as Locale | null
|
|
152
|
+
if (saved && AVAILABLE_LOCALES.includes(saved)) locale.value = saved
|
|
153
|
+
|
|
154
|
+
watch(locale, l => {
|
|
155
|
+
localStorage.setItem('cham-locale', l)
|
|
156
|
+
document.documentElement.setAttribute('lang', l === 'zh-Hans' ? 'zh-Hans' : l === 'en' ? 'en' : 'zh-Hant')
|
|
157
|
+
}, { immediate: true })
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function useI18n() {
|
|
161
|
+
function t(key: string, params?: Record<string, string | number>): string {
|
|
162
|
+
let msg = messages[locale.value]?.[key] || messages['zh-Hant']?.[key] || key
|
|
163
|
+
if (params) {
|
|
164
|
+
for (const [k, v] of Object.entries(params)) {
|
|
165
|
+
msg = msg.replace(`{${k}}`, String(v))
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return msg
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function setLocale(l: Locale) { locale.value = l }
|
|
172
|
+
|
|
173
|
+
return { locale, t, setLocale, availableLocales: AVAILABLE_LOCALES, localeLabels: LOCALE_LABELS }
|
|
174
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useI18n } from '../composables/useI18n'
|
|
3
|
+
import { useTitle } from '../composables/useTitle'
|
|
4
|
+
import { useReadingMode } from '../composables/useReadingMode'
|
|
5
|
+
import { useHorizontalScroll } from '../composables/useHorizontalScroll'
|
|
6
|
+
import SideNav from '../components/SideNav.vue'
|
|
7
|
+
import ReadingToolbar from '../components/ReadingToolbar.vue'
|
|
8
|
+
import { ref, computed } from 'vue'
|
|
9
|
+
import { useRouter } from 'vue-router'
|
|
10
|
+
|
|
11
|
+
const { t, locale } = useI18n()
|
|
12
|
+
const { layout } = useReadingMode()
|
|
13
|
+
const isVertical = computed(() => layout.value === 'vertical')
|
|
14
|
+
const vPageRef = ref<HTMLElement | null>(null)
|
|
15
|
+
const vScroll = useHorizontalScroll(vPageRef)
|
|
16
|
+
const router = useRouter()
|
|
17
|
+
|
|
18
|
+
useTitle(locale.value === 'en' ? 'About — Hanology' : '關於 — 漢流')
|
|
19
|
+
|
|
20
|
+
function goBack() { router.push('/') }
|
|
21
|
+
function goHome() { router.push('/') }
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<div v-if="isVertical" class="v-root">
|
|
26
|
+
<SideNav @back="goBack" @home="goHome" />
|
|
27
|
+
<div ref="vPageRef" class="v-page">
|
|
28
|
+
<section class="v-about">
|
|
29
|
+
<h1 class="v-about-title">關 於 漢 流</h1>
|
|
30
|
+
<div class="v-divider"></div>
|
|
31
|
+
<div class="v-about-body">
|
|
32
|
+
<p><strong>漢流</strong>,粵音 Han-Lou,普音 Han-Liu,意為「漢學之流」。</p>
|
|
33
|
+
<p>經典如水,源遠流長,世代浸潤其中,前人開源,後人受益。每一代人都能在經典的長河中,找到屬於自己的領悟。</p>
|
|
34
|
+
<p><strong>Hanology</strong>,English portmanteau of <em>Han</em> + <em>anthology</em> + <em>-logy</em> (the study of). The sound "lou/liu" echoes the first syllable of "-logy" — a perfect phonetic and semantic fit.</p>
|
|
35
|
+
<p>Hanology is a digital library for classical Chinese texts, designed to make the wisdom of the ages accessible in the modern world.</p>
|
|
36
|
+
</div>
|
|
37
|
+
</section>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div v-else class="h-root">
|
|
42
|
+
<header class="h-header">
|
|
43
|
+
<button class="h-back" @click="goBack">← {{ t('nav.back') }}</button>
|
|
44
|
+
<h1 class="h-page-title">關於漢流 / About Hanology</h1>
|
|
45
|
+
</header>
|
|
46
|
+
<div class="h-content">
|
|
47
|
+
<div class="h-seal">漢流</div>
|
|
48
|
+
<div class="h-about-block">
|
|
49
|
+
<h2>漢流 · Hanology</h2>
|
|
50
|
+
<p><strong>漢流</strong>,粵音 Han-Lou,普音 Han-Liu,意為「漢學之流」。</p>
|
|
51
|
+
<p>經典如水,源遠流長,世代浸潤其中,前人開源,後人受益。每一代人都能在經典的長河中,找到屬於自己的領悟。</p>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="h-about-block">
|
|
54
|
+
<h2>Hanology</h2>
|
|
55
|
+
<p>English portmanteau of <em>Han</em> + <em>anthology</em> + <em>-logy</em> (the study of).</p>
|
|
56
|
+
<p>The sound "lou/liu" echoes the first syllable of "-logy" — a perfect phonetic and semantic fit. Just as the classics flow through generations, Hanology aims to carry that stream into the digital age.</p>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="h-about-block">
|
|
59
|
+
<h2>Our Mission</h2>
|
|
60
|
+
<p>Hanology is a digital library for classical Chinese texts. We believe that the wisdom of antiquity should not be locked behind impenetrable editions or forgotten in dusty shelves. By combining rigorous scholarship with thoughtful design, we make the classics accessible, beautiful, and alive for every reader.</p>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
<ReadingToolbar />
|
|
64
|
+
</div>
|
|
65
|
+
</template>
|
|
66
|
+
|
|
67
|
+
<style scoped>
|
|
68
|
+
/* ═══════ 直排模式 ═══════ */
|
|
69
|
+
.v-page {
|
|
70
|
+
height: 100vh;
|
|
71
|
+
display: flex;
|
|
72
|
+
flex-direction: row-reverse;
|
|
73
|
+
overflow-x: auto;
|
|
74
|
+
overflow-y: hidden;
|
|
75
|
+
margin-right: var(--nav-width, 56px);
|
|
76
|
+
padding: 0 32px;
|
|
77
|
+
background: var(--paper);
|
|
78
|
+
scrollbar-width: thin;
|
|
79
|
+
scrollbar-color: var(--gold) transparent;
|
|
80
|
+
}
|
|
81
|
+
.v-page::-webkit-scrollbar { height: 4px; }
|
|
82
|
+
.v-page::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 2px; }
|
|
83
|
+
|
|
84
|
+
.v-about {
|
|
85
|
+
writing-mode: vertical-rl;
|
|
86
|
+
text-orientation: mixed;
|
|
87
|
+
flex-shrink: 0;
|
|
88
|
+
height: 100vh;
|
|
89
|
+
display: flex;
|
|
90
|
+
flex-direction: column;
|
|
91
|
+
align-items: flex-start;
|
|
92
|
+
justify-content: center;
|
|
93
|
+
padding: 40px 24px;
|
|
94
|
+
}
|
|
95
|
+
.v-about-title {
|
|
96
|
+
font-size: 48px; font-weight: 900;
|
|
97
|
+
letter-spacing: 16px; color: var(--ink);
|
|
98
|
+
margin-left: 20px; padding-left: 20px;
|
|
99
|
+
border-left: 4px solid var(--vermillion);
|
|
100
|
+
line-height: 1.6;
|
|
101
|
+
}
|
|
102
|
+
.v-divider {
|
|
103
|
+
width: 2px; height: 80px;
|
|
104
|
+
background: linear-gradient(180deg, transparent, var(--gold), transparent);
|
|
105
|
+
margin-left: 20px;
|
|
106
|
+
}
|
|
107
|
+
.v-about-body {
|
|
108
|
+
font-size: 16px; line-height: 2.4;
|
|
109
|
+
color: var(--ink-mid);
|
|
110
|
+
max-height: 80vh;
|
|
111
|
+
overflow-x: auto;
|
|
112
|
+
}
|
|
113
|
+
.v-about-body p {
|
|
114
|
+
margin-left: 16px;
|
|
115
|
+
text-indent: 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* ═══════ 橫排模式 ═══════ */
|
|
119
|
+
.h-root { max-width: 960px; margin: 0 auto; padding: 40px 24px 120px; }
|
|
120
|
+
.h-header {
|
|
121
|
+
display: flex; align-items: center; gap: 16px;
|
|
122
|
+
margin-bottom: 40px; padding-bottom: 16px;
|
|
123
|
+
border-bottom: 1px solid var(--border);
|
|
124
|
+
}
|
|
125
|
+
.h-back {
|
|
126
|
+
padding: 6px 16px; border: 1px solid var(--border);
|
|
127
|
+
border-radius: 2px; background: none;
|
|
128
|
+
font-family: var(--sans); font-size: 13px;
|
|
129
|
+
color: var(--ink-mid); cursor: pointer;
|
|
130
|
+
transition: all 0.2s;
|
|
131
|
+
}
|
|
132
|
+
.h-back:hover { background: var(--ink); color: var(--paper); border-color: var(--ink); }
|
|
133
|
+
.h-page-title { font-size: 20px; font-weight: 700; letter-spacing: 2px; }
|
|
134
|
+
|
|
135
|
+
.h-content { max-width: 680px; margin: 0 auto; }
|
|
136
|
+
.h-seal {
|
|
137
|
+
writing-mode: vertical-rl;
|
|
138
|
+
text-orientation: upright;
|
|
139
|
+
display: inline-flex;
|
|
140
|
+
align-items: center; justify-content: center;
|
|
141
|
+
width: 56px; height: 72px;
|
|
142
|
+
border: 2px solid var(--vermillion);
|
|
143
|
+
color: var(--vermillion);
|
|
144
|
+
font-size: 24px; font-family: var(--serif);
|
|
145
|
+
font-weight: 900; letter-spacing: 2px;
|
|
146
|
+
margin: 0 auto 40px; border-radius: 4px;
|
|
147
|
+
line-height: 1;
|
|
148
|
+
}
|
|
149
|
+
.h-about-block {
|
|
150
|
+
margin-bottom: 40px; padding: 32px;
|
|
151
|
+
background: var(--surface);
|
|
152
|
+
border: 1px solid var(--border-light);
|
|
153
|
+
border-radius: 8px;
|
|
154
|
+
}
|
|
155
|
+
.h-about-block:last-child { margin-bottom: 0; }
|
|
156
|
+
.h-about-block h2 {
|
|
157
|
+
font-size: 20px; font-weight: 700;
|
|
158
|
+
letter-spacing: 3px; color: var(--ink);
|
|
159
|
+
margin-bottom: 16px; padding-bottom: 12px;
|
|
160
|
+
border-bottom: 1px solid var(--border);
|
|
161
|
+
}
|
|
162
|
+
.h-about-block p {
|
|
163
|
+
font-size: 16px; line-height: 2.2;
|
|
164
|
+
color: var(--ink-mid); text-align: justify;
|
|
165
|
+
text-indent: 2em; margin-bottom: 12px;
|
|
166
|
+
}
|
|
167
|
+
.h-about-block p:last-child { margin-bottom: 0; }
|
|
168
|
+
|
|
169
|
+
@media (max-width: 768px) {
|
|
170
|
+
.h-root { padding: 24px 16px 80px; }
|
|
171
|
+
.h-about-block { padding: 20px; }
|
|
172
|
+
}
|
|
173
|
+
</style>
|
|
@@ -5,13 +5,13 @@ import { useBook } from '../composables/useBook'
|
|
|
5
5
|
import { useTitle } from '../composables/useTitle'
|
|
6
6
|
import { useReadingMode } from '../composables/useReadingMode'
|
|
7
7
|
import { useHorizontalScroll } from '../composables/useHorizontalScroll'
|
|
8
|
-
import {
|
|
8
|
+
import { useAnnotationInteraction } from '../composables/useAnnotationInteraction'
|
|
9
9
|
import { useData } from '../composables/useData'
|
|
10
10
|
import VerticalScroll from '../components/VerticalScroll.vue'
|
|
11
11
|
import HorizontalDisplay from '../components/HorizontalDisplay.vue'
|
|
12
12
|
import SectionBlock from '../components/SectionBlock.vue'
|
|
13
13
|
import AnnotationTooltip from '../components/AnnotationTooltip.vue'
|
|
14
|
-
import
|
|
14
|
+
import AnnotationControlBar from '../components/AnnotationControlBar.vue'
|
|
15
15
|
import SideNav from '../components/SideNav.vue'
|
|
16
16
|
import type { Piece, Annotation, AnnotationLayer } from '../types'
|
|
17
17
|
|
|
@@ -26,7 +26,7 @@ const vScroll = useHorizontalScroll(vPageRef)
|
|
|
26
26
|
|
|
27
27
|
const authorPaneOpen = ref(false)
|
|
28
28
|
const selectedAuthorId = ref('')
|
|
29
|
-
const
|
|
29
|
+
const interaction = useAnnotationInteraction()
|
|
30
30
|
const titleCollapsed = ref(false)
|
|
31
31
|
const vTitleRef = ref<HTMLElement | null>(null)
|
|
32
32
|
|
|
@@ -61,6 +61,7 @@ const isVertical = computed(() => layout.value === 'vertical')
|
|
|
61
61
|
const annotationLayers = computed<AnnotationLayer[]>(() => piece.value?.annotationLayers || [])
|
|
62
62
|
const hasLayers = computed(() => annotationLayers.value.length > 1)
|
|
63
63
|
const activeLayerIds = ref<string[]>([])
|
|
64
|
+
const annotationsVisible = ref(true)
|
|
64
65
|
|
|
65
66
|
function initLayers() {
|
|
66
67
|
if (hasLayers.value && activeLayerIds.value.length === 0) {
|
|
@@ -94,7 +95,7 @@ const layerLabels = computed(() => {
|
|
|
94
95
|
})
|
|
95
96
|
|
|
96
97
|
const layerAnnotationBlocks = computed(() => {
|
|
97
|
-
if (!hasLayers.value) return []
|
|
98
|
+
if (!hasLayers.value || !annotationsVisible.value) return []
|
|
98
99
|
const result: { label: string; text: string }[] = []
|
|
99
100
|
const activeLayers = annotationLayers.value.filter(l => activeLayerIds.value.includes(l.id) && l.id !== 'default')
|
|
100
101
|
for (const layer of activeLayers) {
|
|
@@ -152,33 +153,7 @@ const proseSections = computed(() => {
|
|
|
152
153
|
return result
|
|
153
154
|
})
|
|
154
155
|
|
|
155
|
-
let hideTimer: ReturnType<typeof setTimeout> | null = null
|
|
156
|
-
function cancelHide() {
|
|
157
|
-
if (hideTimer) { clearTimeout(hideTimer); hideTimer = null }
|
|
158
|
-
}
|
|
159
|
-
function scheduleHide(delay = 150) {
|
|
160
|
-
cancelHide()
|
|
161
|
-
hideTimer = setTimeout(() => { tooltip.hide(); hideTimer = null }, delay)
|
|
162
|
-
}
|
|
163
156
|
|
|
164
|
-
function handleAnnotationHover(event: MouseEvent, annotations: Annotation[]) {
|
|
165
|
-
cancelHide()
|
|
166
|
-
tooltip.show(event, annotations)
|
|
167
|
-
}
|
|
168
|
-
function handleAnnotationLeave() {
|
|
169
|
-
if (window.innerWidth >= 768) scheduleHide()
|
|
170
|
-
}
|
|
171
|
-
function handleAnnotationTap(event: MouseEvent, annotations: Annotation[]) {
|
|
172
|
-
cancelHide()
|
|
173
|
-
tooltip.toggle(event, annotations)
|
|
174
|
-
}
|
|
175
|
-
function handleTooltipEnter() {
|
|
176
|
-
cancelHide()
|
|
177
|
-
}
|
|
178
|
-
function handleTooltipLeave() {
|
|
179
|
-
if (window.innerWidth >= 768) scheduleHide()
|
|
180
|
-
}
|
|
181
|
-
function dismissTooltip() { cancelHide(); tooltip.hide() }
|
|
182
157
|
const { getAuthor, loadShared } = useData()
|
|
183
158
|
await loadShared()
|
|
184
159
|
|
|
@@ -269,41 +244,54 @@ function tcy(n: number): string {
|
|
|
269
244
|
:verses="piece.verses"
|
|
270
245
|
:author-initial="piece.author?.charAt(0) || '詩'"
|
|
271
246
|
:annotations="mergedAnnotations"
|
|
272
|
-
@annotation-hover="
|
|
273
|
-
@annotation-leave="
|
|
274
|
-
@annotation-tap="
|
|
247
|
+
@annotation-hover="interaction.onHover"
|
|
248
|
+
@annotation-leave="interaction.onLeave"
|
|
249
|
+
@annotation-tap="interaction.onTap"
|
|
275
250
|
@open-author="openAuthorPane"
|
|
276
251
|
/>
|
|
277
252
|
</section>
|
|
278
253
|
|
|
279
|
-
<
|
|
254
|
+
<SectionBlock
|
|
255
|
+
v-if="annotationsVisible && piece.sections.annotations"
|
|
256
|
+
num=""
|
|
257
|
+
label="注釋"
|
|
258
|
+
:special="false"
|
|
259
|
+
:text="piece.sections.annotations"
|
|
260
|
+
:is-annotations="true"
|
|
261
|
+
:vertical="true"
|
|
262
|
+
class="v-section"
|
|
263
|
+
/>
|
|
264
|
+
<template v-if="hasLayers">
|
|
265
|
+
<div class="v-layers-inline v-section">
|
|
266
|
+
<AnnotationControlBar
|
|
267
|
+
:layers="annotationLayers"
|
|
268
|
+
:has-annotations="piece.annotations.length > 0"
|
|
269
|
+
v-model:active-ids="activeLayerIds"
|
|
270
|
+
v-model:annotations-visible="annotationsVisible"
|
|
271
|
+
/>
|
|
272
|
+
</div>
|
|
280
273
|
<SectionBlock
|
|
274
|
+
v-for="block in visibleLayerBlocks"
|
|
275
|
+
:key="block.label"
|
|
281
276
|
num=""
|
|
282
|
-
label="
|
|
277
|
+
:label="block.label"
|
|
283
278
|
:special="false"
|
|
284
|
-
:text="
|
|
279
|
+
:text="block.text"
|
|
285
280
|
:is-annotations="true"
|
|
286
281
|
:vertical="true"
|
|
282
|
+
class="v-section"
|
|
287
283
|
/>
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
:label="block.label"
|
|
300
|
-
:special="false"
|
|
301
|
-
:text="block.text"
|
|
302
|
-
:is-annotations="true"
|
|
303
|
-
:vertical="true"
|
|
304
|
-
/>
|
|
305
|
-
</template>
|
|
306
|
-
</div>
|
|
284
|
+
</template>
|
|
285
|
+
<SectionBlock
|
|
286
|
+
v-else-if="piece.annotations.length > 0"
|
|
287
|
+
num=""
|
|
288
|
+
label="注釋"
|
|
289
|
+
:special="false"
|
|
290
|
+
:text="piece.sections.annotations || ''"
|
|
291
|
+
:is-annotations="true"
|
|
292
|
+
:vertical="true"
|
|
293
|
+
class="v-section"
|
|
294
|
+
/>
|
|
307
295
|
|
|
308
296
|
<SectionBlock
|
|
309
297
|
v-for="(sec, idx) in proseSections"
|
|
@@ -333,13 +321,13 @@ function tcy(n: number): string {
|
|
|
333
321
|
</div>
|
|
334
322
|
|
|
335
323
|
<AnnotationTooltip
|
|
336
|
-
:visible="
|
|
337
|
-
:annotations="
|
|
324
|
+
:visible="interaction.visible"
|
|
325
|
+
:annotations="interaction.items"
|
|
338
326
|
:layer-labels="layerLabels"
|
|
339
|
-
:style="
|
|
340
|
-
@close="
|
|
341
|
-
@tooltip-enter="
|
|
342
|
-
@tooltip-leave="
|
|
327
|
+
:style="interaction.style"
|
|
328
|
+
@close="interaction.dismiss"
|
|
329
|
+
@tooltip-enter="interaction.onTooltipEnter"
|
|
330
|
+
@tooltip-leave="interaction.onTooltipLeave"
|
|
343
331
|
/>
|
|
344
332
|
|
|
345
333
|
<Teleport to="body">
|
|
@@ -395,16 +383,23 @@ function tcy(n: number): string {
|
|
|
395
383
|
:author="piece.author"
|
|
396
384
|
:verses="piece.verses"
|
|
397
385
|
:annotations="mergedAnnotations"
|
|
398
|
-
@annotation-hover="
|
|
399
|
-
@annotation-leave="
|
|
400
|
-
@annotation-tap="
|
|
386
|
+
@annotation-hover="interaction.onHover"
|
|
387
|
+
@annotation-leave="interaction.onLeave"
|
|
388
|
+
@annotation-tap="interaction.onTap"
|
|
401
389
|
/>
|
|
402
390
|
</div>
|
|
403
391
|
|
|
404
392
|
<div class="h-sections">
|
|
405
|
-
<div v-if="piece.sections.annotations || hasLayers" class="h-ann-section">
|
|
393
|
+
<div v-if="(piece.sections.annotations && annotationsVisible) || hasLayers" class="h-ann-section">
|
|
394
|
+
<AnnotationControlBar
|
|
395
|
+
:layers="annotationLayers"
|
|
396
|
+
:has-annotations="piece.annotations.length > 0"
|
|
397
|
+
v-model:active-ids="activeLayerIds"
|
|
398
|
+
v-model:annotations-visible="annotationsVisible"
|
|
399
|
+
style="margin-bottom: 16px"
|
|
400
|
+
/>
|
|
406
401
|
<SectionBlock
|
|
407
|
-
v-if="piece.sections.annotations"
|
|
402
|
+
v-if="annotationsVisible && piece.sections.annotations"
|
|
408
403
|
num=""
|
|
409
404
|
label="注釋"
|
|
410
405
|
:special="false"
|
|
@@ -412,12 +407,6 @@ function tcy(n: number): string {
|
|
|
412
407
|
:is-annotations="true"
|
|
413
408
|
/>
|
|
414
409
|
<template v-if="hasLayers">
|
|
415
|
-
<div class="h-layers-inline">
|
|
416
|
-
<AnnotationLayerSelector
|
|
417
|
-
:layers="annotationLayers"
|
|
418
|
-
v-model:activeIds="activeLayerIds"
|
|
419
|
-
/>
|
|
420
|
-
</div>
|
|
421
410
|
<SectionBlock
|
|
422
411
|
v-for="block in layerAnnotationBlocks"
|
|
423
412
|
:key="block.label"
|
|
@@ -457,13 +446,13 @@ function tcy(n: number): string {
|
|
|
457
446
|
</div>
|
|
458
447
|
|
|
459
448
|
<AnnotationTooltip
|
|
460
|
-
:visible="
|
|
461
|
-
:annotations="
|
|
449
|
+
:visible="interaction.visible"
|
|
450
|
+
:annotations="interaction.items"
|
|
462
451
|
:layer-labels="layerLabels"
|
|
463
|
-
:style="
|
|
464
|
-
@close="
|
|
465
|
-
@tooltip-enter="
|
|
466
|
-
@tooltip-leave="
|
|
452
|
+
:style="interaction.style"
|
|
453
|
+
@close="interaction.dismiss"
|
|
454
|
+
@tooltip-enter="interaction.onTooltipEnter"
|
|
455
|
+
@tooltip-leave="interaction.onTooltipLeave"
|
|
467
456
|
/>
|
|
468
457
|
|
|
469
458
|
<Teleport to="body">
|