增加pdf中划句或段ai伴读功能
This commit is contained in:
@@ -0,0 +1,36 @@
|
|||||||
|
item-pane {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 1;
|
||||||
|
min-width: var(--width-available-item-pane, $min-width-item-pane + $width-sidenav);
|
||||||
|
min-height: $min-width-item-pane + $width-sidenav;
|
||||||
|
|
||||||
|
&[collapsed] {
|
||||||
|
min-width: $width-sidenav;
|
||||||
|
min-height: $width-sidenav;
|
||||||
|
max-width: $width-sidenav;
|
||||||
|
|
||||||
|
visibility: inherit;
|
||||||
|
|
||||||
|
&.stacked {
|
||||||
|
max-width: unset;
|
||||||
|
max-height: $width-sidenav;
|
||||||
|
}
|
||||||
|
|
||||||
|
#zotero-item-pane-content {
|
||||||
|
visibility: collapse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.stacked {
|
||||||
|
flex-direction: column;
|
||||||
|
height: 0px;
|
||||||
|
|
||||||
|
#zotero-item-pane-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#batch-edit-prompt-message {
|
||||||
|
padding: 3px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
item-pane-custom-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
collapsible-section {
|
||||||
|
&[custom] > .head {
|
||||||
|
& .title::before {
|
||||||
|
content: '';
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
-moz-context-properties: fill, fill-opacity, stroke, stroke-opacity;
|
||||||
|
fill: currentColor;
|
||||||
|
stroke: currentColor;
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
background: var(--custom-section-icon-light) no-repeat center;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
background: var(--custom-section-icon-dark) no-repeat center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toolbarbutton.section-custom-button {
|
||||||
|
fill: currentColor;
|
||||||
|
-moz-context-properties: fill, fill-opacity;
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
list-style-image: var(--custom-button-icon-light)
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
list-style-image: var(--custom-button-icon-dark)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+873
@@ -0,0 +1,873 @@
|
|||||||
|
general-sentence-separator = { " " }
|
||||||
|
|
||||||
|
general-key-control = Control
|
||||||
|
general-key-shift = Shift
|
||||||
|
general-key-alt = Alt
|
||||||
|
general-key-option = Option
|
||||||
|
general-key-command = Command
|
||||||
|
option-or-alt =
|
||||||
|
{ PLATFORM() ->
|
||||||
|
[macos] { general-key-option }
|
||||||
|
*[other] { general-key-alt }
|
||||||
|
}
|
||||||
|
command-or-control =
|
||||||
|
{ PLATFORM() ->
|
||||||
|
[macos] { general-key-command }
|
||||||
|
*[other] { general-key-control }
|
||||||
|
}
|
||||||
|
return-or-enter =
|
||||||
|
{ PLATFORM() ->
|
||||||
|
[macos] Return
|
||||||
|
*[other] Enter
|
||||||
|
}
|
||||||
|
delete-or-backspace =
|
||||||
|
{ PLATFORM() ->
|
||||||
|
[macos] Delete
|
||||||
|
*[other] Backspace
|
||||||
|
}
|
||||||
|
-os-name =
|
||||||
|
{ PLATFORM() ->
|
||||||
|
[macos] macOS
|
||||||
|
[windows] Windows
|
||||||
|
*[other] Linux
|
||||||
|
}
|
||||||
|
|
||||||
|
general-print = Print
|
||||||
|
general-remove = Remove
|
||||||
|
general-add = Add
|
||||||
|
general-remind-me-later = Remind Me Later
|
||||||
|
general-dont-ask-again = Don’t Ask Again
|
||||||
|
general-choose-file = Choose File…
|
||||||
|
general-open-settings = Open Settings
|
||||||
|
general-settings = Settings…
|
||||||
|
general-help = Help
|
||||||
|
general-tag = Tag
|
||||||
|
general-got-it = Got It
|
||||||
|
general-done = Done
|
||||||
|
general-view-troubleshooting-instructions = View Troubleshooting Instructions
|
||||||
|
general-go-back = Go Back
|
||||||
|
general-accept = Accept
|
||||||
|
general-cancel = Cancel
|
||||||
|
cancel-button =
|
||||||
|
.label = { general-cancel }
|
||||||
|
general-show-in-library = Show in Library
|
||||||
|
general-restartApp = Restart { -app-name }
|
||||||
|
general-restartInTroubleshootingMode = Restart in Troubleshooting Mode
|
||||||
|
general-save = Save
|
||||||
|
general-clear = Clear
|
||||||
|
clear-button =
|
||||||
|
.label = { general-clear }
|
||||||
|
general-update = Update
|
||||||
|
general-reset-to-default = Reset to Default
|
||||||
|
general-back = Back
|
||||||
|
general-edit = Edit
|
||||||
|
general-cut = Cut
|
||||||
|
general-copy = Copy
|
||||||
|
general-paste = Paste
|
||||||
|
general-find = Find
|
||||||
|
general-delete = Delete
|
||||||
|
general-insert = Insert
|
||||||
|
general-and = and
|
||||||
|
general-et-al = et al.
|
||||||
|
general-previous = Previous
|
||||||
|
general-next = Next
|
||||||
|
general-learn-more = Learn More
|
||||||
|
general-more-information = More Information
|
||||||
|
general-warning = Warning
|
||||||
|
general-type-to-continue = Type “{ $text }” to continue.
|
||||||
|
general-continue = Continue
|
||||||
|
general-allow = Allow
|
||||||
|
general-always-allow = Always Allow
|
||||||
|
general-deny = Deny
|
||||||
|
|
||||||
|
general-red = Red
|
||||||
|
general-orange = Orange
|
||||||
|
general-yellow = Yellow
|
||||||
|
general-green = Green
|
||||||
|
general-teal = Teal
|
||||||
|
general-blue = Blue
|
||||||
|
general-purple = Purple
|
||||||
|
general-magenta = Magenta
|
||||||
|
general-violet = Violet
|
||||||
|
general-maroon = Maroon
|
||||||
|
general-gray = Gray
|
||||||
|
general-black = Black
|
||||||
|
|
||||||
|
general-loading = Loading…
|
||||||
|
|
||||||
|
db-checking-integrity = Checking database integrity…
|
||||||
|
db-repairing = Repairing database…
|
||||||
|
|
||||||
|
citation-style-label = Citation Style:
|
||||||
|
language-label = Language:
|
||||||
|
|
||||||
|
menu-custom-group-submenu =
|
||||||
|
.label = More Options…
|
||||||
|
|
||||||
|
menu-file-show-in-finder =
|
||||||
|
.label = Show in Finder
|
||||||
|
menu-file-show-file =
|
||||||
|
.label = Show File
|
||||||
|
menu-file-show-files =
|
||||||
|
.label = Show Files
|
||||||
|
menu-print =
|
||||||
|
.label = { general-print }
|
||||||
|
|
||||||
|
menu-density =
|
||||||
|
.label = Density
|
||||||
|
|
||||||
|
add-attachment = Add Attachment
|
||||||
|
new-note = New Note
|
||||||
|
|
||||||
|
menu-add-by-identifier =
|
||||||
|
.label = Add by Identifier…
|
||||||
|
|
||||||
|
menu-add-attachment =
|
||||||
|
.label = { add-attachment }
|
||||||
|
menu-add-standalone-file-attachment =
|
||||||
|
.label = Add File…
|
||||||
|
menu-add-standalone-linked-file-attachment =
|
||||||
|
.label = Add Link to File…
|
||||||
|
menu-add-child-file-attachment =
|
||||||
|
.label = Attach File…
|
||||||
|
menu-add-child-linked-file-attachment =
|
||||||
|
.label = Attach Link to File…
|
||||||
|
menu-add-child-linked-url-attachment =
|
||||||
|
.label = Attach Web Link…
|
||||||
|
|
||||||
|
menu-new-note =
|
||||||
|
.label = { new-note }
|
||||||
|
menu-new-standalone-note =
|
||||||
|
.label = New Standalone Note
|
||||||
|
menu-new-item-note =
|
||||||
|
.label = New Item Note
|
||||||
|
|
||||||
|
menu-restoreToLibrary =
|
||||||
|
.label = Restore to Library
|
||||||
|
menu-deletePermanently =
|
||||||
|
.label = Delete Permanently…
|
||||||
|
|
||||||
|
menu-tools-plugins =
|
||||||
|
.label = Plugins
|
||||||
|
|
||||||
|
menu-view-columns-move-left =
|
||||||
|
.label = Move Column Left
|
||||||
|
menu-view-columns-move-right =
|
||||||
|
.label = Move Column Right
|
||||||
|
menu-view-hide-context-annotation-rows =
|
||||||
|
.label = Hide Non-Matching Annotations
|
||||||
|
|
||||||
|
|
||||||
|
menu-view-note-font-size =
|
||||||
|
.label = Note Font Size
|
||||||
|
|
||||||
|
menu-view-note-tab-font-size =
|
||||||
|
.label = Note Tab Font Size
|
||||||
|
|
||||||
|
menu-show-tabs-menu =
|
||||||
|
.label = Show Tabs Menu
|
||||||
|
|
||||||
|
menu-edit-copy-annotation =
|
||||||
|
.label = { $count ->
|
||||||
|
[one] Copy Annotation
|
||||||
|
*[other] Copy { $count } Annotations
|
||||||
|
}
|
||||||
|
|
||||||
|
main-window-command =
|
||||||
|
.label = Library
|
||||||
|
main-window-key =
|
||||||
|
.key = L
|
||||||
|
|
||||||
|
zotero-toolbar-tabs-menu =
|
||||||
|
.tooltiptext = List all tabs
|
||||||
|
filter-collections = Filter Collections
|
||||||
|
zotero-collections-search =
|
||||||
|
.placeholder = { filter-collections }
|
||||||
|
zotero-collections-search-btn =
|
||||||
|
.tooltiptext = { filter-collections }
|
||||||
|
zotero-tabs-menu-filter =
|
||||||
|
.placeholder = Search Tabs
|
||||||
|
zotero-tabs-menu-close-button =
|
||||||
|
.title = Close Tab
|
||||||
|
zotero-toolbar-tabs-scroll-forwards =
|
||||||
|
.title = Scroll forwards
|
||||||
|
zotero-toolbar-tabs-scroll-backwards =
|
||||||
|
.title = Scroll backwards
|
||||||
|
|
||||||
|
toolbar-add-attachment =
|
||||||
|
.tooltiptext = { add-attachment }
|
||||||
|
|
||||||
|
recently-read = Recently Read
|
||||||
|
collections-menu-show-recently-read =
|
||||||
|
.label = Show { recently-read }
|
||||||
|
item-menu-remove-from-recently-read =
|
||||||
|
.label = Remove from { recently-read }…
|
||||||
|
collections-menu-clear-all-last-read =
|
||||||
|
.label = Clear All Last Read Dates…
|
||||||
|
recently-read-clear-all-confirm = All Last Read dates in this library will be erased.
|
||||||
|
|
||||||
|
# Item list section headers for a multiple-row collection-tree selection within one library
|
||||||
|
items-section-collections-selected =
|
||||||
|
{ $count ->
|
||||||
|
[one] { $count } collection selected
|
||||||
|
*[other] { $count } collections selected
|
||||||
|
}
|
||||||
|
items-section-searches-selected =
|
||||||
|
{ $count ->
|
||||||
|
[one] { $count } saved search selected
|
||||||
|
*[other] { $count } saved searches selected
|
||||||
|
}
|
||||||
|
items-section-sources-selected =
|
||||||
|
{ $count ->
|
||||||
|
[one] { $count } source selected
|
||||||
|
*[other] { $count } sources selected
|
||||||
|
}
|
||||||
|
# Item list section headers for a selection spanning multiple libraries, one per library
|
||||||
|
items-section-library-collections =
|
||||||
|
{ $count ->
|
||||||
|
[one] { $library } ({ $count } collection selected)
|
||||||
|
*[other] { $library } ({ $count } collections selected)
|
||||||
|
}
|
||||||
|
items-section-library-searches =
|
||||||
|
{ $count ->
|
||||||
|
[one] { $library } ({ $count } saved search selected)
|
||||||
|
*[other] { $library } ({ $count } saved searches selected)
|
||||||
|
}
|
||||||
|
items-section-library-sources =
|
||||||
|
{ $count ->
|
||||||
|
[one] { $library } ({ $count } source selected)
|
||||||
|
*[other] { $library } ({ $count } sources selected)
|
||||||
|
}
|
||||||
|
items-section-library-recently-read = { $library } ({ recently-read })
|
||||||
|
items-section-library = { $library }
|
||||||
|
collections-menu-rename =
|
||||||
|
.label = Rename
|
||||||
|
edit-saved-search = Edit Saved Search
|
||||||
|
collections-menu-edit-search =
|
||||||
|
.label = Edit Search
|
||||||
|
collections-menu-duplicate-search =
|
||||||
|
.label = Duplicate Search
|
||||||
|
collections-menu-move-collection =
|
||||||
|
.label = Move To
|
||||||
|
collections-menu-copy-collection =
|
||||||
|
.label = Copy To
|
||||||
|
collections-menu-export =
|
||||||
|
.label = Export…
|
||||||
|
collections-menu-generate-report =
|
||||||
|
.label = Generate Report…
|
||||||
|
collections-menu-create-bibliography =
|
||||||
|
.label = Create Bibliography…
|
||||||
|
collections-menu-unsubscribe =
|
||||||
|
.label = Unsubscribe…
|
||||||
|
collections-menu-delete =
|
||||||
|
.label = { $count ->
|
||||||
|
[one] Delete Collection…
|
||||||
|
*[other] Delete Collections…
|
||||||
|
}
|
||||||
|
collections-menu-delete-with-items =
|
||||||
|
.label = { $count ->
|
||||||
|
[one] Delete Collection and Items…
|
||||||
|
*[other] Delete Collections and Items…
|
||||||
|
}
|
||||||
|
collections-menu-delete-search =
|
||||||
|
.label = { $count ->
|
||||||
|
[one] Delete Search…
|
||||||
|
*[other] Delete Searches…
|
||||||
|
}
|
||||||
|
|
||||||
|
collections-delete-title =
|
||||||
|
{ $count ->
|
||||||
|
[one] Delete Collection
|
||||||
|
*[other] Delete Collections
|
||||||
|
}
|
||||||
|
collections-delete-message =
|
||||||
|
{ $count ->
|
||||||
|
[one] Are you sure you want to delete this collection?
|
||||||
|
*[other] Are you sure you want to delete { $count } collections?
|
||||||
|
}
|
||||||
|
collections-delete-keep-items =
|
||||||
|
{ $count ->
|
||||||
|
[one] Items within this collection will not be deleted.
|
||||||
|
*[other] Items within these collections will not be deleted.
|
||||||
|
}
|
||||||
|
collections-delete-with-items-title =
|
||||||
|
{ $count ->
|
||||||
|
[one] Delete Collection and Items
|
||||||
|
*[other] Delete Collections and Items
|
||||||
|
}
|
||||||
|
collections-delete-with-items-message =
|
||||||
|
{ $count ->
|
||||||
|
[one] Are you sure you want to delete this collection and move all items within it to the Trash?
|
||||||
|
*[other] Are you sure you want to delete { $count } collections and move all items within them to the Trash?
|
||||||
|
}
|
||||||
|
collections-delete-search-title =
|
||||||
|
{ $count ->
|
||||||
|
[one] Delete Search
|
||||||
|
*[other] Delete Searches
|
||||||
|
}
|
||||||
|
collections-delete-search-message =
|
||||||
|
{ $count ->
|
||||||
|
[one] Are you sure you want to delete this search?
|
||||||
|
*[other] Are you sure you want to delete { $count } searches?
|
||||||
|
}
|
||||||
|
|
||||||
|
item-creator-moveDown =
|
||||||
|
.label = Move Down
|
||||||
|
item-creator-moveToTop =
|
||||||
|
.label = Move to Top
|
||||||
|
item-creator-moveUp =
|
||||||
|
.label = Move Up
|
||||||
|
|
||||||
|
item-menu-viewAttachment =
|
||||||
|
.label = Open { $numAttachments ->
|
||||||
|
[one] { $attachmentType ->
|
||||||
|
[pdf] PDF
|
||||||
|
[epub] EPUB
|
||||||
|
[snapshot] Snapshot
|
||||||
|
[note] Note
|
||||||
|
*[other] Attachment
|
||||||
|
}
|
||||||
|
*[other] { $attachmentType ->
|
||||||
|
[pdf] PDFs
|
||||||
|
[epub] EPUBs
|
||||||
|
[snapshot] Snapshots
|
||||||
|
[note] Notes
|
||||||
|
*[other] Attachments
|
||||||
|
}
|
||||||
|
} {
|
||||||
|
$openIn ->
|
||||||
|
[tab] in New Tab
|
||||||
|
[window] in New Window
|
||||||
|
*[other] { "" }
|
||||||
|
}
|
||||||
|
|
||||||
|
item-menu-add-file =
|
||||||
|
.label = File
|
||||||
|
item-menu-add-linked-file =
|
||||||
|
.label = Linked File
|
||||||
|
item-menu-add-url =
|
||||||
|
.label = Web Link
|
||||||
|
item-menu-change-parent-item =
|
||||||
|
.label = Change Parent Item…
|
||||||
|
item-menu-relate-items =
|
||||||
|
.label = Relate Items
|
||||||
|
|
||||||
|
view-online = View Online
|
||||||
|
item-menu-option-view-online =
|
||||||
|
.label = {view-online}
|
||||||
|
item-button-view-online =
|
||||||
|
.tooltiptext = {view-online}
|
||||||
|
|
||||||
|
file-renaming-file-renamed-to = File renamed to { $filename }
|
||||||
|
|
||||||
|
itembox-button-options =
|
||||||
|
.tooltiptext = Open context menu
|
||||||
|
itembox-button-merge =
|
||||||
|
.aria-label = Select version of { $field } field
|
||||||
|
|
||||||
|
create-parent-intro = Enter a DOI, ISBN, PMID, arXiv ID, or ADS Bibcode to identify this file:
|
||||||
|
|
||||||
|
reader-use-dark-mode-for-content =
|
||||||
|
.label = Use Dark Mode for Content
|
||||||
|
|
||||||
|
update-updates-found-intro-minor = An update for { -app-name } is available:
|
||||||
|
update-updates-found-desc = It is recommended that you apply this update as soon as possible.
|
||||||
|
|
||||||
|
import-window =
|
||||||
|
.title = Import
|
||||||
|
|
||||||
|
import-where-from = Where do you want to import from?
|
||||||
|
import-online-intro-title = Introduction
|
||||||
|
|
||||||
|
import-source-file =
|
||||||
|
.label = A file (BibTeX, RIS, Zotero RDF, etc.)
|
||||||
|
|
||||||
|
import-source-folder =
|
||||||
|
.label = A folder of PDFs or other files
|
||||||
|
|
||||||
|
import-source-online =
|
||||||
|
.label = { $targetApp } online import
|
||||||
|
|
||||||
|
import-options = Options
|
||||||
|
import-importing = Importing…
|
||||||
|
|
||||||
|
import-create-collection =
|
||||||
|
.label = Place imported collections and items into new collection
|
||||||
|
|
||||||
|
import-recreate-structure =
|
||||||
|
.label = Recreate folder structure as collections
|
||||||
|
|
||||||
|
import-fileTypes-header = File Types to Import:
|
||||||
|
|
||||||
|
import-fileTypes-pdf =
|
||||||
|
.label = PDFs
|
||||||
|
|
||||||
|
import-fileTypes-other =
|
||||||
|
.placeholder = Other files by pattern, comma-separated (e.g., *.jpg,*.png)
|
||||||
|
|
||||||
|
import-file-handling = File Handling
|
||||||
|
import-file-handling-store =
|
||||||
|
.label = Copy files to the { -app-name } storage folder
|
||||||
|
import-file-handling-link =
|
||||||
|
.label = Link to files in original location
|
||||||
|
import-fileHandling-description = Linked files cannot be synced by { -app-name }.
|
||||||
|
import-online-new =
|
||||||
|
.label = Download new items only; don’t update previously imported items
|
||||||
|
import-mendeley-username = Username
|
||||||
|
import-mendeley-password = Password
|
||||||
|
|
||||||
|
general-error = Error
|
||||||
|
file-interface-import-error = An error occurred while trying to import the selected file. Please ensure that the file is valid and try again.
|
||||||
|
file-interface-import-complete = Import Complete
|
||||||
|
file-interface-items-were-imported = { $numItems ->
|
||||||
|
[0] No items were imported
|
||||||
|
[one] One item was imported
|
||||||
|
*[other] { $numItems } items were imported
|
||||||
|
}
|
||||||
|
file-interface-items-were-relinked = { $numRelinked ->
|
||||||
|
[0] No items were relinked
|
||||||
|
[one] One item was relinked
|
||||||
|
*[other] { $numRelinked } items were relinked
|
||||||
|
}
|
||||||
|
|
||||||
|
import-mendeley-cannot-decrypt = The selected Mendeley database could not be decrypted. This can happen if the database file has been renamed. See <a data-l10n-name="mendeley-import-kb">How do I import a Mendeley library into Zotero?</a> for more information.
|
||||||
|
import-mendeley-unsupported = The selected file does not appear to be a Mendeley database. See <a data-l10n-name="mendeley-import-kb">How do I import a Mendeley library into Zotero?</a> for more information.
|
||||||
|
import-mendeley-db-in-use = The selected Mendeley database is in use. Please quit Mendeley Desktop and try again.
|
||||||
|
|
||||||
|
file-interface-import-error-translator = An error occurred importing the selected file with “{ $translator }”. Please ensure that the file is valid and try again.
|
||||||
|
|
||||||
|
import-online-intro=In the next step you will be asked to log in to { $targetAppOnline } and grant { -app-name } access. This is necessary to import your { $targetApp } library into { -app-name }.
|
||||||
|
import-online-intro2={ -app-name } will never see or store your { $targetApp } password.
|
||||||
|
import-online-form-intro = Please enter your credentials to log in to { $targetAppOnline }. This is necessary to import your { $targetApp } library into { -app-name }.
|
||||||
|
import-online-wrong-credentials = Login to { $targetApp } failed. Please re-enter credentials and try again.
|
||||||
|
import-online-blocked-by-plugin = The import cannot continue with { $plugin } installed. Please disable this plugin and try again.
|
||||||
|
import-online-relink-only =
|
||||||
|
.label = Relink Mendeley Desktop citations
|
||||||
|
import-online-relink-kb = { general-more-information }
|
||||||
|
import-online-connection-error = { -app-name } could not connect to { $targetApp }. Please check your internet connection and try again.
|
||||||
|
|
||||||
|
tab-title-multiple-collections = Multiple
|
||||||
|
|
||||||
|
items-table-cell-notes =
|
||||||
|
.aria-label = { $count ->
|
||||||
|
[one] { $count } Note
|
||||||
|
*[other] { $count } Notes
|
||||||
|
}
|
||||||
|
|
||||||
|
items-column-added-by = Added By
|
||||||
|
items-column-modified-by = Modified By
|
||||||
|
items-column-last-read = Last Read
|
||||||
|
|
||||||
|
report-error =
|
||||||
|
.label = Report Error…
|
||||||
|
|
||||||
|
rtfScan-wizard =
|
||||||
|
.title = RTF Scan
|
||||||
|
|
||||||
|
rtfScan-introPage-description = { -app-name } can automatically extract and reformat citations and insert a bibliography into RTF files. It currently supports citations in variations of the following formats:
|
||||||
|
rtfScan-introPage-description2 = To get started, select an RTF input file and an output file below:
|
||||||
|
|
||||||
|
rtfScan-input-file = Input File:
|
||||||
|
rtfScan-output-file = Output File:
|
||||||
|
|
||||||
|
rtfScan-no-file-selected = No file selected
|
||||||
|
rtfScan-choose-input-file =
|
||||||
|
.label = { general-choose-file }
|
||||||
|
.aria-label = Choose Input File
|
||||||
|
rtfScan-choose-output-file =
|
||||||
|
.label = { general-choose-file }
|
||||||
|
.aria-label = Choose Output File
|
||||||
|
|
||||||
|
rtfScan-intro-page = Introduction
|
||||||
|
|
||||||
|
rtfScan-scan-page = Scanning for Citations
|
||||||
|
|
||||||
|
rtfScan-scanPage-description = { -app-name } is scanning your document for citations. Please be patient.
|
||||||
|
|
||||||
|
rtfScan-citations-page = Verify Cited Items
|
||||||
|
|
||||||
|
rtfScan-citations-page-description = Please review the list of recognized citations below to ensure that { -app-name } has selected the corresponding items correctly. Any unmapped or ambiguous citations must be resolved before proceeding to the next step.
|
||||||
|
|
||||||
|
rtfScan-style-page = Document Formatting
|
||||||
|
|
||||||
|
rtfScan-format-page = Formatting Citations
|
||||||
|
|
||||||
|
rtfScan-format-page-description = { -app-name } is processing and formatting your RTF file. Please be patient.
|
||||||
|
|
||||||
|
rtfScan-complete-page = RTF Scan Complete
|
||||||
|
|
||||||
|
rtfScan-complete-page-description = Your document has now been scanned and processed. Please ensure that it is formatted correctly.
|
||||||
|
rtfScan-action-find-match =
|
||||||
|
.title = Select matching item
|
||||||
|
|
||||||
|
rtfScan-action-accept-match =
|
||||||
|
.title = Accept this match
|
||||||
|
|
||||||
|
runJS-title = Run JavaScript
|
||||||
|
runJS-editor-label = Code:
|
||||||
|
runJS-run = Run
|
||||||
|
runJS-help = { general-help }
|
||||||
|
runJS-completed = completed successfully
|
||||||
|
runJS-result = {
|
||||||
|
$type ->
|
||||||
|
[async] Return value:
|
||||||
|
*[other] Result:
|
||||||
|
}
|
||||||
|
runJS-run-async = Run as async function
|
||||||
|
|
||||||
|
bibliography-window =
|
||||||
|
.title = { -app-name } - Create Citation/Bibliography
|
||||||
|
bibliography-style-label = { citation-style-label }
|
||||||
|
bibliography-locale-label = { language-label }
|
||||||
|
bibliography-displayAs-label = Display citations as:
|
||||||
|
bibliography-advancedOptions-label = Advanced Options
|
||||||
|
bibliography-outputMode-label = Output Mode:
|
||||||
|
bibliography-outputMode-citations =
|
||||||
|
.label = {
|
||||||
|
$type ->
|
||||||
|
[citation] Citations
|
||||||
|
[note] Notes
|
||||||
|
*[other] Citations
|
||||||
|
}
|
||||||
|
bibliography-outputMode-bibliography =
|
||||||
|
.label = Bibliography
|
||||||
|
bibliography-outputMethod-label = Output Method:
|
||||||
|
bibliography-outputMethod-saveAsRTF =
|
||||||
|
.label = Save as RTF
|
||||||
|
bibliography-outputMethod-saveAsHTML =
|
||||||
|
.label = Save as HTML
|
||||||
|
bibliography-outputMethod-copyToClipboard =
|
||||||
|
.label = Copy to Clipboard
|
||||||
|
bibliography-outputMethod-print =
|
||||||
|
.label = Print
|
||||||
|
bibliography-manageStyles-label = Manage Styles…
|
||||||
|
|
||||||
|
styleEditor-locatorType =
|
||||||
|
.aria-label = Locator type
|
||||||
|
styleEditor-locatorInput = Locator input
|
||||||
|
styleEditor-citationStyle = { citation-style-label }
|
||||||
|
styleEditor-locale = { language-label }
|
||||||
|
styleEditor-editor =
|
||||||
|
.aria-label = Style editor
|
||||||
|
styleEditor-preview =
|
||||||
|
.aria-label = Preview
|
||||||
|
|
||||||
|
stylePreview-generating = Generating previews…
|
||||||
|
|
||||||
|
publications-intro-page = My Publications
|
||||||
|
|
||||||
|
publications-intro = Items you add to My Publications will be shown on your profile page on zotero.org. If you choose to include attached files, they will be made publicly available under the license you specify. Only add work you yourself have created, and only include files if you have the rights to distribute them and wish to do so.
|
||||||
|
publications-include-checkbox-files =
|
||||||
|
.label = Include files
|
||||||
|
publications-include-checkbox-notes =
|
||||||
|
.label = Include notes
|
||||||
|
|
||||||
|
publications-include-adjust-at-any-time = You can adjust what to show at any time from the My Publications collection.
|
||||||
|
publications-intro-authorship =
|
||||||
|
.label = I created this work.
|
||||||
|
publications-intro-authorship-files =
|
||||||
|
.label = I created this work and have the rights to distribute included files.
|
||||||
|
|
||||||
|
publications-sharing-page = Choose how your work may be shared
|
||||||
|
|
||||||
|
publications-sharing-keep-rights-field =
|
||||||
|
.label = Keep the existing Rights field
|
||||||
|
publications-sharing-keep-rights-field-where-available =
|
||||||
|
.label = Keep the existing Rights field where available
|
||||||
|
publications-sharing-text = You can reserve all rights to your work, license it under a Creative Commons license, or dedicate it to the public domain. In all cases, the work will be made publicly available via zotero.org.
|
||||||
|
publications-sharing-prompt = Would you like to allow your work to be shared by others?
|
||||||
|
publications-sharing-reserved =
|
||||||
|
.label = No, only publish my work on zotero.org
|
||||||
|
publications-sharing-cc =
|
||||||
|
.label = Yes, under a Creative Commons license
|
||||||
|
publications-sharing-cc0 =
|
||||||
|
.label = Yes, and place my work in the public domain
|
||||||
|
|
||||||
|
publications-license-page = Choose a Creative Commons license
|
||||||
|
publications-choose-license-text = A Creative Commons license allows others to copy and redistribute your work as long as they give appropriate credit, provide a link to the license, and indicate if changes were made. Additional conditions can be specified below.
|
||||||
|
publications-choose-license-adaptations-prompt = Allow adaptations of your work to be shared?
|
||||||
|
|
||||||
|
publications-choose-license-yes =
|
||||||
|
.label = Yes
|
||||||
|
.accesskey = Y
|
||||||
|
publications-choose-license-no =
|
||||||
|
.label = No
|
||||||
|
.accesskey = N
|
||||||
|
publications-choose-license-sharealike =
|
||||||
|
.label = Yes, as long as others share alike
|
||||||
|
.accesskey = S
|
||||||
|
|
||||||
|
publications-choose-license-commercial-prompt = Allow commercial uses of your work?
|
||||||
|
publications-buttons-add-to-my-publications =
|
||||||
|
.label = Add to My Publications
|
||||||
|
publications-buttons-next-sharing =
|
||||||
|
.label = Next: Sharing
|
||||||
|
publications-buttons-next-choose-license =
|
||||||
|
.label = Choose a License
|
||||||
|
|
||||||
|
licenses-cc-0 = CC0 1.0 Universal Public Domain Dedication
|
||||||
|
licenses-cc-by = Creative Commons Attribution 4.0 International License
|
||||||
|
licenses-cc-by-nd = Creative Commons Attribution-NoDerivatives 4.0 International License
|
||||||
|
licenses-cc-by-sa = Creative Commons Attribution-ShareAlike 4.0 International License
|
||||||
|
licenses-cc-by-nc = Creative Commons Attribution-NonCommercial 4.0 International License
|
||||||
|
licenses-cc-by-nc-nd = Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License
|
||||||
|
licenses-cc-by-nc-sa = Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License
|
||||||
|
licenses-cc-more-info = Be sure you have read the Creative Commons <a data-l10n-name="license-considerations">Considerations for licensors</a> before placing your work under a CC license. Note that the license you apply cannot be revoked, even if you later choose different terms or cease publishing the work.
|
||||||
|
licenses-cc0-more-info = Be sure you have read the Creative Commons <a data-l10n-name="license-considerations">CC0 FAQ</a> before applying CC0 to your work. Please note that dedicating your work to the public domain is irreversible, even if you later choose different terms or cease publishing the work.
|
||||||
|
|
||||||
|
debug-output-logging-restart-in-troubleshooting-mode-checkbox = { general-restartInTroubleshootingMode }
|
||||||
|
|
||||||
|
restart-in-troubleshooting-mode-menuitem =
|
||||||
|
.label = Restart in Troubleshooting Mode…
|
||||||
|
.accesskey = T
|
||||||
|
restart-in-troubleshooting-mode-dialog-title = { general-restartInTroubleshootingMode }
|
||||||
|
restart-in-troubleshooting-mode-dialog-description = { -app-name } will restart with all plugins disabled. Some features may not function correctly while Troubleshooting Mode is enabled.
|
||||||
|
|
||||||
|
menu-ui-density =
|
||||||
|
.label = Density
|
||||||
|
menu-ui-density-comfortable =
|
||||||
|
.label = Comfortable
|
||||||
|
menu-ui-density-compact =
|
||||||
|
.label = Compact
|
||||||
|
|
||||||
|
pane-item-details = Item Details
|
||||||
|
pane-info = Info
|
||||||
|
pane-abstract = Abstract
|
||||||
|
pane-attachments = Attachments
|
||||||
|
pane-notes = Notes
|
||||||
|
pane-note-info = Note Info
|
||||||
|
pane-libraries-collections = Libraries and Collections
|
||||||
|
pane-tags = Tags
|
||||||
|
pane-related = Related
|
||||||
|
pane-attachment-info = Attachment Info
|
||||||
|
pane-attachment-preview = Preview
|
||||||
|
pane-attachment-annotations = Annotations
|
||||||
|
|
||||||
|
pane-header-attachment-associated =
|
||||||
|
.label = Rename associated file
|
||||||
|
|
||||||
|
item-details-pane =
|
||||||
|
.aria-label = { pane-item-details }
|
||||||
|
section-info =
|
||||||
|
.label = { pane-info }
|
||||||
|
section-abstract =
|
||||||
|
.label = { pane-abstract }
|
||||||
|
section-attachments =
|
||||||
|
.label = { $count ->
|
||||||
|
[one] { $count } Attachment
|
||||||
|
*[other] { $count } Attachments
|
||||||
|
}
|
||||||
|
section-attachment-preview =
|
||||||
|
.label = { pane-attachment-preview }
|
||||||
|
section-attachments-annotations =
|
||||||
|
.label = { $count ->
|
||||||
|
[one] { $count } Annotation
|
||||||
|
*[other] { $count } Annotations
|
||||||
|
}
|
||||||
|
section-attachments-move-to-trash-message = Are you sure you want to move “{ $title }” to the trash?
|
||||||
|
section-notes =
|
||||||
|
.label = { $count ->
|
||||||
|
[one] { $count } Note
|
||||||
|
*[other] { $count } Notes
|
||||||
|
}
|
||||||
|
section-libraries-collections =
|
||||||
|
.label = { pane-libraries-collections }
|
||||||
|
section-tags =
|
||||||
|
.label = { $count ->
|
||||||
|
[one] { $count } Tag
|
||||||
|
*[other] { $count } Tags
|
||||||
|
}
|
||||||
|
section-related =
|
||||||
|
.label = { $count } Related
|
||||||
|
section-attachment-info =
|
||||||
|
.label = { pane-attachment-info }
|
||||||
|
|
||||||
|
section-button-remove =
|
||||||
|
.tooltiptext = { general-remove }
|
||||||
|
section-button-add =
|
||||||
|
.tooltiptext = { general-add }
|
||||||
|
section-button-expand =
|
||||||
|
.dynamic-tooltiptext = Expand section
|
||||||
|
.label = Expand { $section } section
|
||||||
|
section-button-collapse =
|
||||||
|
.dynamic-tooltiptext = Collapse section
|
||||||
|
.label = Collapse { $section } section
|
||||||
|
annotations-count =
|
||||||
|
{ $count ->
|
||||||
|
[one] { $count } Annotation
|
||||||
|
*[other] { $count } Annotations
|
||||||
|
}
|
||||||
|
section-button-annotations =
|
||||||
|
.title = { annotations-count }
|
||||||
|
.aria-label = { annotations-count }
|
||||||
|
attachment-preview =
|
||||||
|
.aria-label = { pane-attachment-preview }
|
||||||
|
|
||||||
|
sidenav-info =
|
||||||
|
.tooltiptext = { pane-info }
|
||||||
|
sidenav-abstract =
|
||||||
|
.tooltiptext = { pane-abstract }
|
||||||
|
sidenav-attachments =
|
||||||
|
.tooltiptext = { pane-attachments }
|
||||||
|
sidenav-notes =
|
||||||
|
.tooltiptext = { pane-notes }
|
||||||
|
sidenav-note-info =
|
||||||
|
.tooltiptext = { pane-note-info }
|
||||||
|
sidenav-attachment-info =
|
||||||
|
.tooltiptext = { pane-attachment-info }
|
||||||
|
sidenav-attachment-preview =
|
||||||
|
.tooltiptext = { pane-attachment-preview }
|
||||||
|
sidenav-attachment-annotations =
|
||||||
|
.tooltiptext = { pane-attachment-annotations }
|
||||||
|
sidenav-libraries-collections =
|
||||||
|
.tooltiptext = { pane-libraries-collections }
|
||||||
|
sidenav-tags =
|
||||||
|
.tooltiptext = { pane-tags }
|
||||||
|
sidenav-related =
|
||||||
|
.tooltiptext = { pane-related }
|
||||||
|
sidenav-main-btn-grouping =
|
||||||
|
.aria-label = { pane-item-details }
|
||||||
|
sidenav-reorder-up =
|
||||||
|
.label = Move Section Up
|
||||||
|
sidenav-reorder-down =
|
||||||
|
.label = Move Section Down
|
||||||
|
sidenav-reorder-reset =
|
||||||
|
.label = Reset Section Order
|
||||||
|
|
||||||
|
toggle-item-pane =
|
||||||
|
.tooltiptext = Toggle Item Pane
|
||||||
|
toggle-context-pane =
|
||||||
|
.tooltiptext = Toggle Context Pane
|
||||||
|
|
||||||
|
pin-section =
|
||||||
|
.label = Pin Section
|
||||||
|
unpin-section =
|
||||||
|
.label = Unpin Section
|
||||||
|
|
||||||
|
collapse-other-sections =
|
||||||
|
.label = Collapse Other Sections
|
||||||
|
expand-all-sections =
|
||||||
|
.label = Expand All Sections
|
||||||
|
|
||||||
|
abstract-field =
|
||||||
|
.placeholder = Add abstract…
|
||||||
|
|
||||||
|
tag-field =
|
||||||
|
.aria-label = { general-tag }
|
||||||
|
|
||||||
|
tagselector-search =
|
||||||
|
.placeholder = Filter Tags
|
||||||
|
|
||||||
|
context-notes-search =
|
||||||
|
.placeholder = Search Notes
|
||||||
|
context-notes-return-button =
|
||||||
|
.aria-label = { general-go-back }
|
||||||
|
|
||||||
|
new-collection = New Collection…
|
||||||
|
menu-new-collection =
|
||||||
|
.label = { new-collection }
|
||||||
|
toolbar-new-collection =
|
||||||
|
.tooltiptext = { new-collection }
|
||||||
|
|
||||||
|
new-collection-dialog =
|
||||||
|
.title = New Collection
|
||||||
|
.buttonlabelaccept = Create Collection
|
||||||
|
new-collection-name = Name:
|
||||||
|
new-collection-create-in = Create in:
|
||||||
|
|
||||||
|
show-publications-menuitem =
|
||||||
|
.label = Show My Publications
|
||||||
|
|
||||||
|
attachment-info-title = Title
|
||||||
|
attachment-info-filename = Filename
|
||||||
|
attachment-info-accessed = Accessed
|
||||||
|
attachment-info-pages = Pages
|
||||||
|
attachment-info-modified = Modified
|
||||||
|
attachment-info-index = Indexed
|
||||||
|
attachment-info-convert-note =
|
||||||
|
.label = Migrate to {
|
||||||
|
$type ->
|
||||||
|
[standalone] Standalone
|
||||||
|
[child] Item
|
||||||
|
*[unknown] New
|
||||||
|
} Note
|
||||||
|
.tooltiptext = Adding notes to attachments is no longer supported, but you can edit this note by migrating it to a separate note.
|
||||||
|
|
||||||
|
section-note-info =
|
||||||
|
.label = { pane-note-info }
|
||||||
|
|
||||||
|
note-info-title = Title
|
||||||
|
note-info-parent-item = Parent Item
|
||||||
|
note-info-parent-item-button = {
|
||||||
|
$hasParentItem ->
|
||||||
|
[true] { $parentItemTitle }
|
||||||
|
*[false] None
|
||||||
|
}
|
||||||
|
.title = {
|
||||||
|
$hasParentItem ->
|
||||||
|
[true] View parent item in library
|
||||||
|
*[false] View note item in library
|
||||||
|
}
|
||||||
|
note-info-date-created = Created
|
||||||
|
note-info-date-modified = Modified
|
||||||
|
note-info-size = Size
|
||||||
|
note-info-word-count = Word Count
|
||||||
|
note-info-character-count = Character Count
|
||||||
|
|
||||||
|
item-title-empty-note = Untitled Note
|
||||||
|
|
||||||
|
attachment-preview-placeholder = No attachment to preview
|
||||||
|
attachment-rename-from-parent =
|
||||||
|
.tooltiptext = Rename File to Match Parent Item
|
||||||
|
|
||||||
|
account-log-in = Log In
|
||||||
|
account-not-logged-in-text = Log in to your Zotero account to sync your data.
|
||||||
|
account-error-login-session-expired = Your login session has expired. Please try again.
|
||||||
|
|
||||||
|
|
||||||
|
toggle-preview =
|
||||||
|
.label = {
|
||||||
|
$type ->
|
||||||
|
[open] Hide
|
||||||
|
[collapsed] Show
|
||||||
|
*[unknown] Toggle
|
||||||
|
} Attachment Preview
|
||||||
|
|
||||||
|
annotation-image-not-available = [Image not available]
|
||||||
|
|
||||||
|
quicksearch-mode =
|
||||||
|
.aria-label = Quick Search mode
|
||||||
|
quicksearch-input =
|
||||||
|
.aria-label = Quick Search
|
||||||
|
.placeholder = { $placeholder }
|
||||||
|
.aria-description = { $placeholder }
|
||||||
|
|
||||||
|
advanced-search = Advanced Search
|
||||||
|
menuitem-advanced-search =
|
||||||
|
.label = { advanced-search }
|
||||||
|
quicksearch-advanced-search-button =
|
||||||
|
.tooltiptext = { advanced-search }
|
||||||
|
.aria-label = { advanced-search }
|
||||||
|
advanced-search-close =
|
||||||
|
.tooltiptext = Close Advanced Search
|
||||||
|
advanced-search-expand =
|
||||||
|
.tooltiptext = Expand Advanced Search
|
||||||
|
advanced-search-collapse =
|
||||||
|
.tooltiptext = Collapse Advanced Search
|
||||||
|
|
||||||
|
item-pane-header-view-as =
|
||||||
|
.label = View As
|
||||||
|
item-pane-header-none =
|
||||||
|
.label = None
|
||||||
|
item-pane-header-title =
|
||||||
|
.label = Title
|
||||||
|
item-pane-header-titleCreatorYear =
|
||||||
|
.label = Title, Creator, Year
|
||||||
|
item-pane-header-bibEntry =
|
||||||
|
.label = Bibliography Entry
|
||||||
|
item-pane-header-more-options =
|
||||||
|
.label = More Options
|
||||||
|
|
||||||
|
item-pane-message-items-selected = { $count ->
|
||||||
|
[0] No items selected
|
||||||
|
[one] {
|
||||||
@@ -14,6 +14,27 @@
|
|||||||
min-height: 0 !important;
|
min-height: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
collapsible-section.chatpapers-adaptive-height[open] {
|
||||||
|
--open-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
collapsible-section.chatpapers-adaptive-height[open] > :not(.head) .chatpapers-pane-body {
|
||||||
|
max-height: none;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
collapsible-section.chatpapers-fixed-height[open] > :not(.head) .chatpapers-pane-body {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chatpapers-window-chat-root,
|
||||||
|
#chatpapers-window-lecture-root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
collapsible-section:not([open]) > :not(.head) .chatpapers-root,
|
collapsible-section:not([open]) > :not(.head) .chatpapers-root,
|
||||||
collapsible-section:not([open]) > :not(.head) .chatpapers-lecture-root {
|
collapsible-section:not([open]) > :not(.head) .chatpapers-lecture-root {
|
||||||
min-height: 0 !important;
|
min-height: 0 !important;
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ prefs-title = ChatPapers
|
|||||||
|
|
||||||
item-section-chat-head = ChatPapers
|
item-section-chat-head = ChatPapers
|
||||||
item-section-chat-sidenav = Chat with paper
|
item-section-chat-sidenav = Chat with paper
|
||||||
|
item-pane-open-window = Open in separate window
|
||||||
|
item-pane-adaptive-height = Adaptive height
|
||||||
|
item-pane-adaptive-height-on = Adaptive height (on)
|
||||||
|
item-pane-adaptive-height-off = Adaptive height (off)
|
||||||
|
|
||||||
chat-loading = Loading ChatPapers…
|
chat-loading = Loading ChatPapers…
|
||||||
chat-placeholder = Ask a question about this paper…
|
chat-placeholder = Ask a question about this paper…
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ prefs-title = ChatPapers
|
|||||||
|
|
||||||
item-section-chat-head = ChatPapers
|
item-section-chat-head = ChatPapers
|
||||||
item-section-chat-sidenav = 论文对话
|
item-section-chat-sidenav = 论文对话
|
||||||
|
item-pane-open-window = 在独立窗口中打开
|
||||||
|
item-pane-adaptive-height = 自适应高度
|
||||||
|
item-pane-adaptive-height-on = 自适应高度(已开启)
|
||||||
|
item-pane-adaptive-height-off = 自适应高度(已关闭)
|
||||||
|
|
||||||
chat-loading = 正在加载 ChatPapers…
|
chat-loading = 正在加载 ChatPapers…
|
||||||
chat-placeholder = 就这篇论文提问…
|
chat-placeholder = 就这篇论文提问…
|
||||||
|
|||||||
@@ -20,3 +20,5 @@ pref("lectureTeacherStyle", "balanced");
|
|||||||
pref("mineruPath", "");
|
pref("mineruPath", "");
|
||||||
pref("piperPath", "");
|
pref("piperPath", "");
|
||||||
pref("piperModelPath", "");
|
pref("piperModelPath", "");
|
||||||
|
pref("chatPaneAdaptiveHeight", true);
|
||||||
|
pref("lecturePaneAdaptiveHeight", true);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { initLocale } from "./utils/locale";
|
import { initLocale } from "./utils/locale";
|
||||||
import { createZToolkit } from "./utils/ztoolkit";
|
import { createZToolkit } from "./utils/ztoolkit";
|
||||||
import { registerLecturePane } from "./modules/lecture/ui/registerLecturePane";
|
import { registerLecturePane } from "./modules/lecture/ui/registerLecturePane";
|
||||||
|
import { registerSelectionExplainPopup } from "./modules/lecture/ui/registerSelectionExplainPopup";
|
||||||
import { registerChatPane, registerPrefs } from "./modules/ui/readerPane";
|
import { registerChatPane, registerPrefs } from "./modules/ui/readerPane";
|
||||||
import { registerPrefsScripts } from "./modules/ui/prefs";
|
import { registerPrefsScripts } from "./modules/ui/prefs";
|
||||||
import { registerItemMenus } from "./modules/ui/itemMenu";
|
import { registerItemMenus } from "./modules/ui/itemMenu";
|
||||||
@@ -18,6 +19,7 @@ async function onStartup() {
|
|||||||
registerChatPane();
|
registerChatPane();
|
||||||
registerLecturePane();
|
registerLecturePane();
|
||||||
registerReaderSelectionHook();
|
registerReaderSelectionHook();
|
||||||
|
registerSelectionExplainPopup();
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Zotero.getMainWindows().map((win) => onMainWindowLoad(win)),
|
Zotero.getMainWindows().map((win) => onMainWindowLoad(win)),
|
||||||
|
|||||||
@@ -0,0 +1,246 @@
|
|||||||
|
import type { SummaryJson } from "../domain/types";
|
||||||
|
import { isPrimarilyEnglish } from "../infrastructure/llm/paragraphLlm";
|
||||||
|
import { generateSentenceExplanation } from "../infrastructure/llm/sentenceLlm";
|
||||||
|
import { ensureAudioDir } from "../infrastructure/storage/audioStorage";
|
||||||
|
import {
|
||||||
|
loadLectureData,
|
||||||
|
mergeLectureData,
|
||||||
|
type AdHocSelectionExplain,
|
||||||
|
type StoredParagraph,
|
||||||
|
} from "../infrastructure/storage/lectureStore";
|
||||||
|
import { audioFilePath } from "../infrastructure/storage/paths";
|
||||||
|
import { synthesizeSpeech } from "../infrastructure/tts/client";
|
||||||
|
import { TtsError } from "../infrastructure/tts/types";
|
||||||
|
import {
|
||||||
|
getRegularItem,
|
||||||
|
resolvePaperAttachment,
|
||||||
|
} from "../infrastructure/zotero/adapter";
|
||||||
|
import { paperIdFromCacheKey } from "../infrastructure/pdf/fileHash";
|
||||||
|
import type { TextChunk } from "../infrastructure/pdf/chunker";
|
||||||
|
import {
|
||||||
|
findMatchingPreparedSentence,
|
||||||
|
normalizeSelectionText,
|
||||||
|
} from "./matchPreparedSentence";
|
||||||
|
|
||||||
|
const TTS_ZH_MAX = 280;
|
||||||
|
const TTS_EN_MAX = 600;
|
||||||
|
|
||||||
|
export interface SelectionExplainResult {
|
||||||
|
original: string;
|
||||||
|
translation: string;
|
||||||
|
syntaxNote: string;
|
||||||
|
contextRole: string;
|
||||||
|
audioPathEn?: string;
|
||||||
|
audioPathZh: string;
|
||||||
|
page?: number;
|
||||||
|
fromCache: boolean;
|
||||||
|
fromPrepared: boolean;
|
||||||
|
matched?: {
|
||||||
|
paragraphId: string;
|
||||||
|
sentenceId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectionUnitId(text: string): string {
|
||||||
|
const norm = normalizeSelectionText(text);
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < norm.length; i++) {
|
||||||
|
hash = (hash * 31 + norm.charCodeAt(i)) | 0;
|
||||||
|
}
|
||||||
|
return `adhoc-${Math.abs(hash).toString(36)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findSurroundingContext(
|
||||||
|
chunks: TextChunk[] | undefined,
|
||||||
|
selectedText: string,
|
||||||
|
): string {
|
||||||
|
if (!chunks?.length || !selectedText.trim()) return "";
|
||||||
|
const needle = normalizeSelectionText(selectedText).slice(0, 48);
|
||||||
|
if (!needle) return "";
|
||||||
|
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
const hay = normalizeSelectionText(chunk.text);
|
||||||
|
const idx = hay.indexOf(needle);
|
||||||
|
if (idx < 0) continue;
|
||||||
|
const start = Math.max(0, idx - 220);
|
||||||
|
const end = Math.min(chunk.text.length, idx + selectedText.length + 220);
|
||||||
|
return chunk.text.slice(start, end);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function findAdHocCache(
|
||||||
|
entries: AdHocSelectionExplain[] | undefined,
|
||||||
|
selectedText: string,
|
||||||
|
): AdHocSelectionExplain | undefined {
|
||||||
|
if (!entries?.length) return undefined;
|
||||||
|
const norm = normalizeSelectionText(selectedText);
|
||||||
|
return entries.find(
|
||||||
|
(entry) => normalizeSelectionText(entry.original) === norm && entry.audioPathZh,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function preparedToResult(
|
||||||
|
match: NonNullable<ReturnType<typeof findMatchingPreparedSentence>>,
|
||||||
|
): SelectionExplainResult {
|
||||||
|
const { paragraph, sentence } = match;
|
||||||
|
return {
|
||||||
|
original: sentence.text,
|
||||||
|
translation: sentence.translation,
|
||||||
|
syntaxNote: "",
|
||||||
|
contextRole: paragraph.roleInPaper || paragraph.contextLink || "",
|
||||||
|
audioPathEn:
|
||||||
|
sentence.ttsEnStatus === "ready" ? sentence.audioPathEn : undefined,
|
||||||
|
audioPathZh: sentence.audioPathZh!,
|
||||||
|
page: sentence.page ?? paragraph.page,
|
||||||
|
fromCache: true,
|
||||||
|
fromPrepared: true,
|
||||||
|
matched: {
|
||||||
|
paragraphId: paragraph.id,
|
||||||
|
sentenceId: sentence.id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function adHocToResult(entry: AdHocSelectionExplain): SelectionExplainResult {
|
||||||
|
return {
|
||||||
|
original: entry.original,
|
||||||
|
translation: entry.translation,
|
||||||
|
syntaxNote: entry.syntaxNote,
|
||||||
|
contextRole: entry.contextRole,
|
||||||
|
audioPathEn: entry.audioPathEn,
|
||||||
|
audioPathZh: entry.audioPathZh!,
|
||||||
|
page: entry.page,
|
||||||
|
fromCache: true,
|
||||||
|
fromPrepared: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function synthesizeSelectionAudio(options: {
|
||||||
|
cacheKey: string;
|
||||||
|
unitId: string;
|
||||||
|
original: string;
|
||||||
|
translation: string;
|
||||||
|
}): Promise<{ audioPathEn?: string; audioPathZh: string }> {
|
||||||
|
await ensureAudioDir(options.cacheKey);
|
||||||
|
const en = isPrimarilyEnglish(options.original);
|
||||||
|
let audioPathEn: string | undefined;
|
||||||
|
|
||||||
|
if (en) {
|
||||||
|
const outPath = audioFilePath(options.cacheKey, `${options.unitId}-en`, "wav");
|
||||||
|
const result = await synthesizeSpeech({
|
||||||
|
text: options.original.slice(0, TTS_EN_MAX),
|
||||||
|
outputPath: outPath,
|
||||||
|
lang: "en",
|
||||||
|
});
|
||||||
|
audioPathEn = result.audioPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const zhPath = audioFilePath(options.cacheKey, `${options.unitId}-zh`, "wav");
|
||||||
|
const zhResult = await synthesizeSpeech({
|
||||||
|
text: options.translation.slice(0, TTS_ZH_MAX),
|
||||||
|
outputPath: zhPath,
|
||||||
|
lang: "zh",
|
||||||
|
});
|
||||||
|
|
||||||
|
return { audioPathEn, audioPathZh: zhResult.audioPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function explainSelectionLecture(options: {
|
||||||
|
item: Zotero.Item;
|
||||||
|
selectedText: string;
|
||||||
|
onProgress?: (message: string) => void;
|
||||||
|
}): Promise<SelectionExplainResult> {
|
||||||
|
const selectedText = options.selectedText.trim();
|
||||||
|
if (!selectedText) {
|
||||||
|
throw new Error("请先选中 PDF 中的文本。");
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = await resolvePaperAttachment(options.item);
|
||||||
|
const data = await loadLectureData(ctx.cacheKey);
|
||||||
|
const paragraphs = data?.paragraphs;
|
||||||
|
|
||||||
|
const prepared = findMatchingPreparedSentence(paragraphs, selectedText);
|
||||||
|
if (prepared?.sentence.audioPathZh) {
|
||||||
|
return preparedToResult(prepared);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached = findAdHocCache(data?.adHocSelections, selectedText);
|
||||||
|
if (cached?.audioPathZh) {
|
||||||
|
return adHocToResult(cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
options.onProgress?.("正在生成单句讲解…");
|
||||||
|
|
||||||
|
const parent = getRegularItem(options.item);
|
||||||
|
const title = String(parent.getField("title") || "");
|
||||||
|
const summary = data?.summary as SummaryJson | undefined;
|
||||||
|
const surroundingContext = findSurroundingContext(data?.chunks, selectedText);
|
||||||
|
|
||||||
|
const llm = await generateSentenceExplanation({
|
||||||
|
paperTitle: title,
|
||||||
|
summary,
|
||||||
|
selectedText,
|
||||||
|
surroundingContext,
|
||||||
|
});
|
||||||
|
|
||||||
|
options.onProgress?.("正在合成语音…");
|
||||||
|
|
||||||
|
const unitId = selectionUnitId(selectedText);
|
||||||
|
let audioPathEn: string | undefined;
|
||||||
|
let audioPathZh: string;
|
||||||
|
try {
|
||||||
|
const audio = await synthesizeSelectionAudio({
|
||||||
|
cacheKey: ctx.cacheKey,
|
||||||
|
unitId,
|
||||||
|
original: selectedText,
|
||||||
|
translation: llm.translation,
|
||||||
|
});
|
||||||
|
audioPathEn = audio.audioPathEn;
|
||||||
|
audioPathZh = audio.audioPathZh;
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof TtsError) throw e;
|
||||||
|
throw new Error(e instanceof Error ? e.message : String(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry: AdHocSelectionExplain = {
|
||||||
|
id: unitId,
|
||||||
|
original: selectedText,
|
||||||
|
translation: llm.translation,
|
||||||
|
syntaxNote: llm.syntaxNote,
|
||||||
|
contextRole: llm.contextRole,
|
||||||
|
audioPathEn,
|
||||||
|
audioPathZh,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const existing = data?.adHocSelections ?? [];
|
||||||
|
const nextEntries = [
|
||||||
|
entry,
|
||||||
|
...existing.filter(
|
||||||
|
(e) => normalizeSelectionText(e.original) !== normalizeSelectionText(selectedText),
|
||||||
|
),
|
||||||
|
].slice(0, 40);
|
||||||
|
|
||||||
|
await mergeLectureData(ctx.cacheKey, paperIdFromCacheKey(ctx.cacheKey), {
|
||||||
|
adHocSelections: nextEntries,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
original: selectedText,
|
||||||
|
translation: llm.translation,
|
||||||
|
syntaxNote: llm.syntaxNote,
|
||||||
|
contextRole: llm.contextRole,
|
||||||
|
audioPathEn,
|
||||||
|
audioPathZh,
|
||||||
|
fromCache: false,
|
||||||
|
fromPrepared: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findPreparedMatchForSelection(
|
||||||
|
paragraphs: StoredParagraph[] | undefined,
|
||||||
|
selectedText: string,
|
||||||
|
) {
|
||||||
|
return findMatchingPreparedSentence(paragraphs, selectedText);
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import type {
|
||||||
|
StoredParagraph,
|
||||||
|
StoredSentence,
|
||||||
|
} from "../infrastructure/storage/lectureStore";
|
||||||
|
|
||||||
|
export function normalizeSelectionText(text: string): string {
|
||||||
|
return text
|
||||||
|
.replace(/[\u0000-\u001f\f]/g, " ")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PreparedSentenceMatch {
|
||||||
|
paragraph: StoredParagraph;
|
||||||
|
sentence: StoredSentence;
|
||||||
|
}
|
||||||
|
|
||||||
|
function overlapScore(a: string, b: string): number {
|
||||||
|
const na = normalizeSelectionText(a);
|
||||||
|
const nb = normalizeSelectionText(b);
|
||||||
|
if (!na || !nb) return 0;
|
||||||
|
if (na === nb) return 100;
|
||||||
|
if (na.includes(nb) || nb.includes(na)) {
|
||||||
|
const shorter = Math.min(na.length, nb.length);
|
||||||
|
const longer = Math.max(na.length, nb.length);
|
||||||
|
return Math.round((shorter / longer) * 90);
|
||||||
|
}
|
||||||
|
|
||||||
|
const aWords = new Set(na.split(/\s+/).filter((w) => w.length > 2));
|
||||||
|
const bWords = nb.split(/\s+/).filter((w) => w.length > 2);
|
||||||
|
if (!aWords.size || !bWords.length) return 0;
|
||||||
|
let shared = 0;
|
||||||
|
for (const w of bWords) {
|
||||||
|
if (aWords.has(w)) shared += 1;
|
||||||
|
}
|
||||||
|
return Math.round((shared / bWords.length) * 70);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findMatchingPreparedSentence(
|
||||||
|
paragraphs: StoredParagraph[] | undefined,
|
||||||
|
selectedText: string,
|
||||||
|
options?: { requireTts?: boolean },
|
||||||
|
): PreparedSentenceMatch | undefined {
|
||||||
|
if (!paragraphs?.length || !selectedText.trim()) return undefined;
|
||||||
|
|
||||||
|
const requireTts = options?.requireTts !== false;
|
||||||
|
let best: PreparedSentenceMatch | undefined;
|
||||||
|
let bestScore = 0;
|
||||||
|
|
||||||
|
for (const paragraph of paragraphs) {
|
||||||
|
if (paragraph.genStatus !== "ready") continue;
|
||||||
|
|
||||||
|
for (const sentence of paragraph.sentences ?? []) {
|
||||||
|
if (requireTts && sentence.ttsZhStatus !== "ready") continue;
|
||||||
|
const score = overlapScore(selectedText, sentence.text);
|
||||||
|
if (score > bestScore) {
|
||||||
|
bestScore = score;
|
||||||
|
best = { paragraph, sentence };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!paragraph.sentences?.length) {
|
||||||
|
const score = overlapScore(selectedText, paragraph.originalText);
|
||||||
|
if (score > bestScore && (!requireTts || paragraph.ttsZhStatus === "ready")) {
|
||||||
|
bestScore = score;
|
||||||
|
best = {
|
||||||
|
paragraph,
|
||||||
|
sentence: {
|
||||||
|
id: `${paragraph.id}-whole`,
|
||||||
|
orderIndex: 0,
|
||||||
|
text: paragraph.originalText,
|
||||||
|
translation: paragraph.translation,
|
||||||
|
page: paragraph.page,
|
||||||
|
searchText: paragraph.searchText,
|
||||||
|
audioPathEn: paragraph.audioPathEn,
|
||||||
|
audioPathZh: paragraph.audioPathZh,
|
||||||
|
ttsEnStatus: paragraph.ttsEnStatus,
|
||||||
|
ttsZhStatus: paragraph.ttsZhStatus,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestScore >= 55 ? best : undefined;
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { pathToFileUrl } from "../infrastructure/platform/paths";
|
||||||
|
import type { SelectionExplainResult } from "./explainSelectionLecture";
|
||||||
|
|
||||||
|
function waitAudioEnded(audio: HTMLAudioElement): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const cleanup = () => {
|
||||||
|
audio.onended = null;
|
||||||
|
audio.onerror = null;
|
||||||
|
};
|
||||||
|
audio.onended = () => {
|
||||||
|
cleanup();
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
audio.onerror = () => {
|
||||||
|
cleanup();
|
||||||
|
reject(new Error("Audio playback failed"));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function audioPaths(result: SelectionExplainResult): string[] {
|
||||||
|
const paths: string[] = [];
|
||||||
|
if (result.audioPathEn) paths.push(result.audioPathEn);
|
||||||
|
if (result.audioPathZh) paths.push(result.audioPathZh);
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function playSelectionExplainAudio(
|
||||||
|
audio: HTMLAudioElement,
|
||||||
|
result: SelectionExplainResult,
|
||||||
|
): Promise<void> {
|
||||||
|
const paths = audioPaths(result);
|
||||||
|
if (!paths.length) return;
|
||||||
|
|
||||||
|
audio.pause();
|
||||||
|
for (const path of paths) {
|
||||||
|
audio.src = pathToFileUrl(path);
|
||||||
|
audio.load();
|
||||||
|
await audio.play();
|
||||||
|
await waitAudioEnded(audio);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let standaloneAudio: HTMLAudioElement | undefined;
|
||||||
|
|
||||||
|
function getStandaloneAudio(): HTMLAudioElement {
|
||||||
|
if (standaloneAudio) return standaloneAudio;
|
||||||
|
const win = Zotero.getMainWindow?.() as Window | undefined;
|
||||||
|
const doc = win?.document ?? document;
|
||||||
|
standaloneAudio = doc.createElement("audio");
|
||||||
|
standaloneAudio.preload = "none";
|
||||||
|
standaloneAudio.style.display = "none";
|
||||||
|
doc.body?.appendChild(standaloneAudio);
|
||||||
|
return standaloneAudio;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function playSelectionExplainStandalone(
|
||||||
|
result: SelectionExplainResult,
|
||||||
|
): Promise<void> {
|
||||||
|
await playSelectionExplainAudio(getStandaloneAudio(), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopSelectionExplainStandalone(): void {
|
||||||
|
standaloneAudio?.pause();
|
||||||
|
}
|
||||||
@@ -9,6 +9,12 @@ export type LectureEventMap = {
|
|||||||
lectureStatus: string;
|
lectureStatus: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
};
|
};
|
||||||
|
"lecture:selection_explain": {
|
||||||
|
paperId: string;
|
||||||
|
cacheKey: string;
|
||||||
|
itemId: string;
|
||||||
|
result: import("../application/explainSelectionLecture").SelectionExplainResult;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type Handler<K extends keyof LectureEventMap> = (
|
type Handler<K extends keyof LectureEventMap> = (
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { parseJsonLoose } from "../../../llm/jsonParse";
|
||||||
|
import { LlmError } from "../../../llm/types";
|
||||||
|
import type { SummaryJson } from "../../domain/types";
|
||||||
|
import { requestStructuredOutput } from "./structuredOutput";
|
||||||
|
|
||||||
|
const SENTENCE_SYSTEM = `你是 ChatPapers 论文伴读助手。用户选中论文中的一句原文,需要你解释并用于语音朗读。
|
||||||
|
必须只输出一个 JSON 对象,不要 markdown 代码块。
|
||||||
|
格式:{"translation":"...","syntaxNote":"...","contextRole":"..."}
|
||||||
|
|
||||||
|
要求:
|
||||||
|
- translation:准确中文翻译,并用 30–80 字口语解释该句含义(不要念 LaTeX,公式转述为口语)。
|
||||||
|
- syntaxNote:1–2 句,拆解句法结构或关键术语(英文句为主;中文句可简述逻辑)。
|
||||||
|
- contextRole:1 句,说明该句在段落或全文论证中的作用。
|
||||||
|
- 术语翻译与全文保持一致。`;
|
||||||
|
|
||||||
|
export interface SentenceLlmResult {
|
||||||
|
translation: string;
|
||||||
|
syntaxNote: string;
|
||||||
|
contextRole: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSentenceJson(raw: string): SentenceLlmResult {
|
||||||
|
const parsed = parseJsonLoose<Record<string, unknown>>(raw);
|
||||||
|
const translation = String(parsed.translation || parsed.zh || "").trim();
|
||||||
|
if (!translation) {
|
||||||
|
throw new LlmError("provider", "单句讲解 JSON 缺少 translation 字段。");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
translation,
|
||||||
|
syntaxNote: String(parsed.syntaxNote || parsed.syntax || "").trim(),
|
||||||
|
contextRole: String(parsed.contextRole || parsed.role || parsed.context || "").trim(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallbackSentence(selectedText: string): SentenceLlmResult {
|
||||||
|
return {
|
||||||
|
translation: selectedText.slice(0, 200),
|
||||||
|
syntaxNote: "",
|
||||||
|
contextRole: "(模型未能生成上下文说明,请重试或更换模型。)",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSummaryBlock(summary?: SummaryJson): string {
|
||||||
|
if (!summary) return "(暂无全文摘要)";
|
||||||
|
return [
|
||||||
|
`问题:${summary.problem}`,
|
||||||
|
`方法:${summary.method}`,
|
||||||
|
`结果:${summary.result}`,
|
||||||
|
`局限:${summary.limitation}`,
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateSentenceExplanation(options: {
|
||||||
|
paperTitle: string;
|
||||||
|
summary?: SummaryJson;
|
||||||
|
selectedText: string;
|
||||||
|
surroundingContext?: string;
|
||||||
|
}): Promise<SentenceLlmResult> {
|
||||||
|
const user = `【论文标题】${options.paperTitle}
|
||||||
|
|
||||||
|
【全文摘要】
|
||||||
|
${formatSummaryBlock(options.summary)}
|
||||||
|
|
||||||
|
【选区附近上下文(可能为空)】
|
||||||
|
${options.surroundingContext?.trim() || "(无)"}
|
||||||
|
|
||||||
|
【用户选中的句子】
|
||||||
|
${options.selectedText.trim()}
|
||||||
|
|
||||||
|
请输出 JSON。`;
|
||||||
|
|
||||||
|
return requestStructuredOutput({
|
||||||
|
system: SENTENCE_SYSTEM,
|
||||||
|
user,
|
||||||
|
parse: parseSentenceJson,
|
||||||
|
jsonMode: true,
|
||||||
|
fallback: () => fallbackSentence(options.selectedText),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -44,6 +44,18 @@ export interface StoredParagraph {
|
|||||||
genStatus: "pending" | "ready" | "failed";
|
genStatus: "pending" | "ready" | "failed";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdHocSelectionExplain {
|
||||||
|
id: string;
|
||||||
|
original: string;
|
||||||
|
translation: string;
|
||||||
|
syntaxNote: string;
|
||||||
|
contextRole: string;
|
||||||
|
page?: number;
|
||||||
|
audioPathEn?: string;
|
||||||
|
audioPathZh?: string;
|
||||||
|
createdAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface LectureData {
|
export interface LectureData {
|
||||||
paperId: string;
|
paperId: string;
|
||||||
cacheKey: string;
|
cacheKey: string;
|
||||||
@@ -53,6 +65,7 @@ export interface LectureData {
|
|||||||
chunks?: TextChunk[];
|
chunks?: TextChunk[];
|
||||||
beats?: StoredBeat[];
|
beats?: StoredBeat[];
|
||||||
paragraphs?: StoredParagraph[];
|
paragraphs?: StoredParagraph[];
|
||||||
|
adHocSelections?: AdHocSelectionExplain[];
|
||||||
noteItemId?: string;
|
noteItemId?: string;
|
||||||
extractChars?: number;
|
extractChars?: number;
|
||||||
extractSource?: string;
|
extractSource?: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Headphones, Pause, Play, SkipBack, SkipForward, Sparkles, Volume2 } from "lucide";
|
import { Headphones, Highlighter, Pause, Play, SkipBack, SkipForward, Sparkles, Volume2 } from "lucide";
|
||||||
import type { LectureStatus, PaperCache } from "../domain/types";
|
import type { LectureStatus, PaperCache } from "../domain/types";
|
||||||
import { prepareLecture } from "../application/prepareLecture";
|
import { prepareLecture } from "../application/prepareLecture";
|
||||||
import {
|
import {
|
||||||
@@ -42,6 +42,12 @@ import {
|
|||||||
import { paperIdFromCacheKey } from "../infrastructure/pdf/fileHash";
|
import { paperIdFromCacheKey } from "../infrastructure/pdf/fileHash";
|
||||||
import { chunkById } from "../infrastructure/pdf/chunker";
|
import { chunkById } from "../infrastructure/pdf/chunker";
|
||||||
import { highlightTextInPdf } from "../../pdf/readerSync";
|
import { highlightTextInPdf } from "../../pdf/readerSync";
|
||||||
|
import { getReaderSelectedText } from "../../pdf/selection";
|
||||||
|
import {
|
||||||
|
explainSelectionLecture,
|
||||||
|
type SelectionExplainResult,
|
||||||
|
} from "../application/explainSelectionLecture";
|
||||||
|
import { playSelectionExplainAudio } from "../application/playSelectionExplain";
|
||||||
import { getString } from "../../../utils/locale";
|
import { getString } from "../../../utils/locale";
|
||||||
import { createLucideIcon } from "../../../utils/icons";
|
import { createLucideIcon } from "../../../utils/icons";
|
||||||
|
|
||||||
@@ -62,12 +68,15 @@ export class LecturePaneView {
|
|||||||
private activeParagraphId?: string;
|
private activeParagraphId?: string;
|
||||||
private activeSentenceId?: string;
|
private activeSentenceId?: string;
|
||||||
private pdfSyncEnabled = true;
|
private pdfSyncEnabled = true;
|
||||||
|
private explainingSelection = false;
|
||||||
|
private lastSelectionResult?: SelectionExplainResult;
|
||||||
private lectureChunks?: import("../infrastructure/pdf/chunker").TextChunk[];
|
private lectureChunks?: import("../infrastructure/pdf/chunker").TextChunk[];
|
||||||
|
|
||||||
private statusEl!: HTMLElement;
|
private statusEl!: HTMLElement;
|
||||||
private statusTextEl!: HTMLElement;
|
private statusTextEl!: HTMLElement;
|
||||||
private ttsMetaEl!: HTMLElement;
|
private ttsMetaEl!: HTMLElement;
|
||||||
private actionBtn!: HTMLButtonElement;
|
private actionBtn!: HTMLButtonElement;
|
||||||
|
private explainSelectionBtn!: HTMLButtonElement;
|
||||||
private ttsTestBtn!: HTMLButtonElement;
|
private ttsTestBtn!: HTMLButtonElement;
|
||||||
private noteBtn!: HTMLButtonElement;
|
private noteBtn!: HTMLButtonElement;
|
||||||
private hintEl!: HTMLElement;
|
private hintEl!: HTMLElement;
|
||||||
@@ -76,6 +85,7 @@ export class LecturePaneView {
|
|||||||
private granularityTabsEl!: HTMLElement;
|
private granularityTabsEl!: HTMLElement;
|
||||||
private beatListEl!: HTMLElement;
|
private beatListEl!: HTMLElement;
|
||||||
private paragraphDetailEl!: HTMLElement;
|
private paragraphDetailEl!: HTMLElement;
|
||||||
|
private selectionExplainEl!: HTMLElement;
|
||||||
private playerTitleEl!: HTMLElement;
|
private playerTitleEl!: HTMLElement;
|
||||||
private playBtn!: HTMLButtonElement;
|
private playBtn!: HTMLButtonElement;
|
||||||
private pauseBtn!: HTMLButtonElement;
|
private pauseBtn!: HTMLButtonElement;
|
||||||
@@ -154,6 +164,13 @@ export class LecturePaneView {
|
|||||||
this.noteBtn.textContent = getString("lecture-save-note");
|
this.noteBtn.textContent = getString("lecture-save-note");
|
||||||
this.noteBtn.addEventListener("click", () => void this.onSaveNote());
|
this.noteBtn.addEventListener("click", () => void this.onSaveNote());
|
||||||
|
|
||||||
|
this.explainSelectionBtn = this.makeActionBtn(
|
||||||
|
Highlighter,
|
||||||
|
getString("lecture-explain-selection"),
|
||||||
|
() => void this.onExplainSelection(),
|
||||||
|
{ preserveSelection: true },
|
||||||
|
);
|
||||||
|
|
||||||
this.playerSection = this.el("div", "chatpapers-lecture-player");
|
this.playerSection = this.el("div", "chatpapers-lecture-player");
|
||||||
this.playerSection.hidden = true;
|
this.playerSection.hidden = true;
|
||||||
|
|
||||||
@@ -175,6 +192,8 @@ export class LecturePaneView {
|
|||||||
this.playerTitleEl = playerTitle;
|
this.playerTitleEl = playerTitle;
|
||||||
this.beatListEl = this.el("div", "chatpapers-lecture-beat-list");
|
this.beatListEl = this.el("div", "chatpapers-lecture-beat-list");
|
||||||
this.paragraphDetailEl = this.el("div", "chatpapers-lecture-para-detail");
|
this.paragraphDetailEl = this.el("div", "chatpapers-lecture-para-detail");
|
||||||
|
this.selectionExplainEl = this.el("div", "chatpapers-lecture-selection-explain");
|
||||||
|
this.selectionExplainEl.hidden = true;
|
||||||
|
|
||||||
const playerControls = this.el("div", "chatpapers-lecture-player-controls");
|
const playerControls = this.el("div", "chatpapers-lecture-player-controls");
|
||||||
|
|
||||||
@@ -203,6 +222,7 @@ export class LecturePaneView {
|
|||||||
playerTitle,
|
playerTitle,
|
||||||
this.beatListEl,
|
this.beatListEl,
|
||||||
this.paragraphDetailEl,
|
this.paragraphDetailEl,
|
||||||
|
this.selectionExplainEl,
|
||||||
playerControls,
|
playerControls,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -215,7 +235,12 @@ export class LecturePaneView {
|
|||||||
this.updatePlatformHint();
|
this.updatePlatformHint();
|
||||||
|
|
||||||
const actions = this.el("div", "chatpapers-lecture-actions");
|
const actions = this.el("div", "chatpapers-lecture-actions");
|
||||||
actions.append(this.actionBtn, this.noteBtn, this.ttsTestBtn);
|
actions.append(
|
||||||
|
this.actionBtn,
|
||||||
|
this.explainSelectionBtn,
|
||||||
|
this.noteBtn,
|
||||||
|
this.ttsTestBtn,
|
||||||
|
);
|
||||||
|
|
||||||
this.body.append(
|
this.body.append(
|
||||||
header,
|
header,
|
||||||
@@ -277,9 +302,42 @@ export class LecturePaneView {
|
|||||||
if (!this.shouldHandleEvent(d.paperId)) return;
|
if (!this.shouldHandleEvent(d.paperId)) return;
|
||||||
void this.refreshPlayer();
|
void this.refreshPlayer();
|
||||||
}),
|
}),
|
||||||
|
lectureEvents.on("lecture:selection_explain", (d) => {
|
||||||
|
if (String(getRegularItem(this.item).id) !== d.itemId) return;
|
||||||
|
void this.applySelectionExplainResult(d.result);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private makeActionBtn(
|
||||||
|
icon: typeof Play,
|
||||||
|
label: string,
|
||||||
|
onClick: () => void,
|
||||||
|
options?: { preserveSelection?: boolean },
|
||||||
|
): HTMLButtonElement {
|
||||||
|
const btn = this.doc.createElement("button");
|
||||||
|
btn.type = "button";
|
||||||
|
btn.className = "chatpapers-lecture-secondary";
|
||||||
|
btn.title = label;
|
||||||
|
btn.append(
|
||||||
|
createLucideIcon(this.doc, icon, {
|
||||||
|
size: 14,
|
||||||
|
className: "chatpapers-lecture-btn-icon",
|
||||||
|
}),
|
||||||
|
this.doc.createTextNode(label),
|
||||||
|
);
|
||||||
|
if (options?.preserveSelection) {
|
||||||
|
btn.addEventListener("mousedown", (ev) => {
|
||||||
|
if ((ev as MouseEvent).button !== 0) return;
|
||||||
|
ev.preventDefault();
|
||||||
|
onClick();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
btn.addEventListener("click", onClick);
|
||||||
|
}
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
private makeModeTab(
|
private makeModeTab(
|
||||||
mode: "overview" | "paragraph",
|
mode: "overview" | "paragraph",
|
||||||
label: string,
|
label: string,
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ import { getLocaleID, getString } from "../../../utils/locale";
|
|||||||
import { findPdfAttachment } from "../../pdf/extractor";
|
import { findPdfAttachment } from "../../pdf/extractor";
|
||||||
import { ensureChatPapersStyles } from "../../ui/readerPane";
|
import { ensureChatPapersStyles } from "../../ui/readerPane";
|
||||||
import {
|
import {
|
||||||
|
applyAdaptiveHeight,
|
||||||
|
buildItemPaneSectionButtons,
|
||||||
onItemPaneSectionToggle,
|
onItemPaneSectionToggle,
|
||||||
prepareItemPaneBody,
|
prepareItemPaneBody,
|
||||||
|
refreshSectionOpenHeight,
|
||||||
} from "../../ui/itemPaneSection";
|
} from "../../ui/itemPaneSection";
|
||||||
import { LecturePaneView } from "./lecturePane";
|
import { LecturePaneView } from "./lecturePane";
|
||||||
|
|
||||||
@@ -26,10 +29,12 @@ export function registerLecturePane() {
|
|||||||
l10nID: getLocaleID("item-section-lecture-sidenav"),
|
l10nID: getLocaleID("item-section-lecture-sidenav"),
|
||||||
icon: iconURL("lecture.svg"),
|
icon: iconURL("lecture.svg"),
|
||||||
},
|
},
|
||||||
|
sectionButtons: buildItemPaneSectionButtons("lecture"),
|
||||||
onInit: ({ body }) => {
|
onInit: ({ body }) => {
|
||||||
const doc = body.ownerDocument;
|
const doc = body.ownerDocument;
|
||||||
if (doc) ensureChatPapersStyles(doc);
|
if (doc) ensureChatPapersStyles(doc);
|
||||||
prepareItemPaneBody(body);
|
prepareItemPaneBody(body);
|
||||||
|
applyAdaptiveHeight(body, "lecture");
|
||||||
},
|
},
|
||||||
onDestroy: ({ body }) => {
|
onDestroy: ({ body }) => {
|
||||||
views.get(body)?.destroy();
|
views.get(body)?.destroy();
|
||||||
@@ -65,8 +70,11 @@ export function registerLecturePane() {
|
|||||||
err.textContent = getString("lecture-mount-error");
|
err.textContent = getString("lecture-mount-error");
|
||||||
body.append(err);
|
body.append(err);
|
||||||
}
|
}
|
||||||
|
applyAdaptiveHeight(body, "lecture");
|
||||||
const section = body.closest("collapsible-section");
|
const section = body.closest("collapsible-section");
|
||||||
if (section && !section.hasAttribute("open")) {
|
if (section?.hasAttribute("open")) {
|
||||||
|
refreshSectionOpenHeight(body);
|
||||||
|
} else if (section) {
|
||||||
onItemPaneSectionToggle({ body });
|
onItemPaneSectionToggle({ body });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { config } from "../../../../package.json";
|
||||||
|
import { getString } from "../../../utils/locale";
|
||||||
|
import { cacheReaderSelection } from "../../pdf/selection";
|
||||||
|
import { highlightTextInPdf } from "../../pdf/readerSync";
|
||||||
|
import { getRegularItem, resolvePaperAttachment } from "../infrastructure/zotero/adapter";
|
||||||
|
import { paperIdFromCacheKey } from "../infrastructure/pdf/fileHash";
|
||||||
|
import { explainSelectionLecture } from "../application/explainSelectionLecture";
|
||||||
|
import { playSelectionExplainStandalone } from "../application/playSelectionExplain";
|
||||||
|
import { lectureEvents } from "../infrastructure/events";
|
||||||
|
|
||||||
|
function tryString(value: unknown): string {
|
||||||
|
if (typeof value === "string") return value.trim();
|
||||||
|
if (value == null) return "";
|
||||||
|
return String(value).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function itemFromReader(reader: any): Zotero.Item | undefined {
|
||||||
|
const attachmentId = reader?.itemID ?? reader?._itemID;
|
||||||
|
if (!attachmentId) return undefined;
|
||||||
|
const attachment = Zotero.Items.get(attachmentId);
|
||||||
|
if (!attachment) return undefined;
|
||||||
|
return getRegularItem(attachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showHeadline(text: string): void {
|
||||||
|
try {
|
||||||
|
const pw = new Zotero.ProgressWindow({ closeOnClick: true });
|
||||||
|
pw.changeHeadline(text);
|
||||||
|
pw.show();
|
||||||
|
pw.startCloseTimer(4000);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runSelectionExplainFromReader(
|
||||||
|
reader: any,
|
||||||
|
selectedText: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const text = tryString(selectedText);
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
const item = itemFromReader(reader);
|
||||||
|
if (!item) {
|
||||||
|
showHeadline(getString("lecture-selection-no-item"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheReaderSelection(text);
|
||||||
|
showHeadline(getString("lecture-selection-explaining"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ctx = await resolvePaperAttachment(item);
|
||||||
|
const result = await explainSelectionLecture({
|
||||||
|
item,
|
||||||
|
selectedText: text,
|
||||||
|
onProgress: (message) => showHeadline(message),
|
||||||
|
});
|
||||||
|
|
||||||
|
await highlightTextInPdf(item, {
|
||||||
|
text: result.original,
|
||||||
|
page: result.page,
|
||||||
|
});
|
||||||
|
|
||||||
|
lectureEvents.emit("lecture:selection_explain", {
|
||||||
|
paperId: paperIdFromCacheKey(ctx.cacheKey),
|
||||||
|
cacheKey: ctx.cacheKey,
|
||||||
|
itemId: String(getRegularItem(item).id),
|
||||||
|
result,
|
||||||
|
});
|
||||||
|
|
||||||
|
await playSelectionExplainStandalone(result);
|
||||||
|
showHeadline(getString("lecture-selection-done"));
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
|
ztoolkit.log("[ChatPapers:Lecture] selection explain failed", e);
|
||||||
|
showHeadline(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerSelectionExplainPopup(): void {
|
||||||
|
try {
|
||||||
|
Zotero.Reader.registerEventListener(
|
||||||
|
"renderTextSelectionPopup",
|
||||||
|
(event: any) => {
|
||||||
|
try {
|
||||||
|
const { doc, params, append, reader } = event;
|
||||||
|
const text =
|
||||||
|
tryString(params?.annotation?.text) || tryString(params?.text);
|
||||||
|
if (!text) return;
|
||||||
|
cacheReaderSelection(text);
|
||||||
|
|
||||||
|
const btn = doc.createElement("button");
|
||||||
|
btn.type = "button";
|
||||||
|
btn.className =
|
||||||
|
"toolbar-button wide-button chatpapers-selection-explain-btn";
|
||||||
|
btn.textContent = getString("lecture-explain-selection-short");
|
||||||
|
btn.title = getString("lecture-explain-selection");
|
||||||
|
btn.addEventListener("click", (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
void runSelectionExplainFromReader(reader, text);
|
||||||
|
});
|
||||||
|
append(btn);
|
||||||
|
} catch (e) {
|
||||||
|
ztoolkit.log("[ChatPapers:Lecture] selection popup button failed", e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
config.addonID,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
ztoolkit.log("[ChatPapers:Lecture] registerSelectionExplainPopup failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,104 @@
|
|||||||
/** Helpers for Zotero ItemPane collapsible sections. */
|
/** Helpers for Zotero ItemPane collapsible sections. */
|
||||||
|
|
||||||
|
import { getLocaleID, getString } from "../../utils/locale";
|
||||||
|
import { getPref, setPref } from "../../utils/prefs";
|
||||||
|
import { openChatPaneWindow, openLecturePaneWindow } from "./itemPaneWindow";
|
||||||
|
|
||||||
|
export type ItemPaneKind = "chat" | "lecture";
|
||||||
|
|
||||||
|
const ADAPTIVE_PREF: Record<ItemPaneKind, keyof _ZoteroTypes.Prefs["PluginPrefsMap"]> =
|
||||||
|
{
|
||||||
|
chat: "chatPaneAdaptiveHeight",
|
||||||
|
lecture: "lecturePaneAdaptiveHeight",
|
||||||
|
};
|
||||||
|
|
||||||
|
const FIXED_HEIGHT: Record<ItemPaneKind, number> = {
|
||||||
|
chat: 360,
|
||||||
|
lecture: 420,
|
||||||
|
};
|
||||||
|
|
||||||
|
const OPEN_WINDOW_ICON = "chrome://zotero/skin/16/universal/open.svg";
|
||||||
|
const ADAPTIVE_ICON = "chrome://zotero/skin/16/universal/expand-all.svg";
|
||||||
|
const ADAPTIVE_ICON_OFF = "chrome://zotero/skin/16/universal/collapse-all.svg";
|
||||||
|
|
||||||
export function prepareItemPaneBody(body: HTMLElement): void {
|
export function prepareItemPaneBody(body: HTMLElement): void {
|
||||||
body.classList.add("chatpapers-pane-body");
|
body.classList.add("chatpapers-pane-body");
|
||||||
body.style.minHeight = "0";
|
body.style.minHeight = "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isAdaptiveHeight(kind: ItemPaneKind): boolean {
|
||||||
|
const val = getPref(ADAPTIVE_PREF[kind]);
|
||||||
|
return val !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyAdaptiveHeight(
|
||||||
|
body: HTMLElement,
|
||||||
|
kind: ItemPaneKind,
|
||||||
|
adaptive = isAdaptiveHeight(kind),
|
||||||
|
): void {
|
||||||
|
body.classList.toggle("chatpapers-pane-adaptive", adaptive);
|
||||||
|
body.classList.toggle("chatpapers-pane-fixed", !adaptive);
|
||||||
|
body.dataset.chatpapersPaneKind = kind;
|
||||||
|
|
||||||
|
const section = resolveCollapsibleSection(body);
|
||||||
|
section?.classList.toggle("chatpapers-adaptive-height", adaptive);
|
||||||
|
section?.classList.toggle("chatpapers-fixed-height", !adaptive);
|
||||||
|
updateAdaptiveButtonState(body, adaptive);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleAdaptiveHeight(
|
||||||
|
body: HTMLElement,
|
||||||
|
kind: ItemPaneKind,
|
||||||
|
): boolean {
|
||||||
|
const next = !isAdaptiveHeight(kind);
|
||||||
|
setPref(ADAPTIVE_PREF[kind], next);
|
||||||
|
applyAdaptiveHeight(body, kind, next);
|
||||||
|
refreshSectionOpenHeight(body);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function refreshSectionOpenHeight(body: HTMLElement): void {
|
||||||
|
const section = resolveCollapsibleSection(body);
|
||||||
|
if (!section?.hasAttribute("open")) return;
|
||||||
|
|
||||||
|
if (section.classList.contains("chatpapers-adaptive-height")) {
|
||||||
|
section.style.setProperty("--open-height", "auto");
|
||||||
|
body.style.removeProperty("max-height");
|
||||||
|
body.style.removeProperty("overflow");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kind = (body.dataset.chatpapersPaneKind || "chat") as ItemPaneKind;
|
||||||
|
const maxH = FIXED_HEIGHT[kind] ?? 360;
|
||||||
|
body.style.maxHeight = `${maxH}px`;
|
||||||
|
body.style.overflowY = "auto";
|
||||||
|
section.style.setProperty("--open-height", `${maxH}px`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateAdaptiveButtonState(
|
||||||
|
body: HTMLElement,
|
||||||
|
adaptive: boolean,
|
||||||
|
): void {
|
||||||
|
const section = resolveCollapsibleSection(body);
|
||||||
|
const btn = section?.querySelector(
|
||||||
|
".adaptive-height.section-custom-button",
|
||||||
|
) as HTMLElement | null;
|
||||||
|
if (!btn) return;
|
||||||
|
btn.setAttribute("aria-pressed", adaptive ? "true" : "false");
|
||||||
|
btn.classList.toggle("chatpapers-adaptive-active", adaptive);
|
||||||
|
btn.style.setProperty(
|
||||||
|
"--custom-button-icon-light",
|
||||||
|
`url('${adaptive ? ADAPTIVE_ICON : ADAPTIVE_ICON_OFF}')`,
|
||||||
|
);
|
||||||
|
btn.style.setProperty(
|
||||||
|
"--custom-button-icon-dark",
|
||||||
|
`url('${adaptive ? ADAPTIVE_ICON : ADAPTIVE_ICON_OFF}')`,
|
||||||
|
);
|
||||||
|
btn.title = getString(
|
||||||
|
adaptive ? "item-pane-adaptive-height-on" : "item-pane-adaptive-height-off",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function onItemPaneSectionToggle(options: {
|
export function onItemPaneSectionToggle(options: {
|
||||||
body: HTMLElement;
|
body: HTMLElement;
|
||||||
event?: Event;
|
event?: Event;
|
||||||
@@ -13,28 +107,57 @@ export function onItemPaneSectionToggle(options: {
|
|||||||
if (!section) return;
|
if (!section) return;
|
||||||
|
|
||||||
const open = section.hasAttribute("open");
|
const open = section.hasAttribute("open");
|
||||||
|
const adaptive = section.classList.contains("chatpapers-adaptive-height");
|
||||||
|
|
||||||
if (open) {
|
if (open) {
|
||||||
options.body.style.removeProperty("height");
|
options.body.style.removeProperty("height");
|
||||||
options.body.style.removeProperty("overflow");
|
|
||||||
options.body.style.minHeight = "0";
|
options.body.style.minHeight = "0";
|
||||||
const raf = options.body.ownerDocument?.defaultView?.requestAnimationFrame;
|
|
||||||
const updateHeight = () => {
|
if (adaptive) {
|
||||||
const h = options.body.scrollHeight;
|
options.body.style.removeProperty("overflow");
|
||||||
if (h > 0) {
|
options.body.style.removeProperty("max-height");
|
||||||
section.style.setProperty("--open-height", `${h}px`);
|
section.style.setProperty("--open-height", "auto");
|
||||||
}
|
return;
|
||||||
};
|
}
|
||||||
if (raf) raf(updateHeight);
|
|
||||||
else updateHeight();
|
options.body.style.overflowY = "auto";
|
||||||
|
const kind = (options.body.dataset.chatpapersPaneKind || "chat") as ItemPaneKind;
|
||||||
|
const maxH = FIXED_HEIGHT[kind] ?? 360;
|
||||||
|
options.body.style.maxHeight = `${maxH}px`;
|
||||||
|
section.style.setProperty("--open-height", `${maxH}px`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
options.body.style.minHeight = "0";
|
options.body.style.minHeight = "0";
|
||||||
options.body.style.height = "0";
|
options.body.style.height = "0";
|
||||||
options.body.style.overflow = "hidden";
|
options.body.style.overflow = "hidden";
|
||||||
|
options.body.style.removeProperty("max-height");
|
||||||
section.style.setProperty("--open-height", "0px");
|
section.style.setProperty("--open-height", "0px");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildItemPaneSectionButtons(kind: ItemPaneKind) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: "open-in-window",
|
||||||
|
icon: OPEN_WINDOW_ICON,
|
||||||
|
l10nID: getLocaleID("item-pane-open-window"),
|
||||||
|
onClick: ({ item }: { item?: Zotero.Item }) => {
|
||||||
|
if (!item) return;
|
||||||
|
if (kind === "chat") openChatPaneWindow(item);
|
||||||
|
else openLecturePaneWindow(item);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "adaptive-height",
|
||||||
|
icon: ADAPTIVE_ICON,
|
||||||
|
l10nID: getLocaleID("item-pane-adaptive-height"),
|
||||||
|
onClick: ({ body }: { body: HTMLElement }) => {
|
||||||
|
toggleAdaptiveHeight(body, kind);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function resolveCollapsibleSection(
|
function resolveCollapsibleSection(
|
||||||
body: HTMLElement,
|
body: HTMLElement,
|
||||||
event?: Event,
|
event?: Event,
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
import { getString } from "../../utils/locale";
|
||||||
|
import { ChatView } from "./chatView";
|
||||||
|
import { LecturePaneView } from "../lecture/ui/lecturePane";
|
||||||
|
import { ensureChatPapersStyles } from "./readerPane";
|
||||||
|
|
||||||
|
type PaneKind = "chat" | "lecture";
|
||||||
|
|
||||||
|
interface OpenPaneWindow {
|
||||||
|
dialog: any;
|
||||||
|
view: ChatView | LecturePaneView;
|
||||||
|
onResize?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const openWindows = new Map<string, OpenPaneWindow>();
|
||||||
|
|
||||||
|
function windowKey(kind: PaneKind, itemId: number): string {
|
||||||
|
return `${kind}:${itemId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureDialogStyles(doc: Document): void {
|
||||||
|
ensureChatPapersStyles(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPreferredDialogSize(): { width: number; height: number } {
|
||||||
|
try {
|
||||||
|
const win = Zotero.getMainWindow?.() as Window | undefined;
|
||||||
|
const screen = win?.screen;
|
||||||
|
const aw = screen?.availWidth || win?.outerWidth || 1280;
|
||||||
|
const ah = screen?.availHeight || win?.outerHeight || 800;
|
||||||
|
return {
|
||||||
|
width: Math.max(720, Math.min(Math.round(aw * 0.82), aw - 48)),
|
||||||
|
height: Math.max(560, Math.min(Math.round(ah * 0.82), ah - 48)),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { width: 920, height: 680 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideDialogButtonBox(doc: Document): void {
|
||||||
|
for (const sel of [
|
||||||
|
"#dialog-button-box",
|
||||||
|
".dialog-button-box",
|
||||||
|
"dialog > vbox.dialog-button-box",
|
||||||
|
"[anonid='buttons']",
|
||||||
|
]) {
|
||||||
|
for (const node of Array.from(doc.querySelectorAll(sel))) {
|
||||||
|
(node as HTMLElement).style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fitPaneRoot(win: Window, rootId: string): void {
|
||||||
|
const doc = win.document;
|
||||||
|
hideDialogButtonBox(doc);
|
||||||
|
const root = doc.getElementById(rootId) as HTMLElement | null;
|
||||||
|
if (!root) return;
|
||||||
|
|
||||||
|
for (const el of [doc.documentElement, doc.body]) {
|
||||||
|
if (!el) continue;
|
||||||
|
(el as HTMLElement).style.height = "100%";
|
||||||
|
(el as HTMLElement).style.margin = "0";
|
||||||
|
(el as HTMLElement).style.padding = "0";
|
||||||
|
(el as HTMLElement).style.overflow = "hidden";
|
||||||
|
(el as HTMLElement).style.display = "flex";
|
||||||
|
(el as HTMLElement).style.flexDirection = "column";
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node of Array.from(doc.querySelectorAll("dialog"))) {
|
||||||
|
(node as HTMLElement).style.padding = "0";
|
||||||
|
(node as HTMLElement).style.margin = "0";
|
||||||
|
(node as HTMLElement).style.height = "100%";
|
||||||
|
}
|
||||||
|
|
||||||
|
const h = Math.max(400, (win.innerHeight || 600) - 8);
|
||||||
|
root.style.width = "100%";
|
||||||
|
root.style.height = `${h}px`;
|
||||||
|
root.style.maxHeight = `${h}px`;
|
||||||
|
root.style.minHeight = "0";
|
||||||
|
root.style.flex = "1 1 auto";
|
||||||
|
root.style.overflow = "hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusExisting(key: string): boolean {
|
||||||
|
const existing = openWindows.get(key);
|
||||||
|
const win = existing?.dialog?.window as Window | undefined;
|
||||||
|
if (!win) return false;
|
||||||
|
try {
|
||||||
|
win.focus();
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
openWindows.delete(key);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeWindow(key: string): void {
|
||||||
|
const existing = openWindows.get(key);
|
||||||
|
if (!existing) return;
|
||||||
|
try {
|
||||||
|
if (existing.dialog?.window && existing.onResize) {
|
||||||
|
existing.dialog.window.removeEventListener("resize", existing.onResize);
|
||||||
|
}
|
||||||
|
existing.view.destroy();
|
||||||
|
existing.dialog?.window?.close?.();
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
openWindows.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPaneWindow(options: {
|
||||||
|
kind: PaneKind;
|
||||||
|
item: Zotero.Item;
|
||||||
|
rootId: string;
|
||||||
|
title: string;
|
||||||
|
mount: (doc: Document, root: HTMLElement) => Promise<ChatView | LecturePaneView>;
|
||||||
|
}): void {
|
||||||
|
const key = windowKey(options.kind, options.item.id);
|
||||||
|
if (focusExisting(key)) return;
|
||||||
|
|
||||||
|
closeWindow(key);
|
||||||
|
|
||||||
|
const size = getPreferredDialogSize();
|
||||||
|
let activeView: ChatView | LecturePaneView | null = null;
|
||||||
|
let onResize: (() => void) | null = null;
|
||||||
|
|
||||||
|
const dialogHelper = new ztoolkit.Dialog(1, 1)
|
||||||
|
.addCell(0, 0, {
|
||||||
|
tag: "div",
|
||||||
|
namespace: "html",
|
||||||
|
id: options.rootId,
|
||||||
|
styles: {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
minWidth: "560px",
|
||||||
|
minHeight: "0",
|
||||||
|
overflow: "hidden",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.setDialogData({
|
||||||
|
loadCallback: () => {
|
||||||
|
try {
|
||||||
|
const win = dialogHelper.window as Window;
|
||||||
|
const doc = win.document;
|
||||||
|
ensureDialogStyles(doc);
|
||||||
|
fitPaneRoot(win, options.rootId);
|
||||||
|
onResize = () => fitPaneRoot(win, options.rootId);
|
||||||
|
win.addEventListener("resize", onResize);
|
||||||
|
|
||||||
|
const root = doc.getElementById(options.rootId) as HTMLElement | null;
|
||||||
|
if (!root) return;
|
||||||
|
void options.mount(doc, root).then((view) => {
|
||||||
|
activeView = view;
|
||||||
|
openWindows.set(key, {
|
||||||
|
dialog: dialogHelper,
|
||||||
|
view,
|
||||||
|
onResize: onResize ?? undefined,
|
||||||
|
});
|
||||||
|
fitPaneRoot(win, options.rootId);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
ztoolkit.log("[ChatPapers] openPaneWindow load failed", e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unloadCallback: () => {
|
||||||
|
try {
|
||||||
|
if (dialogHelper.window && onResize) {
|
||||||
|
dialogHelper.window.removeEventListener("resize", onResize);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
activeView?.destroy();
|
||||||
|
activeView = null;
|
||||||
|
onResize = null;
|
||||||
|
openWindows.delete(key);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
openWindows.set(key, {
|
||||||
|
dialog: dialogHelper,
|
||||||
|
view: null as unknown as ChatView,
|
||||||
|
onResize: onResize ?? undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogHelper.open(options.title, {
|
||||||
|
centerscreen: true,
|
||||||
|
resizable: true,
|
||||||
|
noDialogMode: true,
|
||||||
|
fitContent: false,
|
||||||
|
width: size.width,
|
||||||
|
height: size.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openChatPaneWindow(item: Zotero.Item): void {
|
||||||
|
const title = String(item.getField("title") || "ChatPapers");
|
||||||
|
openPaneWindow({
|
||||||
|
kind: "chat",
|
||||||
|
item,
|
||||||
|
rootId: "chatpapers-window-chat-root",
|
||||||
|
title: `ChatPapers — ${title}`,
|
||||||
|
mount: async (doc, root) => {
|
||||||
|
const view = new ChatView(doc, root, item);
|
||||||
|
await view.mount();
|
||||||
|
return view;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openLecturePaneWindow(item: Zotero.Item): void {
|
||||||
|
const title = String(item.getField("title") || getString("item-section-lecture-head"));
|
||||||
|
openPaneWindow({
|
||||||
|
kind: "lecture",
|
||||||
|
item,
|
||||||
|
rootId: "chatpapers-window-lecture-root",
|
||||||
|
title: `${getString("item-section-lecture-head")} — ${title}`,
|
||||||
|
mount: async (doc, root) => {
|
||||||
|
const view = new LecturePaneView(doc, root, item);
|
||||||
|
await view.mount();
|
||||||
|
return view;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
import { config } from "../../../package.json";
|
import { config } from "../../../package.json";
|
||||||
import { getLocaleID, getString } from "../../utils/locale";
|
import { getLocaleID, getString } from "../../utils/locale";
|
||||||
import { ChatView } from "./chatView";
|
import { ChatView } from "./chatView";
|
||||||
import { onItemPaneSectionToggle, prepareItemPaneBody } from "./itemPaneSection";
|
import {
|
||||||
|
applyAdaptiveHeight,
|
||||||
|
buildItemPaneSectionButtons,
|
||||||
|
onItemPaneSectionToggle,
|
||||||
|
prepareItemPaneBody,
|
||||||
|
refreshSectionOpenHeight,
|
||||||
|
} from "./itemPaneSection";
|
||||||
|
|
||||||
const views = new WeakMap<HTMLElement, ChatView>();
|
const views = new WeakMap<HTMLElement, ChatView>();
|
||||||
|
|
||||||
@@ -21,10 +27,12 @@ export function registerChatPane() {
|
|||||||
l10nID: getLocaleID("item-section-chat-sidenav"),
|
l10nID: getLocaleID("item-section-chat-sidenav"),
|
||||||
icon: iconURL("chat.svg"),
|
icon: iconURL("chat.svg"),
|
||||||
},
|
},
|
||||||
|
sectionButtons: buildItemPaneSectionButtons("chat"),
|
||||||
onInit: ({ body }) => {
|
onInit: ({ body }) => {
|
||||||
const doc = body.ownerDocument;
|
const doc = body.ownerDocument;
|
||||||
if (doc) ensureStyles(doc);
|
if (doc) ensureStyles(doc);
|
||||||
prepareItemPaneBody(body);
|
prepareItemPaneBody(body);
|
||||||
|
applyAdaptiveHeight(body, "chat");
|
||||||
},
|
},
|
||||||
onDestroy: ({ body }) => {
|
onDestroy: ({ body }) => {
|
||||||
views.get(body)?.destroy();
|
views.get(body)?.destroy();
|
||||||
@@ -53,8 +61,11 @@ export function registerChatPane() {
|
|||||||
const view = new ChatView(doc, body, item);
|
const view = new ChatView(doc, body, item);
|
||||||
views.set(body, view);
|
views.set(body, view);
|
||||||
await view.mount();
|
await view.mount();
|
||||||
|
applyAdaptiveHeight(body, "chat");
|
||||||
const section = body.closest("collapsible-section");
|
const section = body.closest("collapsible-section");
|
||||||
if (section && !section.hasAttribute("open")) {
|
if (section?.hasAttribute("open")) {
|
||||||
|
refreshSectionOpenHeight(body);
|
||||||
|
} else if (section) {
|
||||||
onItemPaneSectionToggle({ body });
|
onItemPaneSectionToggle({ body });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Vendored
+4
@@ -32,6 +32,10 @@ export type FluentMessageId =
|
|||||||
| 'cite-exported'
|
| 'cite-exported'
|
||||||
| 'cite-loading'
|
| 'cite-loading'
|
||||||
| 'cite-style'
|
| 'cite-style'
|
||||||
|
| 'item-pane-adaptive-height'
|
||||||
|
| 'item-pane-adaptive-height-off'
|
||||||
|
| 'item-pane-adaptive-height-on'
|
||||||
|
| 'item-pane-open-window'
|
||||||
| 'item-section-chat-head'
|
| 'item-section-chat-head'
|
||||||
| 'item-section-chat-sidenav'
|
| 'item-section-chat-sidenav'
|
||||||
| 'item-section-lecture-head'
|
| 'item-section-lecture-head'
|
||||||
|
|||||||
Vendored
+2
@@ -29,6 +29,8 @@ declare namespace _ZoteroTypes {
|
|||||||
"mineruPath": string;
|
"mineruPath": string;
|
||||||
"piperPath": string;
|
"piperPath": string;
|
||||||
"piperModelPath": string;
|
"piperModelPath": string;
|
||||||
|
"chatPaneAdaptiveHeight": boolean;
|
||||||
|
"lecturePaneAdaptiveHeight": boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user