diff --git a/.circleci/config.yml b/.circleci/config.yml index 2fdb079897..39c1527ecb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,7 +5,7 @@ jobs: build: docker: # Primary container image where all steps run. - - image: avalonmediasystem/avalon:7.3.0-dev + - image: avalonmediasystem/avalon:develop environment: - DATABASE_URL=postgresql://postgres@localhost:5432/postgres - FEDORA_URL=http://localhost:8080/fcrepo/rest @@ -55,6 +55,8 @@ jobs: name: Clean out existing code command: rm -rf .[!.]* * + - run: git config --global --add safe.directory /home/app/avalon + - samvera/cached_checkout - run: cp config/controlled_vocabulary.yml.example config/controlled_vocabulary.yml @@ -138,7 +140,7 @@ jobs: default: 4 docker: # Primary container image where all steps run. - - image: avalonmediasystem/avalon:7.3.0-dev + - image: avalonmediasystem/avalon:develop working_directory: /home/app/avalon diff --git a/Gemfile b/Gemfile index ceea39185b..0d4ffe2ce3 100644 --- a/Gemfile +++ b/Gemfile @@ -3,10 +3,12 @@ source 'https://rubygems.org' # Core rails gem 'bootsnap', require: false gem 'listen' -gem 'rails', '=6.0.5.1' +gem 'rails', '=6.0.6.1' gem 'sprockets', '~>3.7.2' #gem 'sprockets-rails', require: 'sprockets/railtie' gem 'sqlite3' +# Force newer version of mail for compatibility with rails 6.0.6.1 +gem 'mail', '> 2.8.0.1' # Assets gem 'bootstrap', '~> 4.0' @@ -70,7 +72,7 @@ gem 'omniauth-lti', git: "https://github.com/avalonmediasystem/omniauth-lti.git" gem "omniauth-saml", "~> 2.0" # Media Access & Transcoding -gem 'active_encode', '~> 1.0' +gem 'active_encode', '~> 1.0', '>= 1.1.2' gem 'audio_waveform-ruby', '~> 1.0.7', require: 'audio_waveform' gem 'browse-everything', git: "https://github.com/avalonmediasystem/browse-everything.git", branch: 'v1.2-avalon' gem 'fastimage' diff --git a/Gemfile.lock b/Gemfile.lock index a779028398..6fb6a6a090 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -74,38 +74,38 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (6.0.5.1) - actionpack (= 6.0.5.1) + actioncable (6.0.6.1) + actionpack (= 6.0.6.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.0.5.1) - actionpack (= 6.0.5.1) - activejob (= 6.0.5.1) - activerecord (= 6.0.5.1) - activestorage (= 6.0.5.1) - activesupport (= 6.0.5.1) + actionmailbox (6.0.6.1) + actionpack (= 6.0.6.1) + activejob (= 6.0.6.1) + activerecord (= 6.0.6.1) + activestorage (= 6.0.6.1) + activesupport (= 6.0.6.1) mail (>= 2.7.1) - actionmailer (6.0.5.1) - actionpack (= 6.0.5.1) - actionview (= 6.0.5.1) - activejob (= 6.0.5.1) + actionmailer (6.0.6.1) + actionpack (= 6.0.6.1) + actionview (= 6.0.6.1) + activejob (= 6.0.6.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.0.5.1) - actionview (= 6.0.5.1) - activesupport (= 6.0.5.1) + actionpack (6.0.6.1) + actionview (= 6.0.6.1) + activesupport (= 6.0.6.1) rack (~> 2.0, >= 2.0.8) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.0.5.1) - actionpack (= 6.0.5.1) - activerecord (= 6.0.5.1) - activestorage (= 6.0.5.1) - activesupport (= 6.0.5.1) + actiontext (6.0.6.1) + actionpack (= 6.0.6.1) + activerecord (= 6.0.6.1) + activestorage (= 6.0.6.1) + activesupport (= 6.0.6.1) nokogiri (>= 1.8.5) - actionview (6.0.5.1) - activesupport (= 6.0.5.1) + actionview (6.0.6.1) + activesupport (= 6.0.6.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -132,7 +132,7 @@ GEM active_elastic_job (3.2.0) aws-sdk-sqs (~> 1) rails (>= 5.2.6, < 7.1) - active_encode (1.1.1) + active_encode (1.1.2) addressable (~> 2.8) rails active_fedora-datastreams (0.4.0) @@ -142,8 +142,8 @@ GEM om (~> 3.1) rdf (< 3.2) rdf-rdfxml (~> 2.0) - activejob (6.0.5.1) - activesupport (= 6.0.5.1) + activejob (6.0.6.1) + activesupport (= 6.0.6.1) globalid (>= 0.3.6) activejob-traffic_control (0.1.3) activejob (>= 4.2) @@ -152,23 +152,23 @@ GEM activejob-uniqueness (0.2.2) activejob (>= 4.2, < 7) redlock (>= 1.2, < 2) - activemodel (6.0.5.1) - activesupport (= 6.0.5.1) - activerecord (6.0.5.1) - activemodel (= 6.0.5.1) - activesupport (= 6.0.5.1) + activemodel (6.0.6.1) + activesupport (= 6.0.6.1) + activerecord (6.0.6.1) + activemodel (= 6.0.6.1) + activesupport (= 6.0.6.1) activerecord-session_store (2.0.0) actionpack (>= 5.2.4.1) activerecord (>= 5.2.4.1) multi_json (~> 1.11, >= 1.11.2) rack (>= 2.0.8, < 3) railties (>= 5.2.4.1) - activestorage (6.0.5.1) - actionpack (= 6.0.5.1) - activejob (= 6.0.5.1) - activerecord (= 6.0.5.1) + activestorage (6.0.6.1) + actionpack (= 6.0.6.1) + activejob (= 6.0.6.1) + activerecord (= 6.0.6.1) marcel (~> 1.0) - activesupport (6.0.5.1) + activesupport (6.0.6.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) @@ -311,7 +311,7 @@ GEM coffee-script-source execjs coffee-script-source (1.12.2) - concurrent-ruby (1.1.10) + concurrent-ruby (1.2.0) config (4.0.0) deep_merge (~> 1.2, >= 1.2.1) dry-validation (~> 1.0, >= 1.0.0) @@ -320,13 +320,14 @@ GEM rexml crass (1.0.6) daemons (1.4.1) - dalli (3.2.1) + dalli (3.2.3) database_cleaner (2.0.1) database_cleaner-active_record (~> 2.0.0) database_cleaner-active_record (2.0.1) activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) + date (3.3.3) declarative (0.0.20) deep_merge (1.2.2) deprecation (1.1.0) @@ -398,7 +399,7 @@ GEM mail (~> 2.7) equivalent-xml (0.6.0) nokogiri (>= 1.4.3) - erubi (1.11.0) + erubi (1.12.0) et-orbi (1.2.6) tzinfo ethon (0.15.0) @@ -428,7 +429,7 @@ GEM fugit (1.5.2) et-orbi (~> 1.1, >= 1.1.8) raabro (~> 1.4) - globalid (1.0.0) + globalid (1.1.0) activesupport (>= 5.0) google-analytics-rails (1.1.0) google-apis-core (0.7.0) @@ -548,11 +549,14 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.19.0) + loofah (2.19.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) - mail (2.7.1) + mail (2.8.1.rc2) mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp marc (1.1.1) rexml scrub_rb (>= 1.0.1, < 2) @@ -565,16 +569,25 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2022.0105) mini_mime (1.1.2) - mini_portile2 (2.8.0) - minitest (5.16.3) + mini_portile2 (2.8.1) + minitest (5.17.0) msgpack (1.4.5) multi_json (1.15.0) multi_xml (0.6.0) multipart-post (2.2.3) mysql2 (0.5.3) + net-imap (0.3.4) + date + net-protocol net-ldap (0.17.0) + net-pop (0.1.2) + net-protocol + net-protocol (0.2.1) + timeout net-scp (3.0.0) net-ssh (>= 2.6.5, < 7.0.0) + net-smtp (0.3.3) + net-protocol net-ssh (6.1.0) netrc (0.11.0) nio4r (2.5.8) @@ -582,7 +595,7 @@ GEM noid-rails (3.0.3) actionpack (>= 5.0.0, < 7) noid (~> 0.9) - nokogiri (1.13.9) + nokogiri (1.14.1) mini_portile2 (~> 2.8.0) racc (~> 1.4) nom-xml (1.2.0) @@ -635,8 +648,8 @@ GEM puma (5.6.4) nio4r (~> 2.0) raabro (1.4.0) - racc (1.6.0) - rack (2.2.4) + racc (1.6.2) + rack (2.2.6.2) rack-cors (1.1.1) rack (>= 2.0.0) rack-protection (2.2.0) @@ -645,20 +658,20 @@ GEM rack rack-test (2.0.2) rack (>= 1.3) - rails (6.0.5.1) - actioncable (= 6.0.5.1) - actionmailbox (= 6.0.5.1) - actionmailer (= 6.0.5.1) - actionpack (= 6.0.5.1) - actiontext (= 6.0.5.1) - actionview (= 6.0.5.1) - activejob (= 6.0.5.1) - activemodel (= 6.0.5.1) - activerecord (= 6.0.5.1) - activestorage (= 6.0.5.1) - activesupport (= 6.0.5.1) + rails (6.0.6.1) + actioncable (= 6.0.6.1) + actionmailbox (= 6.0.6.1) + actionmailer (= 6.0.6.1) + actionpack (= 6.0.6.1) + actiontext (= 6.0.6.1) + actionview (= 6.0.6.1) + activejob (= 6.0.6.1) + activemodel (= 6.0.6.1) + activerecord (= 6.0.6.1) + activestorage (= 6.0.6.1) + activesupport (= 6.0.6.1) bundler (>= 1.3.0) - railties (= 6.0.5.1) + railties (= 6.0.6.1) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) @@ -667,14 +680,14 @@ GEM rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.4.3) - loofah (~> 2.3) + rails-html-sanitizer (1.5.0) + loofah (~> 2.19, >= 2.19.1) rails_same_site_cookie (0.1.9) rack (>= 1.5) user_agent_parser (~> 2.6) - railties (6.0.5.1) - actionpack (= 6.0.5.1) - activesupport (= 6.0.5.1) + railties (6.0.6.1) + actionpack (= 6.0.6.1) + activesupport (= 6.0.6.1) method_source rake (>= 0.8.7) thor (>= 0.20.3, < 2.0) @@ -892,6 +905,7 @@ GEM thor (1.2.1) thread_safe (0.3.6) tilt (2.0.10) + timeout (0.3.1) trailblazer-option (0.1.2) twitter-typeahead-rails (0.11.1.pre.corejavascript) actionpack (>= 3.1) @@ -899,7 +913,7 @@ GEM railties (>= 3.1) typhoeus (1.4.0) ethon (>= 0.9.0) - tzinfo (1.2.10) + tzinfo (1.2.11) thread_safe (~> 0.1) uber (0.0.15) uglifier (4.2.0) @@ -959,7 +973,7 @@ DEPENDENCIES active-fedora (~> 13.2, >= 13.2.5) active_annotations (~> 0.4) active_elastic_job - active_encode (~> 1.0) + active_encode (~> 1.0, >= 1.1.2) active_fedora-datastreams (~> 0.4) activejob-traffic_control activejob-uniqueness @@ -1024,6 +1038,7 @@ DEPENDENCIES ldp (~> 1.0.3) listen lograge + mail (> 2.8.0.1) marc media_element_add_to_playlist! mediainfo! @@ -1041,7 +1056,7 @@ DEPENDENCIES pry-rails puma (>= 4.3.8) rack-cors - rails (= 6.0.5.1) + rails (= 6.0.6.1) rails-controller-testing rails_same_site_cookie rb-readline diff --git a/app/assets/javascripts/expand_filename.js b/app/assets/javascripts/expand_filename.js new file mode 100644 index 0000000000..34b030da1d --- /dev/null +++ b/app/assets/javascripts/expand_filename.js @@ -0,0 +1,15 @@ +function expandFilename(id) { + var trunc = document.getElementById("truncated_" + id); + var full = document.getElementById("full_" + id); + var expand = document.getElementById("expand_" + id); + + if (full.style.display === "none") { + full.style.display = ""; + trunc.style.display = "none"; + expand.innerHTML = "(Show less)"; + } else { + full.style.display = "none"; + trunc.style.display = ""; + expand.innerHTML = "(Expand)" + } +} diff --git a/app/assets/javascripts/media_player_wrapper/avalon_player_new.es6 b/app/assets/javascripts/media_player_wrapper/avalon_player_new.es6 index 591fb64494..12765c2587 100644 --- a/app/assets/javascripts/media_player_wrapper/avalon_player_new.es6 +++ b/app/assets/javascripts/media_player_wrapper/avalon_player_new.es6 @@ -396,8 +396,13 @@ class MEJSPlayer { : parseFloat(this.segmentsMap[target.id].fragmentbegin); this.mediaElement.setCurrentTime(time); } - this.mejsUtility.showControlsBriefly(this.player); + // Fix for paused seeking halt when using structure navigation + // by forcing timeupdate event to fire with a quick and pause + if(this.mediaElement.paused) { + this.mediaElement.play(); + this.mediaElement.pause(); + } } /** @@ -538,33 +543,6 @@ class MEJSPlayer { return Object.keys(this.segmentsMap).length > 0; } - /** - * Update section links to reflect active section playing - * @function highlightSectionLink - * @param {string} segmentId - HTML node of section link clicked on - * @return {void} - */ - highlightSectionLink(segmentId) { - const accordionEl = document.getElementById('accordion'); - const htmlCollection = accordionEl.getElementsByClassName('playable wrap'); - let segmentLinks = [].slice.call(htmlCollection); - let segmentEl = document.getElementById(segmentId); - - // Clear "active" style on all section links - segmentLinks.forEach(segmentLink => { - segmentLink.classList.remove('current-stream'); - segmentLink.classList.remove('current-section'); - }); - if (segmentEl) { - // Add style to clicked segment link - segmentEl.classList.add('current-stream'); - // Add style to section title - document - .getElementById('section-title-' + segmentEl.dataset.segment) - .classList.add('current-section'); - } - } - /** * Highlight a range of the Mediaelement player's time rail * @function highlightTimeRail diff --git a/app/assets/stylesheets/avalon.scss b/app/assets/stylesheets/avalon.scss index adab8c6589..866af83572 100644 --- a/app/assets/stylesheets/avalon.scss +++ b/app/assets/stylesheets/avalon.scss @@ -409,7 +409,7 @@ a[data-trigger='submit'] { margin-top: 10px; margin-bottom: 10px; } - + @include media-breakpoint-down(sm) { margin-top: 1rem; padding: 0; @@ -842,6 +842,14 @@ h5.card-title { float: right; width: auto; } + + /* + Override default d-flex behavior to display + media object edit page buttons properly + */ + .d-flex { + gap: 5px; + } } .file-upload { diff --git a/app/controllers/admin/collections_controller.rb b/app/controllers/admin/collections_controller.rb index 21d24fc029..f7ab5d33ab 100644 --- a/app/controllers/admin/collections_controller.rb +++ b/app/controllers/admin/collections_controller.rb @@ -82,7 +82,7 @@ def edit # GET /collections/1/items def items mos = paginate @collection.media_objects - render json: mos.to_a.collect{|mo| [mo.id, mo.as_json] }.to_h + render json: mos.to_a.collect { |mo| [mo.id, mo.as_json(include_structure: params[:include_structure] == "true")] }.to_h end # POST /collections @@ -97,10 +97,25 @@ def create subject: "New collection: #{@collection.name}" ).deliver_later end - render json: {id: @collection.id}, status: 200 + respond_to do |format| + format.html do + redirect_to @collection, notice: 'Collection was successfully created.' + end + format.json do + render json: {id: @collection.id}, status: 200 + end + end else logger.warn "Failed to create collection #{@collection.name rescue ''}: #{@collection.errors.full_messages}" - render json: {errors: ['Failed to create collection:']+@collection.errors.full_messages}, status: 422 + respond_to do |format| + format.html do + flash.now[:error] = @collection.errors.full_messages.to_sentence + render action: 'new' + end + format.json do + render json: { errors: ['Failed to create collection:']+@collection.errors.full_messages}, status: 422 + end + end end end diff --git a/app/controllers/media_objects_controller.rb b/app/controllers/media_objects_controller.rb index 4b457566ff..d4f3f4f105 100644 --- a/app/controllers/media_objects_controller.rb +++ b/app/controllers/media_objects_controller.rb @@ -318,11 +318,8 @@ def custom_update end def index - respond_to do |format| - format.json { - paginate json: MediaObject.accessible_by(current_ability, :index) - } - end + mos = paginate MediaObject.accessible_by(current_ability, :index) + render json: mos.to_a.collect { |mo| mo.as_json(include_structure: params[:include_structure] == "true") } end def show diff --git a/app/javascript/packs/iiif-timeliner.js b/app/javascript/packs/iiif-timeliner.js index 46bd3f6c28..c5e8786c40 100644 --- a/app/javascript/packs/iiif-timeliner.js +++ b/app/javascript/packs/iiif-timeliner.js @@ -10917,7 +10917,7 @@ import "../iiif-timeliner-styles.css" /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; - eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_slicedToArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/slicedToArray */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/slicedToArray.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_objectWithoutProperties__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/objectWithoutProperties */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/objectWithoutProperties.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! react-redux */ \"./node_modules/react-redux/es/index.js\");\n/* harmony import */ var _actions_canvas__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../actions/canvas */ \"./src/actions/canvas.js\");\n/* harmony import */ var _actions_viewState__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../actions/viewState */ \"./src/actions/viewState.js\");\n/* harmony import */ var mediaelement_standalone__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! mediaelement/standalone */ \"./node_modules/mediaelement/standalone.js\");\n/* harmony import */ var mediaelement_standalone__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(mediaelement_standalone__WEBPACK_IMPORTED_MODULE_6__);\n/* harmony import */ var _hooks_useEventListener__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../../hooks/useEventListener */ \"./src/hooks/useEventListener.js\");\n/* harmony import */ var _hooks_useInterval__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ../../hooks/useInterval */ \"./src/hooks/useInterval.js\");\n/* harmony import */ var _constants_canvas__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ../../constants/canvas */ \"./src/constants/canvas.js\");\n\n\nvar _jsxFileName = \"/home/dwithana/github/iu/timeliner/src/containers/Audio/Audio.js\";\n\n\n\n // Media Element\n\n\n\n\n\nvar _window = window,\n MediaElement = _window.MediaElement;\n\nfunction Audio(_ref) {\n var url = _ref.url,\n volume = _ref.volume,\n currentTime = _ref.currentTime,\n startTime = _ref.startTime,\n isPlaying = _ref.isPlaying,\n props = Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_objectWithoutProperties__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_ref, [\"url\", \"volume\", \"currentTime\", \"startTime\", \"isPlaying\"]);\n\n var audio = Object(react__WEBPACK_IMPORTED_MODULE_2__[\"useRef\"])();\n var player = Object(react__WEBPACK_IMPORTED_MODULE_2__[\"useRef\"])();\n\n var _useState = Object(react__WEBPACK_IMPORTED_MODULE_2__[\"useState\"])(),\n _useState2 = Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_slicedToArray__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(_useState, 2),\n duration = _useState2[0],\n setDuration = _useState2[1];\n\n var _useState3 = Object(react__WEBPACK_IMPORTED_MODULE_2__[\"useState\"])(),\n _useState4 = Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_slicedToArray__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(_useState3, 2),\n loaded = _useState4[0],\n setLoaded = _useState4[1];\n\n var sources = [{\n src: url\n }];\n var lastTime = Object(react__WEBPACK_IMPORTED_MODULE_2__[\"useRef\"])(function () {\n return startTime - 1;\n }); // Bootstrap the element.\n\n Object(react__WEBPACK_IMPORTED_MODULE_2__[\"useLayoutEffect\"])(function () {\n var element = new MediaElement(audio.current, {\n startVolume: volume / 100,\n currentTime: currentTime / 1000\n }, sources);\n player.current = element;\n setLoaded(false);\n return function () {\n element.remove();\n };\n }, []);\n Object(_hooks_useEventListener__WEBPACK_IMPORTED_MODULE_7__[\"default\"])(player, 'error', function (event) {\n if (event && event.type === 'error') {\n // This will need to be refined.\n props.mediaError('error', _constants_canvas__WEBPACK_IMPORTED_MODULE_9__[\"ERROR_CODES\"].MEDIA_ERR_NETWORK);\n }\n }); // Loop timer for calculating current time.\n\n Object(_hooks_useInterval__WEBPACK_IMPORTED_MODULE_8__[\"default\"])(function () {\n var position = player.current.getCurrentTime();\n var relPosition = position * 1000 - startTime;\n\n if (position * 1000 !== lastTime.current) {\n lastTime.current = position * 1000;\n props.setCurrentTime(relPosition);\n }\n\n if (player.current.readyState && loaded === false) {\n setDuration(props.runTime || player.current.duration * 1000);\n props.mediaLoading(1, 1, props.runTime || player.current.duration * 1000);\n props.mediaLoaded(true);\n setLoaded(true);\n }\n\n if (relPosition >= duration && isPlaying) {\n props.finishedPlaying();\n }\n }, 1000 / 5, [loaded]); // Handle play/pause\n\n Object(react__WEBPACK_IMPORTED_MODULE_2__[\"useLayoutEffect\"])(function () {\n if (player.current) {\n if (isPlaying) {\n player.current.play();\n } else {\n if (player.current.readyState) {\n player.current.pause();\n }\n }\n }\n }, [isPlaying, url]); // Handle volume change.\n\n Object(react__WEBPACK_IMPORTED_MODULE_2__[\"useLayoutEffect\"])(function () {\n if (player.current) {\n player.current.setVolume(volume / 100);\n }\n }, [volume, url]); // Handle user-changed current time.\n\n Object(react__WEBPACK_IMPORTED_MODULE_2__[\"useLayoutEffect\"])(function () {\n if (player.current && currentTime !== lastTime.current) {\n // Toggle isSeeked flag in the state\n props.seek(!props.isSeeked);\n lastTime.current = currentTime;\n player.current.setCurrentTime(currentTime / 1000);\n props.setCurrentTime(currentTime);\n }\n }, [currentTime, url]);\n\n if (!url) {\n return null;\n }\n\n return react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(\"div\", {\n __source: {\n fileName: _jsxFileName,\n lineNumber: 113\n },\n __self: this\n }, react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(\"audio\", {\n ref: audio,\n preload: \"auto\",\n __source: {\n fileName: _jsxFileName,\n lineNumber: 114\n },\n __self: this\n }, sources.map(function (source, key) {\n return react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(\"source\", {\n key: key,\n src: source.src,\n __source: {\n fileName: _jsxFileName,\n lineNumber: 116\n },\n __self: this\n });\n })));\n}\n\nvar mapStateProps = function mapStateProps(state) {\n return {\n url: state.canvas.url,\n isSeeked: state.viewState.isSeeked,\n isPlaying: state.viewState.isPlaying,\n currentTime: state.viewState.currentTime + state.viewState.startTime,\n volume: state.viewState.volume,\n runTime: state.viewState.runTime,\n startTime: state.viewState.startTime\n };\n};\n\nvar mapDispatchToProps = {\n mediaLoading: _actions_canvas__WEBPACK_IMPORTED_MODULE_4__[\"mediaLoading\"],\n mediaLoaded: _actions_canvas__WEBPACK_IMPORTED_MODULE_4__[\"mediaLoaded\"],\n mediaError: _actions_canvas__WEBPACK_IMPORTED_MODULE_4__[\"mediaError\"],\n setCurrentTime: _actions_viewState__WEBPACK_IMPORTED_MODULE_5__[\"setCurrentTime\"],\n finishedPlaying: _actions_viewState__WEBPACK_IMPORTED_MODULE_5__[\"finishedPlaying\"],\n seek: _actions_viewState__WEBPACK_IMPORTED_MODULE_5__[\"seek\"]\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = (Object(react_redux__WEBPACK_IMPORTED_MODULE_3__[\"connect\"])(mapStateProps, mapDispatchToProps)(Audio));\n\n//# sourceURL=webpack:///./src/containers/Audio/Audio.js?"); + eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_slicedToArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/slicedToArray */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/slicedToArray.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_objectWithoutProperties__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/objectWithoutProperties */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/objectWithoutProperties.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! react-redux */ \"./node_modules/react-redux/es/index.js\");\n/* harmony import */ var _actions_canvas__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../actions/canvas */ \"./src/actions/canvas.js\");\n/* harmony import */ var _actions_viewState__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../actions/viewState */ \"./src/actions/viewState.js\");\n/* harmony import */ var mediaelement_standalone__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! mediaelement/standalone */ \"./node_modules/mediaelement/standalone.js\");\n/* harmony import */ var mediaelement_standalone__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(mediaelement_standalone__WEBPACK_IMPORTED_MODULE_6__);\n/* harmony import */ var _hooks_useEventListener__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../../hooks/useEventListener */ \"./src/hooks/useEventListener.js\");\n/* harmony import */ var _hooks_useInterval__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ../../hooks/useInterval */ \"./src/hooks/useInterval.js\");\n/* harmony import */ var _constants_canvas__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ../../constants/canvas */ \"./src/constants/canvas.js\");\n\n\nvar _jsxFileName = \"/home/dwithana/github/iu/timeliner/src/containers/Audio/Audio.js\";\n\n\n\n // Media Element\n\n\n\n\n\nvar _window = window,\n MediaElement = _window.MediaElement;\n\nfunction Audio(_ref) {\n var url = _ref.url,\n volume = _ref.volume,\n currentTime = _ref.currentTime,\n startTime = _ref.startTime,\n isPlaying = _ref.isPlaying,\n props = Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_objectWithoutProperties__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_ref, [\"url\", \"volume\", \"currentTime\", \"startTime\", \"isPlaying\"]);\n\n var audio = Object(react__WEBPACK_IMPORTED_MODULE_2__[\"useRef\"])();\n var player = Object(react__WEBPACK_IMPORTED_MODULE_2__[\"useRef\"])();\n\n var _useState = Object(react__WEBPACK_IMPORTED_MODULE_2__[\"useState\"])(),\n _useState2 = Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_slicedToArray__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(_useState, 2),\n duration = _useState2[0],\n setDuration = _useState2[1];\n\n var _useState3 = Object(react__WEBPACK_IMPORTED_MODULE_2__[\"useState\"])(),\n _useState4 = Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_slicedToArray__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(_useState3, 2),\n loaded = _useState4[0],\n setLoaded = _useState4[1];\n\n var sources = [{\n src: url\n }];\n var lastTime = Object(react__WEBPACK_IMPORTED_MODULE_2__[\"useRef\"])(function () {\n return startTime - 1;\n }); // Bootstrap the element.\n\n Object(react__WEBPACK_IMPORTED_MODULE_2__[\"useLayoutEffect\"])(function () {\n var element = new MediaElement(audio.current, {\n startVolume: volume / 100,\n currentTime: currentTime / 1000\n }, sources);\n player.current = element;\n setLoaded(false);\n return function () {\n element.remove();\n };\n }, []);\n Object(_hooks_useEventListener__WEBPACK_IMPORTED_MODULE_7__[\"default\"])(player, 'error', function (event) {\n if (event && event.type === 'error') {\n // This will need to be refined.\n props.mediaError('error', _constants_canvas__WEBPACK_IMPORTED_MODULE_9__[\"ERROR_CODES\"].MEDIA_ERR_NETWORK);\n }\n }); // Loop timer for calculating current time.\n\n Object(_hooks_useInterval__WEBPACK_IMPORTED_MODULE_8__[\"default\"])(function () {\n var position = player.current.getCurrentTime();\n var relPosition = position * 1000 - startTime;\n\n if (position * 1000 !== lastTime.current) {\n lastTime.current = position * 1000;\n props.setCurrentTime(relPosition);\n }\n\n if (player.current.readyState && loaded === false) {\n setDuration(props.runTime || player.current.duration * 1000);\n props.mediaLoading(1, 1, props.runTime || player.current.duration * 1000);\n props.mediaLoaded(true);\n setLoaded(true);\n }\n\n if (relPosition >= duration && isPlaying) {\n props.finishedPlaying();\n }\n }, 1000 / 5, [loaded]); // Handle play/pause\n\n Object(react__WEBPACK_IMPORTED_MODULE_2__[\"useLayoutEffect\"])(function () {\n if (player.current) {\n if (isPlaying) {\n player.current.play();\n } else {\n if (player.current.readyState) {\n player.current.pause();\n }\n }\n }\n }, [isPlaying, url]); // Handle volume change.\n\n Object(react__WEBPACK_IMPORTED_MODULE_2__[\"useLayoutEffect\"])(function () {\n if (player.current) {\n player.current.setVolume(volume / 100);\n }\n }, [volume, url]); // Handle user-changed current time.\n\n Object(react__WEBPACK_IMPORTED_MODULE_2__[\"useLayoutEffect\"])(function () {\n if (player.current && currentTime !== lastTime.current) {\n // Toggle isSeeked flag in the state\n props.seek(!props.isSeeked);\n lastTime.current = currentTime;\n player.current.setCurrentTime(currentTime / 1000);\n }\n }, [currentTime, url]);\n\n if (!url) {\n return null;\n }\n\n return react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(\"div\", {\n __source: {\n fileName: _jsxFileName,\n lineNumber: 112\n },\n __self: this\n }, react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(\"audio\", {\n ref: audio,\n preload: \"auto\",\n __source: {\n fileName: _jsxFileName,\n lineNumber: 113\n },\n __self: this\n }, sources.map(function (source, key) {\n return react__WEBPACK_IMPORTED_MODULE_2___default.a.createElement(\"source\", {\n key: key,\n src: source.src,\n __source: {\n fileName: _jsxFileName,\n lineNumber: 115\n },\n __self: this\n });\n })));\n}\n\nvar mapStateProps = function mapStateProps(state) {\n return {\n url: state.canvas.url,\n isSeeked: state.viewState.isSeeked,\n isPlaying: state.viewState.isPlaying,\n currentTime: state.viewState.currentTime + state.viewState.startTime,\n volume: state.viewState.volume,\n runTime: state.viewState.runTime,\n startTime: state.viewState.startTime\n };\n};\n\nvar mapDispatchToProps = {\n mediaLoading: _actions_canvas__WEBPACK_IMPORTED_MODULE_4__[\"mediaLoading\"],\n mediaLoaded: _actions_canvas__WEBPACK_IMPORTED_MODULE_4__[\"mediaLoaded\"],\n mediaError: _actions_canvas__WEBPACK_IMPORTED_MODULE_4__[\"mediaError\"],\n setCurrentTime: _actions_viewState__WEBPACK_IMPORTED_MODULE_5__[\"setCurrentTime\"],\n finishedPlaying: _actions_viewState__WEBPACK_IMPORTED_MODULE_5__[\"finishedPlaying\"],\n seek: _actions_viewState__WEBPACK_IMPORTED_MODULE_5__[\"seek\"]\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = (Object(react_redux__WEBPACK_IMPORTED_MODULE_3__[\"connect\"])(mapStateProps, mapDispatchToProps)(Audio));\n\n//# sourceURL=webpack:///./src/containers/Audio/Audio.js?"); /***/ }), @@ -11315,3 +11315,4 @@ import "../iiif-timeliner-styles.css" /***/ }) /******/ }); + \ No newline at end of file diff --git a/app/models/indexed_file.rb b/app/models/indexed_file.rb index 37abeaf868..eafebaecfa 100644 --- a/app/models/indexed_file.rb +++ b/app/models/indexed_file.rb @@ -14,4 +14,9 @@ class IndexedFile < ActiveFedora::File include SpeedyAF::IndexedContent + + # Override + def original_name + super.force_encoding("UTF-8") + end end diff --git a/app/models/master_file.rb b/app/models/master_file.rb index f47faf746f..3d1e3e4621 100644 --- a/app/models/master_file.rb +++ b/app/models/master_file.rb @@ -569,17 +569,17 @@ def find_frame_source(options={}) unless File.exists?(response[:source]) Rails.logger.warn("Masterfile `#{file_location}` not found. Extracting via HLS.") - hls_temp_file, new_offset = create_frame_source_hls_temp_file + hls_temp_file, new_offset = create_frame_source_hls_temp_file(options[:offset]) response = { source: hls_temp_file, offset: new_offset, non_temp_file: false } end return response end - def create_frame_source_hls_temp_file + def create_frame_source_hls_temp_file(offset) playlist_url = self.stream_details[:stream_hls].find { |d| d[:quality] == 'high' }[:url] secure_url = SecurityHandler.secure_url(playlist_url, target: self.id) playlist = Avalon::M3U8Reader.read(secure_url) - details = playlist.at(options[:offset]) + details = playlist.at(offset) # Fixes https://github.com/avalonmediasystem/avalon/issues/3474 target_location = File.basename(details[:location]).split('?')[0] diff --git a/app/services/file_locator.rb b/app/services/file_locator.rb index e4d56755d0..1708358076 100644 --- a/app/services/file_locator.rb +++ b/app/services/file_locator.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -24,7 +24,7 @@ class S3File def initialize(uri) uri = Addressable::URI.parse(uri) @bucket = Addressable::URI.unencode(uri.host) - @key = Addressable::URI.unencode(uri.path).sub(%r(^/*(.+)/*$),'\1') + @key = Addressable::URI.unencode(ActiveEncode.sanitize_uri(uri)).sub(%r(^/*(.+)/*$),'\1') end def object @@ -74,7 +74,9 @@ def location when 's3' S3File.new(uri).object.presigned_url(:get) when 'file' - Addressable::URI.unencode(uri.path) + # In case file name includes ? or # use full uri omitting components before path + # instead of using only path which would miss query or fragment components + Addressable::URI.unencode(uri.omit(:scheme, :user, :password, :host, :port)) else @uri.to_s end diff --git a/app/views/admin/collections/index.html.erb b/app/views/admin/collections/index.html.erb index aa0b01d6c4..61f3d8644c 100644 --- a/app/views/admin/collections/index.html.erb +++ b/app/views/admin/collections/index.html.erb @@ -13,72 +13,76 @@ Unless required by applicable law or agreed to in writing, software distributed specific language governing permissions and limitations under the License. --- END LICENSE_HEADER BLOCK --- %> + +<% unless Avalon::ControlledVocabulary.vocabulary[:units] %> + <% raise Avalon::VocabularyNotFound.new "Units vocabulary not found." %> +<% end %> + <% @page_title = t('collections.title', :application_name => application_name) %> <% unless @collections.empty? %> -
-
-

