diva.js 6.0.1 → 7.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/.clang-format +7 -0
  2. package/.github/workflows/npm-publish.yml +45 -0
  3. package/LICENSE +55 -0
  4. package/Makefile +75 -0
  5. package/README.md +15 -108
  6. package/elm.json +32 -0
  7. package/package.json +12 -59
  8. package/review/elm.json +52 -0
  9. package/review/src/ReviewConfig.elm +87 -0
  10. package/scripts/elm-esm.sh +40 -0
  11. package/scripts/minify-css.mjs +31 -0
  12. package/src/Filters.elm +1044 -0
  13. package/src/Main.elm +1217 -0
  14. package/src/Model.elm +213 -0
  15. package/src/Msg.elm +59 -0
  16. package/src/Utilities.elm +46 -0
  17. package/src/View/CollectionExplorer.elm +172 -0
  18. package/src/View/Helpers.elm +86 -0
  19. package/src/View/HtmlRenderer.elm +136 -0
  20. package/src/View/Icons.elm +159 -0
  21. package/src/View/ManifestInfoModal.elm +363 -0
  22. package/src/View/PageViewModal.elm +1046 -0
  23. package/src/View/Sidebar.elm +786 -0
  24. package/src/View/Toolbar.elm +189 -0
  25. package/src/View.elm +244 -0
  26. package/src/diva.ts +802 -0
  27. package/src/filters.ts +1843 -0
  28. package/src/styles/app.css +328 -0
  29. package/src/styles/collection.css +75 -0
  30. package/src/styles/modal.css +388 -0
  31. package/src/styles/sidebar.css +215 -0
  32. package/src/styles/theme.css +39 -0
  33. package/src/styles/toolbar.css +154 -0
  34. package/src/viewer-element.ts +1307 -0
  35. package/testing/index.html +52 -0
  36. package/testing/testing.html +231 -0
  37. package/tsconfig.json +12 -0
  38. package/AUTHORS +0 -22
  39. package/_site/diva.iml +0 -11
  40. package/build/diva.css +0 -554
  41. package/build/diva.css.map +0 -1
  42. package/build/diva.js +0 -9
  43. package/build/diva.js.map +0 -1
  44. package/build/plugins/download.js +0 -2
  45. package/build/plugins/download.js.map +0 -1
  46. package/build/plugins/manipulation.js +0 -2
  47. package/build/plugins/manipulation.js.map +0 -1
  48. package/build/plugins/metadata.js +0 -2
  49. package/build/plugins/metadata.js.map +0 -1
  50. package/diva.iml +0 -11
  51. package/index.html +0 -28
  52. package/karma.conf.js +0 -87
  53. package/source/css/_mixins.scss +0 -43
  54. package/source/css/_variables.scss +0 -50
  55. package/source/css/_viewer.scss +0 -462
  56. package/source/css/diva.scss +0 -15
  57. package/source/css/plugins/_manipulation.scss +0 -228
  58. package/source/css/plugins/_metadata.scss +0 -31
  59. package/source/img/adjust.svg +0 -11
  60. package/source/img/book-view.svg +0 -6
  61. package/source/img/close.svg +0 -6
  62. package/source/img/download.svg +0 -6
  63. package/source/img/from-fullscreen.svg +0 -8
  64. package/source/img/grid-fewer.svg +0 -6
  65. package/source/img/grid-more.svg +0 -6
  66. package/source/img/grid-view.svg +0 -6
  67. package/source/img/link.svg +0 -6
  68. package/source/img/metadata.svg +0 -9
  69. package/source/img/page-view.svg +0 -6
  70. package/source/img/to-fullscreen.svg +0 -11
  71. package/source/img/zoom-in.svg +0 -6
  72. package/source/img/zoom-out.svg +0 -7
  73. package/source/js/composite-image.js +0 -174
  74. package/source/js/diva-global.js +0 -7
  75. package/source/js/diva.js +0 -1543
  76. package/source/js/document-handler.js +0 -180
  77. package/source/js/document-layout.js +0 -286
  78. package/source/js/exceptions.js +0 -26
  79. package/source/js/gesture-events.js +0 -190
  80. package/source/js/grid-handler.js +0 -122
  81. package/source/js/iiif-source-adapter.js +0 -63
  82. package/source/js/image-cache.js +0 -113
  83. package/source/js/image-manifest.js +0 -157
  84. package/source/js/image-request-handler.js +0 -76
  85. package/source/js/interpolate-animation.js +0 -122
  86. package/source/js/page-layouts/book-layout.js +0 -161
  87. package/source/js/page-layouts/grid-layout.js +0 -97
  88. package/source/js/page-layouts/index.js +0 -38
  89. package/source/js/page-layouts/page-dimensions.js +0 -9
  90. package/source/js/page-layouts/singles-layout.js +0 -27
  91. package/source/js/page-overlay-manager.js +0 -102
  92. package/source/js/page-tools-overlay.js +0 -95
  93. package/source/js/parse-iiif-manifest.js +0 -302
  94. package/source/js/plugins/_filters.js +0 -679
  95. package/source/js/plugins/download.js +0 -83
  96. package/source/js/plugins/manipulation.js +0 -837
  97. package/source/js/plugins/metadata.js +0 -190
  98. package/source/js/renderer.js +0 -584
  99. package/source/js/settings-view.js +0 -30
  100. package/source/js/tile-coverage-map.js +0 -25
  101. package/source/js/toolbar.js +0 -572
  102. package/source/js/utils/dragscroll.js +0 -106
  103. package/source/js/utils/elt.js +0 -94
  104. package/source/js/utils/events.js +0 -190
  105. package/source/js/utils/get-scrollbar-width.js +0 -29
  106. package/source/js/utils/hash-params.js +0 -86
  107. package/source/js/utils/parse-label-value.js +0 -34
  108. package/source/js/utils/vanilla.kinetic.js +0 -527
  109. package/source/js/validation-runner.js +0 -177
  110. package/source/js/viewer-core.js +0 -1505
  111. package/source/js/viewport.js +0 -143
  112. package/test/_setup.js +0 -13
  113. package/test/composite-image_test.js +0 -94
  114. package/test/diva_test.js +0 -43
  115. package/test/hash-params_test.js +0 -221
  116. package/test/image-cache_test.js +0 -106
  117. package/test/main.js +0 -6
  118. package/test/manifests/beromunsterManifest.json +0 -15514
  119. package/test/manifests/iiifv2.json +0 -11032
  120. package/test/manifests/iiifv2pages.json +0 -30437
  121. package/test/manifests/iiifv3.json +0 -10965
  122. package/test/navigation_test.js +0 -355
  123. package/test/parse-iiif-manifest_test.js +0 -68
  124. package/test/public_test.js +0 -881
  125. package/test/settings_test.js +0 -487
  126. package/test/utils/book-layout_test.js +0 -148
  127. package/test/utils/elt_test.js +0 -102
  128. package/test/utils/events_test.js +0 -245
  129. package/test/utils/hash-params_test.js +0 -79
  130. package/test/utils/parse-label-value_test.js +0 -45
  131. package/test/z_plugins_test.js +0 -180
  132. package/webpack.config.js +0 -58
  133. package/webpack.config.test.js +0 -45
