@hanology/cham-browser 0.2.2 → 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 +47 -30
- 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 +68 -58
|
@@ -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,16 +153,7 @@ const proseSections = computed(() => {
|
|
|
152
153
|
return result
|
|
153
154
|
})
|
|
154
155
|
|
|
155
|
-
|
|
156
|
-
tooltip.show(event, annotations)
|
|
157
|
-
}
|
|
158
|
-
function handleAnnotationLeave() {
|
|
159
|
-
if (window.innerWidth >= 768) tooltip.hide()
|
|
160
|
-
}
|
|
161
|
-
function handleAnnotationTap(event: MouseEvent, annotations: Annotation[]) {
|
|
162
|
-
tooltip.toggle(event, annotations)
|
|
163
|
-
}
|
|
164
|
-
function dismissTooltip() { tooltip.hide() }
|
|
156
|
+
|
|
165
157
|
const { getAuthor, loadShared } = useData()
|
|
166
158
|
await loadShared()
|
|
167
159
|
|
|
@@ -252,41 +244,54 @@ function tcy(n: number): string {
|
|
|
252
244
|
:verses="piece.verses"
|
|
253
245
|
:author-initial="piece.author?.charAt(0) || '詩'"
|
|
254
246
|
:annotations="mergedAnnotations"
|
|
255
|
-
@annotation-hover="
|
|
256
|
-
@annotation-leave="
|
|
257
|
-
@annotation-tap="
|
|
247
|
+
@annotation-hover="interaction.onHover"
|
|
248
|
+
@annotation-leave="interaction.onLeave"
|
|
249
|
+
@annotation-tap="interaction.onTap"
|
|
258
250
|
@open-author="openAuthorPane"
|
|
259
251
|
/>
|
|
260
252
|
</section>
|
|
261
253
|
|
|
262
|
-
<
|
|
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>
|
|
263
273
|
<SectionBlock
|
|
274
|
+
v-for="block in visibleLayerBlocks"
|
|
275
|
+
:key="block.label"
|
|
264
276
|
num=""
|
|
265
|
-
label="
|
|
277
|
+
:label="block.label"
|
|
266
278
|
:special="false"
|
|
267
|
-
:text="
|
|
279
|
+
:text="block.text"
|
|
268
280
|
:is-annotations="true"
|
|
269
281
|
:vertical="true"
|
|
282
|
+
class="v-section"
|
|
270
283
|
/>
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
:label="block.label"
|
|
283
|
-
:special="false"
|
|
284
|
-
:text="block.text"
|
|
285
|
-
:is-annotations="true"
|
|
286
|
-
:vertical="true"
|
|
287
|
-
/>
|
|
288
|
-
</template>
|
|
289
|
-
</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
|
+
/>
|
|
290
295
|
|
|
291
296
|
<SectionBlock
|
|
292
297
|
v-for="(sec, idx) in proseSections"
|
|
@@ -316,11 +321,13 @@ function tcy(n: number): string {
|
|
|
316
321
|
</div>
|
|
317
322
|
|
|
318
323
|
<AnnotationTooltip
|
|
319
|
-
:visible="
|
|
320
|
-
:annotations="
|
|
324
|
+
:visible="interaction.visible"
|
|
325
|
+
:annotations="interaction.items"
|
|
321
326
|
:layer-labels="layerLabels"
|
|
322
|
-
:style="
|
|
323
|
-
@close="
|
|
327
|
+
:style="interaction.style"
|
|
328
|
+
@close="interaction.dismiss"
|
|
329
|
+
@tooltip-enter="interaction.onTooltipEnter"
|
|
330
|
+
@tooltip-leave="interaction.onTooltipLeave"
|
|
324
331
|
/>
|
|
325
332
|
|
|
326
333
|
<Teleport to="body">
|
|
@@ -376,16 +383,23 @@ function tcy(n: number): string {
|
|
|
376
383
|
:author="piece.author"
|
|
377
384
|
:verses="piece.verses"
|
|
378
385
|
:annotations="mergedAnnotations"
|
|
379
|
-
@annotation-hover="
|
|
380
|
-
@annotation-leave="
|
|
381
|
-
@annotation-tap="
|
|
386
|
+
@annotation-hover="interaction.onHover"
|
|
387
|
+
@annotation-leave="interaction.onLeave"
|
|
388
|
+
@annotation-tap="interaction.onTap"
|
|
382
389
|
/>
|
|
383
390
|
</div>
|
|
384
391
|
|
|
385
392
|
<div class="h-sections">
|
|
386
|
-
<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
|
+
/>
|
|
387
401
|
<SectionBlock
|
|
388
|
-
v-if="piece.sections.annotations"
|
|
402
|
+
v-if="annotationsVisible && piece.sections.annotations"
|
|
389
403
|
num=""
|
|
390
404
|
label="注釋"
|
|
391
405
|
:special="false"
|
|
@@ -393,12 +407,6 @@ function tcy(n: number): string {
|
|
|
393
407
|
:is-annotations="true"
|
|
394
408
|
/>
|
|
395
409
|
<template v-if="hasLayers">
|
|
396
|
-
<div class="h-layers-inline">
|
|
397
|
-
<AnnotationLayerSelector
|
|
398
|
-
:layers="annotationLayers"
|
|
399
|
-
v-model:activeIds="activeLayerIds"
|
|
400
|
-
/>
|
|
401
|
-
</div>
|
|
402
410
|
<SectionBlock
|
|
403
411
|
v-for="block in layerAnnotationBlocks"
|
|
404
412
|
:key="block.label"
|
|
@@ -438,11 +446,13 @@ function tcy(n: number): string {
|
|
|
438
446
|
</div>
|
|
439
447
|
|
|
440
448
|
<AnnotationTooltip
|
|
441
|
-
:visible="
|
|
442
|
-
:annotations="
|
|
449
|
+
:visible="interaction.visible"
|
|
450
|
+
:annotations="interaction.items"
|
|
443
451
|
:layer-labels="layerLabels"
|
|
444
|
-
:style="
|
|
445
|
-
@close="
|
|
452
|
+
:style="interaction.style"
|
|
453
|
+
@close="interaction.dismiss"
|
|
454
|
+
@tooltip-enter="interaction.onTooltipEnter"
|
|
455
|
+
@tooltip-leave="interaction.onTooltipLeave"
|
|
446
456
|
/>
|
|
447
457
|
|
|
448
458
|
<Teleport to="body">
|