My Collections

-
-
- <%= button_tag("Create Collection", class: 'btn btn-primary btn-large', data: {toggle:"modal", target:"#new_collection"}) unless cannot? :create, Admin::Collection %> +
+
+

My Collections

+
+
+ <%= link_to('Create Collection'.html_safe, new_admin_collection_path) unless cannot? :create, Admin::Collection %> +
-
- - - - - - - - - - - - <% @collections.to_a.each do |collection| %> - - - - - - - - <% end %> - -
TitleItemsManagersDescription
<%= link_to collection.name, admin_collection_path(collection.id) %> - <%= link_to(pluralize(collection.media_object_count, 'item'), search_catalog_path('f[collection_ssim][]' => collection.name)) %> - <% if collection.unpublished_media_object_count > 0 %> - <%=link_to("(#{collection.unpublished_media_object_count} unpublished)", search_catalog_path('f[collection_ssim][]' => collection.name, 'f[workflow_published_sim][]' => "Unpublished")) %> - <% end %> - <%= pluralize(collection.manager_count, 'manager') %> <%= collection.description&.truncate(100, separator: ' ') %> - <% if can?(:destroy, collection) %> -
- <%= link_to('Delete', remove_admin_collection_path(collection.id), class: 'btn btn-danger btn-sm')%> -
- <% else %> -   - <% end %> -
+ + + + + + + + + + + + <% @collections.to_a.each do |collection| %> + + + + + + + + <% end %> + +
TitleItemsManagersDescription
<%= link_to collection.name, admin_collection_path(collection.id) %> + <%= link_to(pluralize(collection.media_object_count, 'item'), search_catalog_path('f[collection_ssim][]' => collection.name)) %> + <% if collection.unpublished_media_object_count > 0 %> + <%=link_to("(#{collection.unpublished_media_object_count} unpublished)", search_catalog_path('f[collection_ssim][]' => collection.name, 'f[workflow_published_sim][]' => "Unpublished")) %> + <% end %> + <%= pluralize(collection.manager_count, 'manager') %> <%= collection.description&.truncate(100, separator: ' ') %> + <% if can?(:destroy, collection) %> +
+ <%= link_to('Delete', remove_admin_collection_path(collection.id), class: 'btn btn-danger btn-sm')%> +
+ <% else %> +   + <% end %> +
<% else %> -
-