package/src/Main.elm ADDED
@@ -0,0 +1,1217 @@
1
+ port module Main exposing (Flags, main)
2
+
3
+ import Browser
4
+ import Browser.Dom as Dom
5
+ import Browser.Events
6
+ import Dict exposing (Dict)
7
+ import Filters exposing (Filters, applyFilterToggle, applyFloatFilter, applyIntFilter, applyStringFilter, decodeFilterJson, encodeActiveFilters, resetAltColourAdjust, resetFilters, updateFilters)
8
+ import Http
9
+ import IIIF
10
+ import IIIF.Language exposing (Language(..))
11
+ import IIIF.Presentation exposing (Collection, CollectionItem(..), IIIFCollection(..), IIIFManifest, IIIFResource(..), Range, RangeItem(..), ViewingDirection(..), isPagedLayout, manifestViewingLayout, toCanvases, toRanges, toViewingDirection)
12
+ import Json.Decode as Decode
13
+ import Model exposing (ContentsView(..), Model, ResourceResponse(..), Response(..), SidebarState(..), ViewMode(..), getPageAt, manifestToPages, primaryImage)
14
+ import Msg exposing (Msg(..))
15
+ import Process
16
+ import Set
17
+ import Task
18
+ import View
19
+
20
+
21
+ port copyToClipboard : String -> Cmd msg
22
+
23
+
24
+ port filterPreviewUpdated :
25
+ { tileSource : String
26
+ , aspect : Float
27
+ , filters : Filters
28
+ }
29
+ -> Cmd msg
30
+
31
+
32
+ port fullscreenChanged : (Bool -> msg) -> Sub msg
33
+
34
+
35
+ port layoutConfigUpdated : { mode : String, direction : String } -> Cmd msg
36
+
37
+
38
+ port layoutModeUpdated : String -> Cmd msg
39
+
40
+
41
+ port pageAspectsUpdated : List Float -> Cmd msg
42
+
43
+
44
+ port pageIndexChanged : (Int -> msg) -> Sub msg
45
+
46
+
47
+ port pageIndexChangedInstant : (Int -> msg) -> Sub msg
48
+
49
+
50
+ port pageLabelsUpdated : List String -> Cmd msg
51
+
52
+
53
+ port saveFilteredImage : () -> Cmd msg
54
+
55
+
56
+ port scrollToIndex : Int -> Cmd msg
57
+
58
+
59
+ port setFullscreen : Bool -> Cmd msg
60
+
61
+
62
+ port tileSourcesUpdated : List String -> Cmd msg
63
+
64
+
65
+ port viewerLoadingChanged : (Bool -> msg) -> Sub msg
66
+
67
+
68
+ port zoomBy : Float -> Cmd msg
69
+
70
+
71
+ port zoomChanged : (Float -> msg) -> Sub msg
72
+
73
+
74
+ port zoomLevelUpdated : Float -> Cmd msg
75
+
76
+
77
+ type alias Flags =
78
+ { rootElementId : String
79
+ , objectData : String
80
+ , acceptHeaders : List String
81
+ , showSidebar : Bool
82
+ , showTitle : Bool
83
+ , userLanguage : String
84
+ }
85
+
86
+
87
+ main : Program Flags Model Msg
88
+ main =
89
+ Browser.element
90
+ { init = init
91
+ , subscriptions = subscriptions
92
+ , update = update
93
+ , view = View.view
94
+ }
95
+
96
+
97
+ buildRangeIndexMap : Dict String Int -> List Range -> Dict String (Maybe Int)
98
+ buildRangeIndexMap canvasIndex ranges =
99
+ List.foldl
100
+ (\range acc ->
101
+ Dict.union (rangeIndexMapForRange canvasIndex range) acc
102
+ )
103
+ Dict.empty
104
+ ranges
105
+
106
+
107
+ ensureSidebarVisible : SidebarState -> SidebarState
108
+ ensureSidebarVisible state =
109
+ case state of
110
+ SidebarHidden ->
111
+ SidebarThumbnails
112
+
113
+ _ ->
114
+ state
115
+
116
+
117
+ findCollectionById : String -> Collection -> Maybe Collection
118
+ findCollectionById collectionId collection =
119
+ let
120
+ loop state stack =
121
+ if state.collection.id == collectionId then
122
+ Just state.collection
123
+
124
+ else
125
+ case state.rest of
126
+ [] ->
127
+ case stack of
128
+ [] ->
129
+ Nothing
130
+
131
+ frame :: rest ->
132
+ loop frame rest
133
+
134
+ item :: rest ->
135
+ case item of
136
+ NestedCollection nested ->
137
+ loop
138
+ { collection = nested
139
+ , rest = nested.items
140
+ }
141
+ ({ collection = state.collection, rest = rest } :: stack)
142
+
143
+ ManifestItem _ ->
144
+ loop { state | rest = rest } stack
145
+ in
146
+ loop { collection = collection, rest = collection.items } []
147
+
148
+
149
+ handleManifestLoaded : Model -> IIIFManifest -> ( Model, Cmd Msg )
150
+ handleManifestLoaded model manifest =
151
+ let
152
+ pagedLayout =
153
+ manifestViewingLayout manifest
154
+ |> isPagedLayout
155
+
156
+ viewingDirection =
157
+ toViewingDirection manifest
158
+
159
+ isSingleCanvas =
160
+ List.length pages == 1
161
+
162
+ pages =
163
+ manifestToPages model.detectedLanguage manifest
164
+
165
+ tileSources =
166
+ List.filterMap (primaryImage >> Maybe.map .tileSource) pages
167
+
168
+ pageAspects =
169
+ List.map .aspect pages
170
+
171
+ viewMode =
172
+ if isSingleCanvas then
173
+ OneUp
174
+
175
+ else if pagedLayout then
176
+ TwoUp
177
+
178
+ else
179
+ OneUp
180
+
181
+ shiftByOne =
182
+ if isSingleCanvas then
183
+ False
184
+
185
+ else
186
+ pagedLayout || viewingDirection == RightToLeft
187
+
188
+ layoutMode =
189
+ layoutModeToString viewMode shiftByOne
190
+
191
+ direction =
192
+ viewingDirectionToString viewingDirection
193
+
194
+ canvasIndexMap =
195
+ toCanvases manifest
196
+ |> List.indexedMap (\index canvas -> ( canvas.id, index ))
197
+ |> Dict.fromList
198
+
199
+ rangeIndexMap =
200
+ toRanges manifest
201
+ |> Maybe.map (buildRangeIndexMap canvasIndexMap)
202
+ |> Maybe.withDefault Dict.empty
203
+ in
204
+ ( { model
205
+ | currentZoom = Nothing
206
+ , filters = resetFilters
207
+ , hasTileSources = not (List.isEmpty tileSources)
208
+ , initialZoom = Nothing
209
+ , isViewerLoading = False
210
+ , pages = pages
211
+ , rangeIndexMap = rangeIndexMap
212
+ , response = Loaded manifest
213
+ , selectedIndex =
214
+ if List.isEmpty pages then
215
+ Nothing
216
+
217
+ else
218
+ Just 0
219
+ , shiftByOne = shiftByOne
220
+ , viewMode = viewMode
221
+ }
222
+ , Cmd.batch
223
+ [ tileSourcesUpdated tileSources
224
+ , pageAspectsUpdated pageAspects
225
+ , pageLabelsUpdated (List.map .label pages)
226
+ , zoomLevelUpdated 1
227
+ , layoutConfigUpdated { direction = direction, mode = layoutMode }
228
+ ]
229
+ )
230
+
231
+
232
+ handlePageChanged : Bool -> Int -> Model -> ( Model, Cmd Msg )
233
+ handlePageChanged instant index model =
234
+ let
235
+ nextModel =
236
+ { model
237
+ | pageViewImageIndex = 0
238
+ , selectedIndex = Just index
239
+ , thumbsInstantScroll = instant
240
+ }
241
+ in
242
+ ( nextModel
243
+ , Cmd.batch
244
+ [ scrollThumbsToIndex (nextModel.sidebarState == SidebarThumbnails) index
245
+ , sendPageViewPreview nextModel
246
+ ]
247
+ )
248
+
249
+
250
+ handlePageViewStep : Int -> Model -> ( Model, Cmd Msg )
251
+ handlePageViewStep delta model =
252
+ case model.selectedIndex of
253
+ Just index ->
254
+ let
255
+ nextIndex =
256
+ index + delta
257
+ in
258
+ if nextIndex >= 0 && nextIndex < List.length model.pages then
259
+ let
260
+ nextModel =
261
+ { model
262
+ | pageViewImageIndex = 0
263
+ , selectedIndex = Just nextIndex
264
+ , thumbsInstantScroll = False
265
+ }
266
+ in
267
+ ( nextModel
268
+ , Cmd.batch
269
+ [ scrollToIndex nextIndex
270
+ , scrollThumbsToIndex (nextModel.sidebarState == SidebarThumbnails) nextIndex
271
+ , sendPageViewPreview nextModel
272
+ ]
273
+ )
274
+
275
+ else
276
+ ( model, Cmd.none )
277
+
278
+ Nothing ->
279
+ ( model, Cmd.none )
280
+
281
+
282
+ httpErrorToString : Http.Error -> String
283
+ httpErrorToString err =
284
+ case err of
285
+ Http.BadUrl url ->
286
+ "Bad URL: " ++ url
287
+
288
+ Http.Timeout ->
289
+ "Request timed out."
290
+
291
+ Http.NetworkError ->
292
+ "Network error. The resource may be unreachable or blocked by CORS."
293
+
294
+ Http.BadStatus statusCode ->
295
+ "HTTP error: " ++ String.fromInt statusCode
296
+
297
+ Http.BadBody _ ->
298
+ "Invalid IIIF response body. URL did not return a valid IIIF Manifest or Collection JSON."
299
+
300
+
301
+ init : Flags -> ( Model, Cmd Msg )
302
+ init flags =
303
+ let
304
+ manifestUrl =
305
+ flags.objectData
306
+
307
+ sidebarState =
308
+ if flags.showSidebar then
309
+ SidebarThumbnails
310
+
311
+ else
312
+ SidebarHidden
313
+
314
+ userLanguage =
315
+ LanguageCode flags.userLanguage
316
+ in
317
+ ( { acceptHeaders = flags.acceptHeaders
318
+ , collectionSidebarDrag = Nothing
319
+ , collectionSidebarVisible = True
320
+ , collectionSidebarWidth = 400
321
+ , contentsView = ContentsIndex
322
+ , currentZoom = Nothing
323
+ , detectedLanguage = userLanguage
324
+ , filterGroupExpanded = Set.empty
325
+ , filters = resetFilters
326
+ , filtersJsonError = Nothing
327
+ , filtersJsonInput = ""
328
+ , fullscreen = False
329
+ , hasTileSources = False
330
+ , initialZoom = Nothing
331
+ , isMobile = False
332
+ , isViewerLoading = False
333
+ , manifestInfoOpen = False
334
+ , manifestUrl = manifestUrl
335
+ , mobileSidebarOpen = False
336
+ , pageViewFullscreen = False
337
+ , pageViewImageIndex = 0
338
+ , pageViewOpen = False
339
+ , pageViewSidebarVisible = True
340
+ , pages = []
341
+ , pendingThumbScroll = Nothing
342
+ , rangeIndexMap = Dict.empty
343
+ , resourceResponse = ResourceLoading
344
+ , response = Loading
345
+ , rootElementId = flags.rootElementId
346
+ , selectedIndex = Nothing
347
+ , selectedRangeId = Nothing
348
+ , shiftByOne = False
349
+ , showTitle = flags.showTitle
350
+ , sidebarDrag = Nothing
351
+ , sidebarState = sidebarState
352
+ , sidebarWidth = 320
353
+ , thumbsInstantScroll = False
354
+ , viewMode = OneUp
355
+ }
356
+ , Cmd.batch
357
+ [ IIIF.requestResource ServerRespondedWithResource flags.acceptHeaders manifestUrl
358
+ , Task.perform (\viewport -> ViewportChanged (round viewport.viewport.width) (round viewport.viewport.height)) Dom.getViewport
359
+ ]
360
+ )
361
+
362
+
363
+ layoutModeToString : ViewMode -> Bool -> String
364
+ layoutModeToString viewMode shiftByOne =
365
+ case viewMode of
366
+ OneUp ->
367
+ "single"
368
+
369
+ TwoUp ->
370
+ if shiftByOne then
371
+ "spread-shift"
372
+
373
+ else
374
+ "spread"
375
+
376
+
377
+ mobileShortSideBreakpoint : Int
378
+ mobileShortSideBreakpoint =
379
+ 720
380
+
381
+
382
+ rangeIndexMapForRange :
383
+ Dict String Int
384
+ -> Range
385
+ -> Dict String (Maybe Int)
386
+ rangeIndexMapForRange canvasIndex range =
387
+ let
388
+ ( firstIndex, childMap ) =
389
+ rangeItemsIndexMap canvasIndex range.items
390
+ in
391
+ Dict.insert range.id firstIndex childMap
392
+
393
+
394
+ rangeItemsIndexMap :
395
+ Dict String Int
396
+ -> List RangeItem
397
+ -> ( Maybe Int, Dict String (Maybe Int) )
398
+ rangeItemsIndexMap canvasIndex items =
399
+ List.foldl
400
+ (\item ( maybeFirst, acc ) ->
401
+ case item of
402
+ RangeCanvas canvasId ->
403
+ let
404
+ nextFirst =
405
+ case maybeFirst of
406
+ Just _ ->
407
+ maybeFirst
408
+
409
+ Nothing ->
410
+ Dict.get canvasId canvasIndex
411
+ in
412
+ ( nextFirst, acc )
413
+
414
+ RangeRange range ->
415
+ let
416
+ rangeMap =
417
+ rangeIndexMapForRange canvasIndex range
418
+
419
+ nextFirst =
420
+ case maybeFirst of
421
+ Just _ ->
422
+ maybeFirst
423
+
424
+ Nothing ->
425
+ Dict.get range.id rangeMap
426
+ |> Maybe.withDefault Nothing
427
+ in
428
+ ( nextFirst, Dict.union rangeMap acc )
429
+ )
430
+ ( Nothing, Dict.empty )
431
+ items
432
+
433
+
434
+ replaceCollectionById : String -> Collection -> Collection -> Collection
435
+ replaceCollectionById collectionId replacement collection =
436
+ let
437
+ continueSearch updatedChild stack =
438
+ case stack of
439
+ [] ->
440
+ updatedChild
441
+
442
+ frame :: rest ->
443
+ loopSearch
444
+ { beforeRev = NestedCollection updatedChild :: frame.beforeRev
445
+ , collection = frame.collection
446
+ , rest = frame.rest
447
+ }
448
+ rest
449
+
450
+ rebuildUp updatedChild stack =
451
+ case stack of
452
+ [] ->
453
+ updatedChild
454
+
455
+ frame :: rest ->
456
+ let
457
+ baseCollection =
458
+ frame.collection
459
+ in
460
+ rebuildUp
461
+ { baseCollection
462
+ | items =
463
+ List.reverse (NestedCollection updatedChild :: frame.beforeRev)
464
+ ++ frame.rest
465
+ }
466
+ rest
467
+
468
+ loopSearch state stack =
469
+ if state.collection.id == collectionId then
470
+ rebuildUp replacement stack
471
+
472
+ else
473
+ case state.rest of
474
+ [] ->
475
+ let
476
+ baseCollection =
477
+ state.collection
478
+ in
479
+ continueSearch
480
+ { baseCollection | items = List.reverse state.beforeRev }
481
+ stack
482
+
483
+ item :: rest ->
484
+ case item of
485
+ NestedCollection nested ->
486
+ loopSearch
487
+ { beforeRev = []
488
+ , collection = nested
489
+ , rest = nested.items
490
+ }
491
+ ({ beforeRev = state.beforeRev
492
+ , collection = state.collection
493
+ , rest = rest
494
+ }
495
+ :: stack
496
+ )
497
+
498
+ ManifestItem _ ->
499
+ loopSearch
500
+ { state | beforeRev = item :: state.beforeRev, rest = rest }
501
+ stack
502
+ in
503
+ loopSearch
504
+ { beforeRev = [], collection = collection, rest = collection.items }
505
+ []
506
+
507
+
508
+ scrollThumbsToIndex : Bool -> Int -> Cmd Msg
509
+ scrollThumbsToIndex showThumbs index =
510
+ if showThumbs then
511
+ let
512
+ delayedTask =
513
+ Process.sleep 0
514
+ |> Task.andThen
515
+ (\_ ->
516
+ let
517
+ thumbId =
518
+ "thumb-" ++ String.fromInt index
519
+ in
520
+ Task.map3
521
+ (\thumb container viewport ->
522
+ max 0 (thumb.element.y - container.element.y + viewport.viewport.y)
523
+ |> Dom.setViewportOf "thumbs" 0
524
+ )
525
+ (Dom.getElement thumbId)
526
+ (Dom.getElement "thumbs")
527
+ (Dom.getViewportOf "thumbs")
528
+ |> Task.andThen identity
529
+ )
530
+ in
531
+ Task.attempt (\_ -> ClientNotifiedScrollThumbs) delayedTask
532
+
533
+ else
534
+ Cmd.none
535
+
536
+
537
+ sendPageViewPreview : Model -> Cmd Msg
538
+ sendPageViewPreview model =
539
+ if model.pageViewOpen then
540
+ model.selectedIndex
541
+ |> Maybe.andThen (\index -> getPageAt index model.pages)
542
+ |> Maybe.andThen
543
+ (\page ->
544
+ List.drop model.pageViewImageIndex page.images
545
+ |> List.head
546
+ |> Maybe.map
547
+ (\image ->
548
+ filterPreviewUpdated
549
+ { aspect = page.aspect
550
+ , filters = model.filters
551
+ , tileSource = image.tileSource
552
+ }
553
+ )
554
+ )
555
+ |> Maybe.withDefault Cmd.none
556
+
557
+ else
558
+ Cmd.none
559
+
560
+
561
+ subscriptions : Model -> Sub Msg
562
+ subscriptions model =
563
+ Sub.batch
564
+ [ pageIndexChanged ClientNotifiedPageChanged
565
+ , pageIndexChangedInstant ClientNotifiedPageChangedInstant
566
+ , fullscreenChanged ClientNotifiedFullscreenChanged
567
+ , zoomChanged UserChangedZoomLevel
568
+ , viewerLoadingChanged ViewerLoadingChanged
569
+ , Browser.Events.onResize ViewportChanged
570
+ , case model.sidebarDrag of
571
+ Just _ ->
572
+ Sub.batch
573
+ [ Browser.Events.onMouseMove
574
+ (Decode.field "clientX" Decode.int |> Decode.map UserDraggedSidebarResize)
575
+ , Browser.Events.onMouseUp (Decode.succeed UserEndedSidebarResize)
576
+ ]
577
+
578
+ Nothing ->
579
+ Sub.none
580
+ , case model.collectionSidebarDrag of
581
+ Just _ ->
582
+ Sub.batch
583
+ [ Browser.Events.onMouseMove
584
+ (Decode.field "clientX" Decode.int |> Decode.map UserDraggedCollectionSidebarResize)
585
+ , Browser.Events.onMouseUp (Decode.succeed UserEndedCollectionSidebarResize)
586
+ ]
587
+
588
+ Nothing ->
589
+ Sub.none
590
+ ]
591
+
592
+
593
+ update : Msg -> Model -> ( Model, Cmd Msg )
594
+ update msg model =
595
+ case msg of
596
+ ClientNotifiedFullscreenChanged enabled ->
597
+ ( { model | fullscreen = enabled }, Cmd.none )
598
+
599
+ ClientNotifiedPageChanged index ->
600
+ handlePageChanged False index model
601
+
602
+ ClientNotifiedPageChangedInstant index ->
603
+ handlePageChanged True index model
604
+
605
+ ClientNotifiedScrollThumbs ->
606
+ ( { model | thumbsInstantScroll = False }, Cmd.none )
607
+
608
+ ServerRespondedWithCollectionItem collectionId result ->
609
+ case model.resourceResponse of
610
+ ResourceLoadedCollection collectionState ->
611
+ let
612
+ nextLoadingIds =
613
+ Set.remove collectionId collectionState.loadingCollectionIds
614
+ in
615
+ case result of
616
+ Ok resource ->
617
+ case resource of
618
+ ResourceCollection (IIIFCollection _ fetchedCollection) ->
619
+ let
620
+ (IIIFCollection rootVersion rootCollection) =
621
+ collectionState.collection
622
+
623
+ nextCollection =
624
+ replaceCollectionById collectionId fetchedCollection rootCollection
625
+
626
+ nextState =
627
+ { collectionState
628
+ | collection = IIIFCollection rootVersion nextCollection
629
+ , loadedCollectionIds =
630
+ Set.insert collectionId collectionState.loadedCollectionIds
631
+ , loadingCollectionIds = nextLoadingIds
632
+ }
633
+ in
634
+ ( { model | resourceResponse = ResourceLoadedCollection nextState }, Cmd.none )
635
+
636
+ _ ->
637
+ ( { model
638
+ | resourceResponse =
639
+ ResourceLoadedCollection
640
+ { collectionState | loadingCollectionIds = nextLoadingIds }
641
+ }
642
+ , Cmd.none
643
+ )
644
+
645
+ Err _ ->
646
+ ( { model
647
+ | resourceResponse =
648
+ ResourceLoadedCollection
649
+ { collectionState | loadingCollectionIds = nextLoadingIds }
650
+ }
651
+ , Cmd.none
652
+ )
653
+
654
+ _ ->
655
+ ( model, Cmd.none )
656
+
657
+ ServerRespondedWithManifestFromCollection manifestId result ->
658
+ case model.resourceResponse of
659
+ ResourceLoadedCollection collectionState ->
660
+ if collectionState.selectedManifestId /= Just manifestId then
661
+ ( model, Cmd.none )
662
+
663
+ else
664
+ case result of
665
+ Ok manifest ->
666
+ handleManifestLoaded model manifest
667
+
668
+ Err err ->
669
+ ( { model
670
+ | isViewerLoading = False
671
+ , response = Failed (httpErrorToString err)
672
+ }
673
+ , Cmd.none
674
+ )
675
+
676
+ _ ->
677
+ ( model, Cmd.none )
678
+
679
+ ServerRespondedWithResource result ->
680
+ case result of
681
+ Ok resource ->
682
+ case resource of
683
+ ResourceManifest manifest ->
684
+ let
685
+ ( nextModel, cmd ) =
686
+ handleManifestLoaded model manifest
687
+ in
688
+ ( { nextModel
689
+ | collectionSidebarVisible = False
690
+ , resourceResponse = ResourceLoadedManifest manifest
691
+ }
692
+ , cmd
693
+ )
694
+
695
+ ResourceCollection (IIIFCollection version collection) ->
696
+ ( { model
697
+ | collectionSidebarVisible = True
698
+ , isViewerLoading = False
699
+ , resourceResponse =
700
+ ResourceLoadedCollection
701
+ { collection = IIIFCollection version collection
702
+ , expandedIds = Set.empty
703
+ , loadedCollectionIds = Set.empty
704
+ , loadingCollectionIds = Set.empty
705
+ , selectedManifestId = Nothing
706
+ }
707
+ , response = NotRequested
708
+ }
709
+ , Cmd.none
710
+ )
711
+
712
+ _ ->
713
+ ( model, Cmd.none )
714
+
715
+ Err err ->
716
+ ( { model
717
+ | isViewerLoading = False
718
+ , resourceResponse = ResourceFailed (httpErrorToString err)
719
+ }
720
+ , Cmd.none
721
+ )
722
+
723
+ UserAppliedFilterJson ->
724
+ case decodeFilterJson model.filtersJsonInput of
725
+ Ok filters ->
726
+ let
727
+ json =
728
+ encodeActiveFilters filters
729
+
730
+ nextModel =
731
+ { model
732
+ | filters = filters
733
+ , filtersJsonError = Nothing
734
+ , filtersJsonInput = json
735
+ }
736
+ in
737
+ ( nextModel, sendPageViewPreview nextModel )
738
+
739
+ Err err ->
740
+ ( { model | filtersJsonError = Just err }, Cmd.none )
741
+
742
+ UserChangedZoomLevel zoom ->
743
+ let
744
+ nextInitialZoom =
745
+ case model.initialZoom of
746
+ Just initialZoom ->
747
+ Just initialZoom
748
+
749
+ Nothing ->
750
+ Just zoom
751
+ in
752
+ ( { model
753
+ | currentZoom = Just zoom
754
+ , initialZoom = nextInitialZoom
755
+ }
756
+ , Cmd.none
757
+ )
758
+
759
+ UserClickedCloseManifestInfo ->
760
+ ( { model | manifestInfoOpen = False }, Cmd.none )
761
+
762
+ UserClickedClosePageView ->
763
+ let
764
+ nextModel =
765
+ { model
766
+ | filters = resetFilters
767
+ , pageViewFullscreen = False
768
+ , pageViewImageIndex = 0
769
+ , pageViewOpen = False
770
+ }
771
+ in
772
+ ( nextModel, Cmd.none )
773
+
774
+ UserClickedCollectionItem collectionId ->
775
+ case model.resourceResponse of
776
+ ResourceLoadedCollection collectionState ->
777
+ let
778
+ (IIIFCollection _ rootCollection) =
779
+ collectionState.collection
780
+
781
+ isItemsEmpty =
782
+ findCollectionById collectionId rootCollection
783
+ |> Maybe.map (.items >> List.isEmpty)
784
+ |> Maybe.withDefault True
785
+
786
+ shouldRequest =
787
+ isItemsEmpty
788
+ && not (Set.member collectionId collectionState.loadedCollectionIds)
789
+ && not (Set.member collectionId collectionState.loadingCollectionIds)
790
+
791
+ isExpanded =
792
+ Set.member collectionId collectionState.expandedIds
793
+
794
+ nextExpandedIds =
795
+ if isExpanded then
796
+ Set.remove collectionId collectionState.expandedIds
797
+
798
+ else
799
+ Set.insert collectionId collectionState.expandedIds
800
+
801
+ nextLoadingIds =
802
+ if shouldRequest then
803
+ Set.insert collectionId collectionState.loadingCollectionIds
804
+
805
+ else
806
+ collectionState.loadingCollectionIds
807
+
808
+ nextState =
809
+ { collectionState
810
+ | expandedIds = nextExpandedIds
811
+ , loadingCollectionIds = nextLoadingIds
812
+ }
813
+ in
814
+ ( { model
815
+ | resourceResponse =
816
+ ResourceLoadedCollection
817
+ nextState
818
+ }
819
+ , if shouldRequest then
820
+ IIIF.requestResource
821
+ (ServerRespondedWithCollectionItem collectionId)
822
+ model.acceptHeaders
823
+ collectionId
824
+
825
+ else
826
+ Cmd.none
827
+ )
828
+
829
+ _ ->
830
+ ( model, Cmd.none )
831
+
832
+ UserClickedManifestItem manifestId manifestUrl ->
833
+ case model.resourceResponse of
834
+ ResourceLoadedCollection collectionState ->
835
+ ( { model
836
+ | isViewerLoading = True
837
+ , response = Loading
838
+ , resourceResponse =
839
+ ResourceLoadedCollection
840
+ { collectionState | selectedManifestId = Just manifestId }
841
+ }
842
+ , IIIF.requestManifest (ServerRespondedWithManifestFromCollection manifestId) model.acceptHeaders manifestUrl
843
+ )
844
+
845
+ _ ->
846
+ ( model, Cmd.none )
847
+
848
+ UserClickedOpenManifestInfo ->
849
+ ( { model | manifestInfoOpen = True }, Cmd.none )
850
+
851
+ UserClickedOpenPageView ->
852
+ let
853
+ nextModel =
854
+ { model
855
+ | pageViewImageIndex = 0
856
+ , pageViewOpen = True
857
+ , pageViewSidebarVisible = True
858
+ , sidebarState = ensureSidebarVisible model.sidebarState
859
+ }
860
+ in
861
+ ( nextModel, sendPageViewPreview nextModel )
862
+
863
+ UserClickedPageViewImageChoice index ->
864
+ let
865
+ nextModel =
866
+ { model | pageViewImageIndex = index }
867
+ in
868
+ ( nextModel, sendPageViewPreview nextModel )
869
+
870
+ UserClickedPageViewNext ->
871
+ handlePageViewStep 1 model
872
+
873
+ UserClickedPageViewPrev ->
874
+ handlePageViewStep -1 model
875
+
876
+ UserClickedRange rangeId maybeIndex ->
877
+ let
878
+ nextModel =
879
+ { model
880
+ | pendingThumbScroll = maybeIndex
881
+ , selectedIndex =
882
+ case maybeIndex of
883
+ Just index ->
884
+ Just index
885
+
886
+ Nothing ->
887
+ model.selectedIndex
888
+ , selectedRangeId = Just rangeId
889
+ , sidebarState = ensureSidebarVisible model.sidebarState
890
+ , thumbsInstantScroll = True
891
+ }
892
+
893
+ scrollCmd =
894
+ case maybeIndex of
895
+ Just index ->
896
+ scrollToIndex index
897
+
898
+ Nothing ->
899
+ Cmd.none
900
+ in
901
+ ( nextModel
902
+ , Cmd.batch
903
+ [ scrollCmd
904
+ , sendPageViewPreview nextModel
905
+ ]
906
+ )
907
+
908
+ UserClickedSaveFilteredImage ->
909
+ ( model, saveFilteredImage () )
910
+
911
+ UserClickedThumbnail index ->
912
+ let
913
+ nextModel =
914
+ { model
915
+ | pageViewImageIndex = 0
916
+ , selectedIndex = Just index
917
+ , sidebarState = ensureSidebarVisible model.sidebarState
918
+ , thumbsInstantScroll = False
919
+ }
920
+ in
921
+ ( nextModel
922
+ , Cmd.batch
923
+ [ scrollToIndex index
924
+ , scrollThumbsToIndex (nextModel.sidebarState == SidebarThumbnails) index
925
+ , sendPageViewPreview nextModel
926
+ ]
927
+ )
928
+
929
+ UserClickedZoomIn ->
930
+ updateZoom model zoomInFactor
931
+
932
+ UserClickedZoomOut ->
933
+ updateZoom model zoomOutFactor
934
+
935
+ UserCopiedFilterJson ->
936
+ let
937
+ json =
938
+ encodeActiveFilters model.filters
939
+ in
940
+ ( { model | filtersJsonError = Nothing, filtersJsonInput = json }
941
+ , copyToClipboard json
942
+ )
943
+
944
+ UserDraggedCollectionSidebarResize clientX ->
945
+ case model.collectionSidebarDrag of
946
+ Just drag ->
947
+ let
948
+ nextWidth =
949
+ (drag.startWidth + (clientX - drag.startX))
950
+ |> clamp 240 480
951
+ in
952
+ ( { model | collectionSidebarWidth = nextWidth }, Cmd.none )
953
+
954
+ Nothing ->
955
+ ( model, Cmd.none )
956
+
957
+ UserDraggedSidebarResize clientX ->
958
+ case model.sidebarDrag of
959
+ Just drag ->
960
+ let
961
+ delta =
962
+ drag.startX - clientX
963
+
964
+ nextWidth =
965
+ clamp 220 520 (drag.startWidth + delta)
966
+ in
967
+ ( { model | sidebarWidth = nextWidth }, Cmd.none )
968
+
969
+ Nothing ->
970
+ ( model, Cmd.none )
971
+
972
+ UserEndedCollectionSidebarResize ->
973
+ ( { model | collectionSidebarDrag = Nothing }, Cmd.none )
974
+
975
+ UserEndedSidebarResize ->
976
+ ( { model | sidebarDrag = Nothing }, Cmd.none )
977
+
978
+ UserResetAllFilters ->
979
+ let
980
+ nextModel =
981
+ { model
982
+ | filters = resetFilters
983
+ , filtersJsonError = Nothing
984
+ }
985
+ in
986
+ ( nextModel, sendPageViewPreview nextModel )
987
+
988
+ UserResetAltColourAdjust ->
989
+ let
990
+ nextModel =
991
+ updateFilters resetAltColourAdjust model
992
+ in
993
+ ( nextModel, sendPageViewPreview nextModel )
994
+
995
+ UserSelectedContentsIndex ->
996
+ ( { model | contentsView = ContentsIndex }, Cmd.none )
997
+
998
+ UserSelectedContentsPages ->
999
+ ( { model | contentsView = ContentsPages }, Cmd.none )
1000
+
1001
+ UserStartedCollectionSidebarResize clientX ->
1002
+ ( { model | collectionSidebarDrag = Just { startWidth = model.collectionSidebarWidth, startX = clientX } }
1003
+ , Cmd.none
1004
+ )
1005
+
1006
+ UserStartedSidebarResize clientX ->
1007
+ ( { model | sidebarDrag = Just { startWidth = model.sidebarWidth, startX = clientX } }
1008
+ , Cmd.none
1009
+ )
1010
+
1011
+ UserToggledContents ->
1012
+ ( { model
1013
+ | sidebarState = SidebarContents
1014
+ }
1015
+ , Cmd.none
1016
+ )
1017
+
1018
+ UserToggledFilter toggle enabled ->
1019
+ let
1020
+ nextModel =
1021
+ updateFilters (applyFilterToggle toggle enabled) model
1022
+ in
1023
+ ( nextModel, sendPageViewPreview nextModel )
1024
+
1025
+ UserToggledFilterGroup groupId ->
1026
+ let
1027
+ nextExpanded =
1028
+ if Set.member groupId model.filterGroupExpanded then
1029
+ Set.remove groupId model.filterGroupExpanded
1030
+
1031
+ else
1032
+ Set.insert groupId model.filterGroupExpanded
1033
+ in
1034
+ ( { model | filterGroupExpanded = nextExpanded }, Cmd.none )
1035
+
1036
+ UserToggledFullscreen ->
1037
+ let
1038
+ nextFullscreen =
1039
+ not model.fullscreen
1040
+ in
1041
+ ( { model | fullscreen = nextFullscreen }
1042
+ , setFullscreen nextFullscreen
1043
+ )
1044
+
1045
+ UserToggledMetadata ->
1046
+ ( { model | sidebarState = SidebarMetadata }, Cmd.none )
1047
+
1048
+ UserToggledPageViewFullscreen ->
1049
+ ( { model | pageViewFullscreen = not model.pageViewFullscreen }, Cmd.none )
1050
+
1051
+ UserToggledPageViewSidebar ->
1052
+ ( { model | pageViewSidebarVisible = not model.pageViewSidebarVisible }, Cmd.none )
1053
+
1054
+ UserToggledShiftByOne ->
1055
+ case model.viewMode of
1056
+ OneUp ->
1057
+ ( model, Cmd.none )
1058
+
1059
+ TwoUp ->
1060
+ let
1061
+ nextShift =
1062
+ not model.shiftByOne
1063
+ in
1064
+ ( { model | shiftByOne = nextShift }
1065
+ , layoutModeUpdated (layoutModeToString TwoUp nextShift)
1066
+ )
1067
+
1068
+ UserToggledSidebar ->
1069
+ if model.isMobile then
1070
+ if model.mobileSidebarOpen then
1071
+ ( { model
1072
+ | mobileSidebarOpen = False
1073
+ , sidebarState = SidebarHidden
1074
+ }
1075
+ , Cmd.none
1076
+ )
1077
+
1078
+ else
1079
+ ( { model
1080
+ | mobileSidebarOpen = True
1081
+ , sidebarState = ensureSidebarVisible model.sidebarState
1082
+ }
1083
+ , Cmd.none
1084
+ )
1085
+
1086
+ else if model.sidebarState == SidebarHidden then
1087
+ ( { model | sidebarState = SidebarThumbnails }, Cmd.none )
1088
+
1089
+ else
1090
+ ( { model | sidebarState = SidebarHidden }, Cmd.none )
1091
+
1092
+ UserToggledThumbnails ->
1093
+ let
1094
+ nextModel =
1095
+ { model | sidebarState = SidebarThumbnails }
1096
+
1097
+ thumbCmd =
1098
+ case ( model.pendingThumbScroll, model.selectedIndex ) of
1099
+ ( Just index, _ ) ->
1100
+ scrollThumbsToIndex True index
1101
+
1102
+ ( Nothing, Just index ) ->
1103
+ scrollThumbsToIndex True index
1104
+
1105
+ _ ->
1106
+ Cmd.none
1107
+
1108
+ nextInstant =
1109
+ case model.pendingThumbScroll of
1110
+ Just _ ->
1111
+ True
1112
+
1113
+ Nothing ->
1114
+ False
1115
+ in
1116
+ ( { nextModel
1117
+ | pendingThumbScroll = Nothing
1118
+ , thumbsInstantScroll = nextInstant
1119
+ }
1120
+ , thumbCmd
1121
+ )
1122
+
1123
+ UserToggledTwoUp ->
1124
+ let
1125
+ nextMode =
1126
+ case model.viewMode of
1127
+ OneUp ->
1128
+ TwoUp
1129
+
1130
+ TwoUp ->
1131
+ OneUp
1132
+ in
1133
+ ( { model | viewMode = nextMode }
1134
+ , layoutModeUpdated (layoutModeToString nextMode model.shiftByOne)
1135
+ )
1136
+
1137
+ UserUpdatedFilterFloat floatFilter raw ->
1138
+ let
1139
+ nextModel =
1140
+ updateFilters (applyFloatFilter floatFilter raw) model
1141
+ in
1142
+ ( nextModel, sendPageViewPreview nextModel )
1143
+
1144
+ UserUpdatedFilterInt intFilter raw ->
1145
+ let
1146
+ nextModel =
1147
+ updateFilters (applyIntFilter intFilter raw) model
1148
+ in
1149
+ ( nextModel, sendPageViewPreview nextModel )
1150
+
1151
+ UserUpdatedFilterJsonInput raw ->
1152
+ ( { model | filtersJsonError = Nothing, filtersJsonInput = raw }, Cmd.none )
1153
+
1154
+ UserUpdatedFilterString stringFilter raw ->
1155
+ let
1156
+ nextModel =
1157
+ updateFilters (applyStringFilter stringFilter raw) model
1158
+ in
1159
+ ( nextModel, sendPageViewPreview nextModel )
1160
+
1161
+ ViewerLoadingChanged isLoading ->
1162
+ ( { model | isViewerLoading = isLoading }, Cmd.none )
1163
+
1164
+ ViewportChanged width height ->
1165
+ let
1166
+ shortSide =
1167
+ min width height
1168
+
1169
+ nextIsMobile =
1170
+ shortSide <= mobileShortSideBreakpoint
1171
+
1172
+ nextModel =
1173
+ if nextIsMobile then
1174
+ { model
1175
+ | isMobile = True
1176
+ , mobileSidebarOpen = False
1177
+ , sidebarState = SidebarHidden
1178
+ }
1179
+
1180
+ else
1181
+ { model
1182
+ | isMobile = False
1183
+ , mobileSidebarOpen = False
1184
+ }
1185
+ in
1186
+ ( nextModel, Cmd.none )
1187
+
1188
+
1189
+ updateZoom : Model -> Float -> ( Model, Cmd Msg )
1190
+ updateZoom model factor =
1191
+ ( model, zoomBy factor )
1192
+
1193
+
1194
+ viewingDirectionToString : ViewingDirection -> String
1195
+ viewingDirectionToString direction =
1196
+ case direction of
1197
+ LeftToRight ->
1198
+ "ltr"
1199
+
1200
+ RightToLeft ->
1201
+ "rtl"
1202
+
1203
+ TopToBottom ->
1204
+ "ltr"
1205
+
1206
+ BottomToTop ->
1207
+ "ltr"
1208
+
1209
+
1210
+ zoomInFactor : Float
1211
+ zoomInFactor =
1212
+ 1.6
1213
+
1214
+
1215
+ zoomOutFactor : Float
1216
+ zoomOutFactor =
1217
+ 1 / zoomInFactor