diva.js 6.0.2 → 7.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.clang-format +7 -0
- package/.github/workflows/npm-publish.yml +45 -0
- package/LICENSE +55 -0
- package/Makefile +75 -0
- package/README.md +15 -114
- package/elm.json +32 -0
- package/package.json +12 -59
- package/review/elm.json +52 -0
- package/review/src/ReviewConfig.elm +87 -0
- package/scripts/elm-esm.sh +40 -0
- package/scripts/minify-css.mjs +31 -0
- package/src/Filters.elm +1044 -0
- package/src/Main.elm +1217 -0
- package/src/Model.elm +213 -0
- package/src/Msg.elm +59 -0
- package/src/Utilities.elm +46 -0
- package/src/View/CollectionExplorer.elm +172 -0
- package/src/View/Helpers.elm +86 -0
- package/src/View/HtmlRenderer.elm +136 -0
- package/src/View/Icons.elm +159 -0
- package/src/View/ManifestInfoModal.elm +363 -0
- package/src/View/PageViewModal.elm +1046 -0
- package/src/View/Sidebar.elm +786 -0
- package/src/View/Toolbar.elm +189 -0
- package/src/View.elm +244 -0
- package/src/diva.ts +802 -0
- package/src/filters.ts +1843 -0
- package/src/styles/app.css +328 -0
- package/src/styles/collection.css +75 -0
- package/src/styles/modal.css +388 -0
- package/src/styles/sidebar.css +215 -0
- package/src/styles/theme.css +39 -0
- package/src/styles/toolbar.css +154 -0
- package/src/viewer-element.ts +1307 -0
- package/testing/index.html +52 -0
- package/testing/testing.html +231 -0
- package/tsconfig.json +12 -0
- package/AUTHORS +0 -22
- package/build/diva.css +0 -554
- package/build/diva.css.map +0 -1
- package/build/diva.js +0 -9
- package/build/diva.js.map +0 -1
- package/build/plugins/download.js +0 -2
- package/build/plugins/download.js.map +0 -1
- package/build/plugins/manipulation.js +0 -2
- package/build/plugins/manipulation.js.map +0 -1
- package/build/plugins/metadata.js +0 -2
- package/build/plugins/metadata.js.map +0 -1
- package/index.html +0 -28
- package/karma.conf.js +0 -87
- package/source/css/_mixins.scss +0 -43
- package/source/css/_variables.scss +0 -50
- package/source/css/_viewer.scss +0 -462
- package/source/css/diva.scss +0 -15
- package/source/css/plugins/_manipulation.scss +0 -228
- package/source/css/plugins/_metadata.scss +0 -31
- package/source/img/adjust.svg +0 -11
- package/source/img/book-view.svg +0 -6
- package/source/img/close.svg +0 -6
- package/source/img/download.svg +0 -6
- package/source/img/from-fullscreen.svg +0 -8
- package/source/img/grid-fewer.svg +0 -6
- package/source/img/grid-more.svg +0 -6
- package/source/img/grid-view.svg +0 -6
- package/source/img/link.svg +0 -6
- package/source/img/metadata.svg +0 -9
- package/source/img/page-view.svg +0 -6
- package/source/img/to-fullscreen.svg +0 -11
- package/source/img/zoom-in.svg +0 -6
- package/source/img/zoom-out.svg +0 -7
- package/source/js/composite-image.js +0 -174
- package/source/js/diva-global.js +0 -7
- package/source/js/diva.js +0 -1543
- package/source/js/document-handler.js +0 -180
- package/source/js/document-layout.js +0 -286
- package/source/js/exceptions.js +0 -26
- package/source/js/gesture-events.js +0 -190
- package/source/js/grid-handler.js +0 -122
- package/source/js/iiif-source-adapter.js +0 -63
- package/source/js/image-cache.js +0 -113
- package/source/js/image-manifest.js +0 -157
- package/source/js/image-request-handler.js +0 -76
- package/source/js/interpolate-animation.js +0 -122
- package/source/js/page-layouts/book-layout.js +0 -161
- package/source/js/page-layouts/grid-layout.js +0 -97
- package/source/js/page-layouts/index.js +0 -38
- package/source/js/page-layouts/page-dimensions.js +0 -9
- package/source/js/page-layouts/singles-layout.js +0 -27
- package/source/js/page-overlay-manager.js +0 -102
- package/source/js/page-tools-overlay.js +0 -95
- package/source/js/parse-iiif-manifest.js +0 -302
- package/source/js/plugins/_filters.js +0 -679
- package/source/js/plugins/download.js +0 -83
- package/source/js/plugins/manipulation.js +0 -837
- package/source/js/plugins/metadata.js +0 -190
- package/source/js/renderer.js +0 -584
- package/source/js/settings-view.js +0 -30
- package/source/js/tile-coverage-map.js +0 -25
- package/source/js/toolbar.js +0 -573
- package/source/js/utils/dragscroll.js +0 -106
- package/source/js/utils/elt.js +0 -94
- package/source/js/utils/events.js +0 -190
- package/source/js/utils/get-scrollbar-width.js +0 -29
- package/source/js/utils/hash-params.js +0 -86
- package/source/js/utils/parse-label-value.js +0 -34
- package/source/js/utils/vanilla.kinetic.js +0 -527
- package/source/js/validation-runner.js +0 -177
- package/source/js/viewer-core.js +0 -1514
- package/source/js/viewport.js +0 -143
- package/test/_setup.js +0 -13
- package/test/composite-image_test.js +0 -94
- package/test/diva_test.js +0 -43
- package/test/hash-params_test.js +0 -221
- package/test/image-cache_test.js +0 -106
- package/test/main.js +0 -6
- package/test/manifests/beromunsterManifest.json +0 -15514
- package/test/manifests/iiifv2.json +0 -11032
- package/test/manifests/iiifv2pages.json +0 -30437
- package/test/manifests/iiifv3.json +0 -10965
- package/test/navigation_test.js +0 -355
- package/test/parse-iiif-manifest_test.js +0 -68
- package/test/public_test.js +0 -881
- package/test/settings_test.js +0 -487
- package/test/utils/book-layout_test.js +0 -148
- package/test/utils/elt_test.js +0 -102
- package/test/utils/events_test.js +0 -245
- package/test/utils/hash-params_test.js +0 -79
- package/test/utils/parse-label-value_test.js +0 -45
- package/test/z_plugins_test.js +0 -180
- package/webpack.config.js +0 -58
- 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
|