You don't have any collections yet

+
+

You don't have any collections yet

- <% if can? :create, Admin::Collection %> -

Would you like to create one?

-
-

- <%= button_tag("Create Collection", class: 'btn btn-primary btn-large', data: {toggle:"modal", target:"#new_collection"}) %> -

- <% else %> -

You'll need to be assigned to one

- <% end %> + <% if can? :create, Admin::Collection %> +

Would you like to create one?

+
+

+ <%= link_to('Create Collection'.html_safe, new_admin_collection_path) %> +

+ <% else %> +

You'll need to be assigned to one

+ <% end %> -
+
<% end %> <% @collection = Admin::Collection.new %> -<%= render "form", modal_title: "Create Collection" %> diff --git a/app/views/admin/collections/new.html.erb b/app/views/admin/collections/new.html.erb index 17bc376ab1..7d7826e996 100644 --- a/app/views/admin/collections/new.html.erb +++ b/app/views/admin/collections/new.html.erb @@ -13,4 +13,18 @@ Unless required by applicable law or agreed to in writing, software distributed specific language governing permissions and limitations under the License. --- END LICENSE_HEADER BLOCK --- %> -<%= render :partial => 'form', locals: { modal_title: 'Create Collection' } %> +
+

New collection

+
+<%= bootstrap_form_for @collection, action: 'create' do |f| %> + <%= f.text_field :name %> + <% if @collection.new_record? || can?(:update_unit, @collection)%> + <%= f.select(:unit, Admin::Collection.units, {}, {:class => 'form-control'}) %> + <% end %> + <%= f.text_area :description, rows: 3 %> + <%= f.text_field :contact_email %> + <%= f.text_field :website_url %> + <%= f.text_field :website_label %> +
+ <%= f.submit class: 'btn btn-primary' %> +<% end %> diff --git a/app/views/media_objects/_file_upload.html.erb b/app/views/media_objects/_file_upload.html.erb index 7d4e8bd016..36005022bb 100644 --- a/app/views/media_objects/_file_upload.html.erb +++ b/app/views/media_objects/_file_upload.html.erb @@ -48,7 +48,7 @@ Unless required by applicable law or agreed to in writing, software distributed
-
+
<% case section.file_format when 'Sound' %> @@ -59,10 +59,23 @@ Unless required by applicable law or agreed to in writing, software distributed <% end %> - <%= section.title || truncate_center(File.basename(section.file_location.to_s), 30, 10) %> + <% filename = section.title || File.basename(section.file_location.to_s) %> + title=<%= filename %>> + <%= section.title || truncate_center(File.basename(section.file_location.to_s), 30, 10) %> + + style="display: none;" title=<%= filename %>> + <%= filename %> + <%= number_to_human_size(section.file_size) %> + <% if filename.length > 30 %> + + + + <% end %>
-
+
<% if can? :edit, @media_object %> <%= link_to 'Delete'.html_safe, @@ -79,7 +92,7 @@ Unless required by applicable law or agreed to in writing, software distributed - @@ -311,7 +324,7 @@ Unless required by applicable law or agreed to in writing, software distributed <% content_for :page_scripts do %>