Repository navigation
Expand file tree
/
Copy pathmain.lua
More file actions
409 lines (355 loc) · 13.6 KB
/
Copy pathmain.lua
File metadata and controls
409 lines (355 loc) · 13.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
local NetworkMgr = require("ui/network/manager")
local UIManager = require("ui/uimanager")
local Dispatcher = require("dispatcher")
local InfoMessage = require("ui/widget/infomessage")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local T = require("ffi/util").template
local json = require("json")
local util = require("util")
local gettext = require("gettext")
local _ = gettext
local DataStorage = require("datastorage")
local logger = require("logger")
local Event = require("ui/event")
local PluginShare = require("pluginshare")
local remote = require("remote")
local utils = require("utils")
local changed_documents = require("changed_documents")
local settings_sync = require("settings_sync")
local SyncManager = require("manager")
local SettingsSelection = require("settings_selection")
local menus = require("menus")
local extractor_push = require("extractor_push")
local has_syncservice, SyncService = pcall(require, "apps/cloudstorage/syncservice")
local manual_sync_description = "Sync annotations and bookmarks of the active document."
local sync_all_description = "Sync annotations and bookmarks of all unsynced documents with pending modifications."
local sync_all_including_unread_description = "Scan the whole library for books with annotations that have never been synced, and sync them too."
local jump_to_device_progress_description = "Jump to the reading progress of another device."
local push_progress_description = "Push the reading progress of the active document to the cloud."
local AnnotationSyncPlugin = WidgetContainer:extend {
-- see also: _meta.lua
is_doc_only = false,
settings = nil,
manager = nil,
has_syncservice = has_syncservice,
}
AnnotationSyncPlugin.default_settings = {
last_sync = "Never",
use_filename= false,
network_auto_sync = false,
progress_sync = false,
progress_sync_interval = 1,
progress_sync_last_word = false,
device_name = "",
selected_settings = {},
progress_sync_excluded_dirs = {},
purged_devices = {},
menu_location = "tools",
}
function AnnotationSyncPlugin:init()
self.ui.menu:registerToMainMenu(self)
-- Ensure the plugin is in the ReaderUI event chain
local found = false
for _, child in ipairs(self.ui) do
if child == self then
found = true
break
end
end
if not found then
table.insert(self.ui, self)
end
utils.insert_after_statistics(self.plugin_id)
self:onDispatcherRegisterActions()
self.settings = G_reader_settings:readSetting(self.plugin_id, util.tableDeepCopy(self.default_settings))
-- Fallback/migration for legacy cloud_server_object
if not self.settings.sync_server then
local server_json = G_reader_settings:readSetting("cloud_server_object")
if server_json and server_json ~= "" then
local ok, server = pcall(json.decode, server_json)
if ok and server then
self.settings.sync_server = server
self:saveSettings()
end
end
end
-- Sanitize corrupted settings
if type(self.settings.progress_sync_interval) ~= "number" then
self.settings.progress_sync_interval = self.default_settings.progress_sync_interval
end
self.settings.progress_sync_excluded_dirs = self.settings.progress_sync_excluded_dirs or {}
self.settings.purged_devices = self.settings.purged_devices or {}
if self.settings.menu_location == nil then
self.settings.menu_location = self.default_settings.menu_location
end
self.manager = SyncManager:new(self)
-- Migrate old annotation_sync_use_filename setting
if G_reader_settings:has("annotation_sync_use_filename") then
self.settings.use_filename = G_reader_settings:isTrue("annotation_sync_use_filename")
G_reader_settings:delSetting("annotation_sync_use_filename")
end
self.settings_key = self.plugin_id
-- Load plugin translations dynamically if available for the active locale
local lang = gettext.current_lang
if lang and lang ~= "C" and lang ~= "" then
local path = self.path or "plugins/AnnotationSync.koplugin"
local mo_path = string.format("%s/l10n/%s/annotation_sync.mo", path, lang)
local f = io.open(mo_path, "r")
if f then
f:close()
gettext.loadMO(mo_path)
end
end
self:registerEvents()
-- Blessed cross-plugin call mechanism: Extractors call
-- require("pluginshare").AnnotationSync.pushExtractorData(...), and may
-- check PluginShare.AnnotationSync ~= nil to detect we're installed.
PluginShare.AnnotationSync = {
pushExtractorData = function(extractor_id, filename, records, writeback_fn)
extractor_push.push(self, extractor_id, filename, records, writeback_fn)
end,
}
end
function AnnotationSyncPlugin:saveSettings()
G_reader_settings:saveSetting(self.plugin_id, self.settings)
end
function AnnotationSyncPlugin:deletePluginSettings()
G_reader_settings:delSetting(self.plugin_id)
G_reader_settings:delSetting("cloud_server_object")
G_reader_settings:delSetting("cloud_download_dir")
G_reader_settings:delSetting("cloud_provider_type")
local track_path = changed_documents.path()
if track_path and util.fileExists(track_path) then
os.remove(track_path)
end
end
function AnnotationSyncPlugin:addToMainMenu(menu_items)
return menus.build_main_menu(self, menu_items)
end
function AnnotationSyncPlugin:registerEvents()
if self.settings.network_auto_sync then
self.onNetworkConnected = self._onNetworkConnected
else
self.onNetworkConnected = nil
end
end
function AnnotationSyncPlugin:_onNetworkConnected()
logger.dbg("AnnotationSync: handling event: NetworkConnected")
-- Fired once per episode regardless of pending documents: Extractor data
-- (VocabDeck, Notebook) is independent of document-change state.
UIManager:broadcastEvent(Event:new("AnnotationSyncRequested"))
if changed_documents.has_pending() then
utils.show_msg("AnnotationSync: Network available, syncing all changed documents")
UIManager:scheduleIn(1, function()
self.manager:syncAllChangedDocuments()
end)
end
end
function AnnotationSyncPlugin:onAnnotationSyncSyncAll()
self.manager:syncAllChangedDocuments()
return true
end
function AnnotationSyncPlugin:onAnnotationSyncSyncAllIncludingUnread()
self.manager:scanAndSyncAllBooks()
return true
end
function AnnotationSyncPlugin:onAnnotationSyncManualSync()
self:manualSync()
return true
end
function AnnotationSyncPlugin:onAnnotationSyncPushSettings()
settings_sync.push(self)
return true
end
function AnnotationSyncPlugin:onAnnotationSyncPullSettings()
settings_sync.pull(self)
return true
end
function AnnotationSyncPlugin:onAnnotationSyncJumpToDeviceProgress()
if not self.ui.cloudstorage and not self.has_syncservice then
utils.show_msg(_("Reading progress sync is not supported on this version of KOReader."))
return true
end
local document = self.ui and self.ui.document
if not document or not document.file then
utils.show_msg(_("A document must be active to jump to device progress."))
return true
end
self.manager:pullProgress()
return true
end
function AnnotationSyncPlugin:onAnnotationSyncPushProgress()
if not self.ui.cloudstorage and not self.has_syncservice then
utils.show_msg(_("Reading progress sync is not supported on this version of KOReader."))
return true
end
local document = self.ui and self.ui.document
if not document or not document.file then
utils.show_msg(_("A document must be active to push reading progress."))
return true
end
utils.show_msg(_("Pushing reading progress..."))
self.manager:syncProgress(function(success)
if success then
utils.show_msg(_("Reading progress pushed successfully."))
else
utils.show_msg(_("Failed to push reading progress."))
end
end)
return true
end
function AnnotationSyncPlugin:onPageUpdate(page_pos)
if self.manager then
self.manager:onPageUpdate(page_pos)
end
end
function AnnotationSyncPlugin:onPosUpdate(page_pos)
if self.manager then
self.manager:onPageUpdate(page_pos)
end
end
function AnnotationSyncPlugin:onPagePositionUpdated(page_pos)
if self.manager then
self.manager:onPageUpdate(page_pos)
end
end
function AnnotationSyncPlugin:onCloseDocument()
if self.manager then
self.manager:onCloseDocument()
end
end
function AnnotationSyncPlugin:onSuspend()
if self.manager then
self.manager:onSuspend()
end
end
function AnnotationSyncPlugin:onDispatcherRegisterActions()
Dispatcher:registerAction("annotation_sync_manual_sync", {
category = "none",
event = "AnnotationSyncManualSync",
title = _("AnnotationSync: Manual Sync"),
text = _(manual_sync_description),
separator = true,
reader = true
})
Dispatcher:registerAction("annotation_sync_push_settings", {
category = "none",
event = "AnnotationSyncPushSettings",
title = _("AnnotationSync: Push settings to cloud"),
text = _("Push the selected settings to the cloud."),
separator = true,
general = true
})
Dispatcher:registerAction("annotation_sync_pull_settings", {
category = "none",
event = "AnnotationSyncPullSettings",
title = _("AnnotationSync: Pull settings from cloud"),
text = _("Pull the selected settings from the cloud."),
separator = true,
general = true
})
Dispatcher:registerAction("annotation_sync_push_progress", {
category = "none",
event = "AnnotationSyncPushProgress",
title = _("AnnotationSync: Push reading progress"),
text = _(push_progress_description),
separator = true,
reader = true
})
Dispatcher:registerAction("annotation_sync_jump_to_device_progress", {
category = "none",
event = "AnnotationSyncJumpToDeviceProgress",
title = _("AnnotationSync: Jump to device progress"),
text = _(jump_to_device_progress_description),
separator = true,
reader = true
})
Dispatcher:registerAction("annotation_sync_sync_all", {
category = "none",
event = "AnnotationSyncSyncAll",
title = _("AnnotationSync: Sync All"),
text = _(sync_all_description),
separator = true,
general = true
})
Dispatcher:registerAction("annotation_sync_sync_all_including_unread", {
category = "none",
event = "AnnotationSyncSyncAllIncludingUnread",
title = _("AnnotationSync: Sync All (including unread)"),
text = _(sync_all_including_unread_description),
separator = true,
general = true
})
end
function AnnotationSyncPlugin:onSyncServiceConfirm(server)
self.settings.sync_server = server
self:saveSettings()
-- Keep G_reader_settings updated for legacy compatibility and menu enablement
G_reader_settings:saveSetting("cloud_server_object", json.encode(server))
G_reader_settings:saveSetting("cloud_download_dir", server.url)
if server.type then
G_reader_settings:saveSetting("cloud_provider_type", server.type)
end
UIManager:show(InfoMessage:new{
text = T(_("Cloud destination set to:\n%1\nProvider: %2"),
server.url, server.type or "unknown"),
timeout = 4
})
if self and self.ui and self.ui.menu and self.ui.menu.showMainMenu then
self.ui.menu:showMainMenu()
end
end
function AnnotationSyncPlugin:manualSync()
local document = self.ui and self.ui.document
local file = document and document.file
if not file then
utils.show_msg("A document must be active to do a manual sync.")
return
end
if NetworkMgr:isConnected() then
UIManager:broadcastEvent(Event:new("AnnotationSyncRequested"))
end
self.manager:syncDocument(document, true)
self.manager:updateLastSync("Manual Sync")
end
function AnnotationSyncPlugin:showDeletedAnnotations()
local document = self.ui and self.ui.document
if not document then return end
menus.show_deleted_annotations(self, document)
end
function AnnotationSyncPlugin:restoreAnnotations(anns, silent)
local document = self.ui and self.ui.document
if not document or not anns or #anns == 0 then return end
local now = os.date("%Y-%m-%d %H:%M:%S")
local current = self.manager:getAnnotationsForDocument(document)
for _, ann in ipairs(anns) do
-- 1. Mark as not deleted and update timestamp
ann.deleted = false
ann.datetime_updated = now
-- 2. Add back to current list
table.insert(current, ann)
end
-- 3. Apply changes once (saves to sidecar and refreshes UI)
self.manager:applySyncedAnnotations(document, current)
-- 4. Flush to local sync JSON immediately (Fix for Issue #39 delayed flush)
self.manager:writeAnnotationsJSON(document)
if not silent then
if #anns == 1 then
utils.show_msg(_("Annotation restored."))
else
utils.show_msg(T(_("Restored %1 annotations."), #anns))
end
end
end
function AnnotationSyncPlugin:restoreAnnotation(ann, silent)
self:restoreAnnotations({ann}, silent)
end
function AnnotationSyncPlugin:onAnnotationsModified(annotations)
if self.manager then
self.manager:onAnnotationsModified(annotations)
end
end
function AnnotationSyncPlugin:showChangedSettings()
SettingsSelection.show(self)
end
return AnnotationSyncPlugin