kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
				
				
				
			Merge branch 'develop'
						commit
						7df97263e5
					
				
							
								
								
									
										14
									
								
								CHANGELOG
								
								
								
								
							
							
						
						
									
										14
									
								
								CHANGELOG
								
								
								
								
							|  | @ -67,7 +67,7 @@ Instance-level moderation tools | |||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| This release includes a first set of moderation tools that will give more control | ||||
| to admins about the way their instance federate with other instance and accounts on the network. | ||||
| to admins about the way their instance federates with other instance and accounts on the network. | ||||
| Using these tools, it's now possible to: | ||||
| 
 | ||||
| - Browse known accounts and domains, and associated data (storage size, software version, etc.) | ||||
|  | @ -75,7 +75,7 @@ Using these tools, it's now possible to: | |||
| - Block or partially restrict interactions with any account or domain | ||||
| 
 | ||||
| All those features are usable using a brand new "moderation" permission, meaning | ||||
| you can appoints one or nultiple moderators to help with this task. | ||||
| you can appoint one or multiple moderators to help with this task. | ||||
| 
 | ||||
| I'd like to thank all Mastodon contributors, because some of the these tools are heavily | ||||
| inspired from what's being done in Mastodon. Thank you so much! | ||||
|  | @ -84,7 +84,7 @@ inspired from what's being done in Mastodon. Thank you so much! | |||
| Iframe widget to embed public tracks and albums [manual action required] | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| Funkwhale now support embedding a lightweight audio player on external websites | ||||
| Funkwhale now supports embedding a lightweight audio player on external websites | ||||
| for album and tracks that are available in public libraries. Important pages, | ||||
| such as artist, album and track pages also include OpenGraph tags that will | ||||
| enable previews on compatible apps (like sharing a Funkwhale track link on Mastodon | ||||
|  | @ -132,13 +132,13 @@ which should be ``/srv/funkwhale/front/dist`` by default, then reload your nginx | |||
| Alternative docker deployment method | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| Thanks to the awesome done by @thetarkus at https://github.com/thetarkus/docker-funkwhale, | ||||
| Thanks to the awesome work done by @thetarkus at https://github.com/thetarkus/docker-funkwhale, | ||||
| we're now able to provide an alternative and easier Docker deployment method! | ||||
| 
 | ||||
| In contrast with our current, multi-container offer, this method integrates | ||||
| all Funkwhale processes and services (database, redis, etc.) into a single, easier to deploy container. | ||||
| 
 | ||||
| Both method will coexist in parallel, as each one has pros and cons. You can learn more | ||||
| Both methods will coexist in parallel, as each one has pros and cons. You can learn more | ||||
| about this exciting new deployment option by visiting https://docs.funkwhale.audio/installation/docker.html! | ||||
| 
 | ||||
| Automatically load .env file | ||||
|  | @ -146,7 +146,7 @@ Automatically load .env file | |||
| 
 | ||||
| On non-docker deployments, earlier versions required you to source | ||||
| the config/.env file before launching any Funkwhale command, with ``export $(cat config/.env | grep -v ^# | xargs)`` | ||||
| This led to more complex and error prode deployment / setup. | ||||
| This led to more complex and error prone deployment / setup. | ||||
| 
 | ||||
| This is not the case anymore, and Funkwhale will automatically load this file if it's available. | ||||
| 
 | ||||
|  | @ -174,7 +174,7 @@ Enable gzip compression [manual action suggested] | |||
| Gzip compression will be enabled on new instances by default | ||||
| and will reduce the amount of bandwidth consumed by your instance. | ||||
| 
 | ||||
| If you with to benefit from gzip compression on your instance, | ||||
| If you want to benefit from gzip compression on your instance, | ||||
| edit your reverse proxy virtualhost file (located at ``/etc/nginx/sites-available/funkwhale.conf``) and add the following snippet | ||||
| in the server block, then reload your nginx server:: | ||||
| 
 | ||||
|  |  | |||
|  | @ -41,15 +41,19 @@ Setup front-end only development environment | |||
| 
 | ||||
|     yarn install | ||||
| 
 | ||||
| 4. Launch the development server:: | ||||
| 4. Compile the translations:: | ||||
| 
 | ||||
|     yarn i18n-compile | ||||
| 
 | ||||
| 5. Launch the development server:: | ||||
| 
 | ||||
|     # this will serve the front-end on http://localhost:8000/front/ | ||||
|     VUE_PORT=8000 yarn serve | ||||
| 
 | ||||
| 5. Make the front-end talk with an existing server (like https://demo.funkwhale.audio), | ||||
| 6. Make the front-end talk with an existing server (like https://demo.funkwhale.audio or https://open.audio), | ||||
|    by clicking on the corresponding link in the footer | ||||
| 
 | ||||
| 6. Start hacking! | ||||
| 7. Start hacking! | ||||
| 
 | ||||
| Setup your development environment | ||||
| ---------------------------------- | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ import uuid | |||
| 
 | ||||
| import markdown | ||||
| import pendulum | ||||
| import pydub | ||||
| from django.conf import settings | ||||
| from django.contrib.postgres.fields import JSONField | ||||
| from django.core.files.base import ContentFile | ||||
|  | @ -780,6 +781,15 @@ class Upload(models.Model): | |||
|             "size": self.get_file_size(), | ||||
|         } | ||||
| 
 | ||||
|     def get_audio_segment(self): | ||||
|         input = self.get_audio_file() | ||||
|         if not input: | ||||
|             return | ||||
| 
 | ||||
|         input_format = utils.MIMETYPE_TO_EXTENSION[self.mimetype] | ||||
|         audio = pydub.AudioSegment.from_file(input, format=input_format) | ||||
|         return audio | ||||
| 
 | ||||
|     def save(self, **kwargs): | ||||
|         if not self.mimetype: | ||||
|             if self.audio_file: | ||||
|  | @ -824,10 +834,9 @@ class Upload(models.Model): | |||
|             0 | ||||
|         ] + ".{}".format(format) | ||||
|         version.audio_file.save(new_name, f) | ||||
|         utils.transcode_file( | ||||
|             input=self.audio_file, | ||||
|         utils.transcode_audio( | ||||
|             audio=self.get_audio_segment(), | ||||
|             output=version.audio_file, | ||||
|             input_format=utils.MIMETYPE_TO_EXTENSION[self.mimetype], | ||||
|             output_format=utils.MIMETYPE_TO_EXTENSION[mimetype], | ||||
|         ) | ||||
|         version.size = version.audio_file.size | ||||
|  |  | |||
|  | @ -75,5 +75,9 @@ def get_actor_from_request(request): | |||
| def transcode_file(input, output, input_format, output_format, **kwargs): | ||||
|     with input.open("rb"): | ||||
|         audio = pydub.AudioSegment.from_file(input, format=input_format) | ||||
|     return transcode_audio(audio, output, output_format, **kwargs) | ||||
| 
 | ||||
| 
 | ||||
| def transcode_audio(audio, output, output_format, **kwargs): | ||||
|     with output.open("wb"): | ||||
|         return audio.export(output, format=output_format, **kwargs) | ||||
|  |  | |||
|  | @ -374,6 +374,32 @@ def test_listen_transcode(factories, now, logged_in_api_client, mocker): | |||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("serve_path", [("/host/music",), ("/app/music",)]) | ||||
| def test_listen_transcode_in_place( | ||||
|     serve_path, factories, now, logged_in_api_client, mocker, settings | ||||
| ): | ||||
|     settings.MUSIC_DIRECTORY_PATH = "/app/music" | ||||
|     settings.MUSIC_DIRECTORY_SERVE_PATH = serve_path | ||||
|     upload = factories["music.Upload"]( | ||||
|         import_status="finished", | ||||
|         library__actor__user=logged_in_api_client.user, | ||||
|         audio_file=None, | ||||
|         source="file://" + os.path.join(DATA_DIR, "test.ogg"), | ||||
|     ) | ||||
| 
 | ||||
|     assert upload.get_audio_segment() | ||||
| 
 | ||||
|     url = reverse("api:v1:listen-detail", kwargs={"uuid": upload.track.uuid}) | ||||
|     handle_serve = mocker.spy(views, "handle_serve") | ||||
|     response = logged_in_api_client.get(url, {"to": "mp3"}) | ||||
| 
 | ||||
|     assert response.status_code == 200 | ||||
| 
 | ||||
|     handle_serve.assert_called_once_with( | ||||
|         upload, user=logged_in_api_client.user, format="mp3" | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| def test_user_can_create_library(factories, logged_in_api_client): | ||||
|     actor = logged_in_api_client.user.create_actor() | ||||
|     url = reverse("api:v1:libraries-list") | ||||
|  |  | |||
|  | @ -0,0 +1 @@ | |||
| Make Apache configuration file work with 0.18 changes (#667) | ||||
|  | @ -0,0 +1 @@ | |||
| Hide pagination when there is only one page of results (#681) | ||||
|  | @ -0,0 +1 @@ | |||
| Fix transcoding of in-place imported tracks (#688) | ||||
|  | @ -5,3 +5,38 @@ Next release notes | |||
| 
 | ||||
|     Those release notes refer to the current development branch and are reset | ||||
|     after each release. | ||||
| 
 | ||||
| Fix Apache configuration file for 0.18 [manual action required] | ||||
| ---------------------------------------------------------- | ||||
| 
 | ||||
| The way front is served has changed since 0.18. The Apache configuration can't serve 0.18 properly, leading to blank screens. | ||||
| 
 | ||||
| If you are on an Apache setup, you will have to replace the `<Location "/api">` block with the following:: | ||||
| 
 | ||||
|    <Location "/"> | ||||
|       # similar to nginx 'client_max_body_size 100M;' | ||||
|       LimitRequestBody 104857600 | ||||
| 
 | ||||
|       ProxyPass ${funkwhale-api}/ | ||||
|       ProxyPassReverse ${funkwhale-api}/ | ||||
|    </Location> | ||||
| 
 | ||||
| And add some more `ProxyPass` directives so that the `Alias` part of your configuration file looks this way:: | ||||
| 
 | ||||
|    ProxyPass "/front" "!" | ||||
|    Alias /front /srv/funkwhale/front/dist | ||||
| 
 | ||||
|    ProxyPass "/media" "!" | ||||
|    Alias /media /srv/funkwhale/data/media | ||||
| 
 | ||||
|    ProxyPass "/staticfiles" "!" | ||||
|    Alias /staticfiles /srv/funkwhale/data/static | ||||
| 
 | ||||
| In case you are using custom css and theming, you also need to match this block:: | ||||
| 
 | ||||
|    ProxyPass "/settings.json" "!" | ||||
|    Alias /settings.json /srv/funkwhale/custom/settings.json | ||||
| 
 | ||||
|    ProxyPass "/custom" "!" | ||||
|    Alias /custom /srv/funkwhale/custom | ||||
| 
 | ||||
|  |  | |||
|  | @ -46,10 +46,6 @@ Define MUSIC_DIRECTORY_PATH /srv/funkwhale/data/music | |||
|    # Tell the api that the client is using https | ||||
|    RequestHeader set X-Forwarded-Proto "https" | ||||
| 
 | ||||
|    DocumentRoot /srv/funkwhale/front/dist | ||||
| 
 | ||||
|    FallbackResource /index.html | ||||
| 
 | ||||
|    # Configure Proxy settings | ||||
|    # ProxyPreserveHost pass the original Host header to the backend server | ||||
|    ProxyVia On | ||||
|  | @ -69,14 +65,14 @@ Define MUSIC_DIRECTORY_PATH /srv/funkwhale/data/music | |||
|    </Proxy> | ||||
| 
 | ||||
|    # Activating WebSockets | ||||
|    ProxyPass "/api/v1/activity"  ${funkwhale-api-ws}/api/v1/activity | ||||
|    ProxyPass "/api/v1/activity" ${funkwhale-api-ws}/api/v1/activity | ||||
| 
 | ||||
|    <Location "/api"> | ||||
|    <Location "/"> | ||||
|       # similar to nginx 'client_max_body_size 100M;' | ||||
|       LimitRequestBody 104857600 | ||||
| 
 | ||||
|       ProxyPass ${funkwhale-api}/api | ||||
|       ProxyPassReverse ${funkwhale-api}/api | ||||
|       ProxyPass ${funkwhale-api}/ | ||||
|       ProxyPassReverse ${funkwhale-api}/ | ||||
|    </Location> | ||||
|    <Location "/federation"> | ||||
|       ProxyPass ${funkwhale-api}/federation | ||||
|  | @ -94,8 +90,13 @@ Define MUSIC_DIRECTORY_PATH /srv/funkwhale/data/music | |||
|       ProxyPassReverse ${funkwhale-api}/.well-known/ | ||||
|    </Location> | ||||
| 
 | ||||
|    ProxyPass "/front" "!" | ||||
|    Alias /front /srv/funkwhale/front/dist | ||||
| 
 | ||||
|    ProxyPass "/media" "!" | ||||
|    Alias /media /srv/funkwhale/data/media | ||||
| 
 | ||||
|    ProxyPass "/staticfiles" "!" | ||||
|    Alias /staticfiles /srv/funkwhale/data/static | ||||
| 
 | ||||
|    # Setting appropriate access levels to serve frontend | ||||
|  |  | |||
|  | @ -62,6 +62,37 @@ easy: | |||
| 
 | ||||
|     This is a warning, not an error, and it can be safely ignored. | ||||
|     Never run the ``makemigrations`` command yourself. | ||||
|      | ||||
| Upgrading the Postgres container | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| With some Funkwhale releases, it is recommended to upgrade the version of the | ||||
| Postgres database server container. For example, Funkwhale 0.17 recommended | ||||
| Postgres 9.4, but Funkwhale 0.18 recommends Postgres 11. When upgrading | ||||
| Postgres, it is not sufficient to change the container referenced in | ||||
| ``docker-compose.yml``. New major versions of Postgres cannot read the databases | ||||
| created by older major versions. The data has to be exported from a running | ||||
| instance of the old version and imported by the new version. | ||||
| 
 | ||||
| Thankfully, there is a Docker container available to automate this process. You | ||||
| can use the following snippet to upgrade your database in ``./postgres``, | ||||
| keeping a backup of the old version in ``./postgres-old``: | ||||
| 
 | ||||
| .. parsed-literal:: | ||||
| 
 | ||||
|     # Replace "9.4" and "11" with the versions you are migrating between. | ||||
|     export OLD_POSTGRES=9.4 | ||||
|     export NEW_POSTGRES=11 | ||||
|     docker-compose stop postgres | ||||
|     docker run --rm \ | ||||
|       -v `pwd`/data/postgres:/var/lib/postgresql/${OLD_POSTGRES}/data \ | ||||
|       -v `pwd`/data/postgres-new:/var/lib/postgresql/${NEW_POSTGRES}/data \ | ||||
|       tianon/postgres-upgrade:${OLD_POSTGRES}-to-${NEW_POSTGRES} | ||||
|     # Add back the access control rule that doesn't survive the upgrade | ||||
|     echo "host all all all trust" | sudo tee -a ./postgres-new/pg_hba.conf | ||||
|     # Swap over to the new database | ||||
|     mv ./data/postgres ./data/postgres-old | ||||
|     mv ./data/postgres-new ./data/postgres | ||||
| 
 | ||||
| 
 | ||||
| Non-docker setup | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
|   <div class="ui pagination menu" role="navigation" :aria-label="labels.pagination"> | ||||
|   <div v-if='maxPage > 1' class="ui pagination menu" role="navigation" :aria-label="labels.pagination"> | ||||
|     <a href | ||||
|       :disabled="current - 1 < 1" | ||||
|       @click.prevent.stop="selectPage(current - 1)" | ||||
|  | @ -13,7 +13,7 @@ | |||
|         {{ page }} | ||||
|       </a href> | ||||
|       <div v-else class="disabled item"> | ||||
|         ... | ||||
|         … | ||||
|       </div> | ||||
|     </template> | ||||
|     <a href | ||||
|  |  | |||
|  | @ -67,7 +67,7 @@ export default { | |||
|     }, | ||||
|     title () { | ||||
|       if (this.playable) { | ||||
|         return this.$gettext('Play now') | ||||
|         return this.$gettext('Play...') | ||||
|       } else { | ||||
|         if (this.track) { | ||||
|           return this.$gettext('This track is not available in any library you have access to') | ||||
|  |  | |||
|  | @ -65,7 +65,7 @@ | |||
|           <translate>We cannot load this track</translate> | ||||
|         </div> | ||||
|         <p v-if="hasNext && playing && $store.state.player.errorCount < $store.state.player.maxConsecutiveErrors"> | ||||
|           <translate>The next track will play automatically in a few seconds...</translate> | ||||
|           <translate>The next track will play automatically in a few seconds…</translate> | ||||
|           <i class="loading spinner icon"></i> | ||||
|         </p> | ||||
|         <p> | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ | |||
|           <img class="ui big circular image" v-else v-lazy="$store.getters['instance/absoluteUrl'](profile.avatar.square_crop)" /> | ||||
|           <div class="content"> | ||||
|             {{ profile.username }} | ||||
|             <div class="sub header" v-translate="{date: signupDate}">Registered since %{ date }</div> | ||||
|             <div class="sub header" v-translate="{date: signupDate}">Member since %{ date }</div> | ||||
|           </div> | ||||
|         </h2> | ||||
|         <div class="ui basic green label"> | ||||
|  |  | |||
|  | @ -68,8 +68,7 @@ | |||
|           <translate>Change my password</translate> | ||||
|         </h2> | ||||
|         <div class="ui message"> | ||||
|           <translate>Changing your password will also change your Subsonic API password if you have requested one.</translate> | ||||
|           <translate>You will have to update your password on your clients that use this password.</translate> | ||||
|           <translate>Changing your password will also change your Subsonic API password if you have requested one.</translate> <translate>You will have to update your password on your clients that use this password.</translate> | ||||
|         </div> | ||||
|         <form class="ui form" @submit.prevent="submitPassword()"> | ||||
|           <div v-if="passwordError" class="ui negative message"> | ||||
|  |  | |||
|  | @ -5,8 +5,7 @@ | |||
|       <translate>The Subsonic API is not available on this Funkwhale instance.</translate> | ||||
|     </p> | ||||
|     <p> | ||||
|       <translate>Funkwhale is compatible with other music players that support the Subsonic API.</translate> | ||||
|       <translate>You can use those to enjoy your playlist and music in offline mode, on your smartphone or tablet, for instance.</translate> | ||||
|       <translate>Funkwhale is compatible with other music players that support the Subsonic API.</translate> <translate>You can use those to enjoy your playlist and music in offline mode, on your smartphone or tablet, for instance.</translate> | ||||
|     </p> | ||||
|     <p> | ||||
|       <translate>However, accessing Funkwhale from those clients require a separate password you can set below.</translate> | ||||
|  |  | |||
|  | @ -168,7 +168,7 @@ export default { | |||
|   computed: { | ||||
|     labels () { | ||||
|       return { | ||||
|         searchPlaceholder: this.$gettext('Search by domain, username, bio...') | ||||
|         searchPlaceholder: this.$gettext('Search by domain, username, bio…') | ||||
|       } | ||||
|     }, | ||||
|     actionFilters () { | ||||
|  |  | |||
|  | @ -148,7 +148,7 @@ export default { | |||
|   computed: { | ||||
|     labels () { | ||||
|       return { | ||||
|         searchPlaceholder: this.$gettext('Search by name...') | ||||
|         searchPlaceholder: this.$gettext('Search by name…') | ||||
|       } | ||||
|     }, | ||||
|     actionFilters () { | ||||
|  |  | |||
|  | @ -22,11 +22,11 @@ | |||
|       <div v-else class="ui list"> | ||||
|         <div class="ui item" v-if="object.silence_activity"> | ||||
|           <i class="feed icon"></i> | ||||
|           <div class="content"><translate>Silence activity</translate></div> | ||||
|           <div class="content"><translate>Mute activity</translate></div> | ||||
|         </div> | ||||
|         <div class="ui item" v-if="object.silence_notifications"> | ||||
|           <i class="bell icon"></i> | ||||
|           <div class="content"><translate>Silence notifications</translate></div> | ||||
|           <div class="content"><translate>Mute notifications</translate></div> | ||||
|         </div> | ||||
|         <div class="ui item" v-if="object.reject_media"> | ||||
|           <i class="file icon"></i> | ||||
|  |  | |||
|  | @ -112,7 +112,7 @@ export default { | |||
|         blockAllHelp: this.$gettext("Block everything from this account or domain. This will prevent any interaction with the entity, and purge related content (uploads, libraries, follows, etc.)"), | ||||
|         silenceActivity: { | ||||
|           help: this.$gettext("Hide account or domain content, except from followers."), | ||||
|           label: this.$gettext("Silence activity"), | ||||
|           label: this.$gettext("Mute activity"), | ||||
|         }, | ||||
|         silenceNotifications: { | ||||
|           help: this.$gettext("Prevent account or domain from triggering notifications, except from followers."), | ||||
|  |  | |||
|  | @ -38,9 +38,11 @@ export default { | |||
|     labels () { | ||||
|       let libraryFollowMessage = this.$gettext('%{ username } followed your library "%{ library }"') | ||||
|       let libraryAcceptFollowMessage = this.$gettext('%{ username } accepted your follow on library "%{ library }"') | ||||
|       let libraryPendingFollowMessage = this.$gettext('%{ username } wants to follow your library "%{ library }"') | ||||
|       return { | ||||
|         libraryFollowMessage, | ||||
|         libraryAcceptFollowMessage, | ||||
| 				libraryPendingFollowMessage, | ||||
|         markRead: this.$gettext('Mark as read'), | ||||
|         markUnread: this.$gettext('Mark as unread'), | ||||
| 
 | ||||
|  | @ -55,19 +57,23 @@ export default { | |||
|       if (a.type === 'Follow') { | ||||
|         if (a.object && a.object.type === 'music.Library') { | ||||
|           let action = null | ||||
| 					let message = null | ||||
|           if (!a.related_object.approved) { | ||||
| 						message = this.labels.libraryPendingFollowMessage | ||||
|             action = { | ||||
|               buttonClass: 'green', | ||||
|               icon: 'check', | ||||
|               label: this.$gettext('Approve'), | ||||
|               handler: () => { self.approveLibraryFollow(a.related_object) } | ||||
|             } | ||||
|           } | ||||
| 					} else { | ||||
| 						message = this.labels.libraryFollowMessage | ||||
| 					} | ||||
|           return { | ||||
|             action, | ||||
|             detailUrl: {name: 'content.libraries.detail', params: {id: a.object.uuid}}, | ||||
|             message: this.$gettextInterpolate( | ||||
|               this.labels.libraryFollowMessage, | ||||
|               message, | ||||
|               {username: this.username, library: a.object.name} | ||||
|             ) | ||||
|           } | ||||
|  |  | |||
|  | @ -160,7 +160,7 @@ export default { | |||
|     }), | ||||
|     labels () { | ||||
|       return { | ||||
|         copyTitle: this.$gettext('Copy tracks from current queue to playlist') | ||||
|         copyTitle: this.$gettext('Copy queued tracks to playlist') | ||||
|       } | ||||
|     }, | ||||
|     status () { | ||||
|  |  | |||
|  | @ -24,7 +24,7 @@ | |||
|           </tbody> | ||||
|         </table> | ||||
|         <p v-else> | ||||
|           <translate>No notifications yet.</translate> | ||||
|           <translate>No notification to show.</translate> | ||||
|         </p> | ||||
|       </div> | ||||
|     </section> | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| <template> | ||||
|   <div class="ui vertical aligned stripe segment"> | ||||
|     <div v-if="isLoading" :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']"> | ||||
|       <div class="ui text loader"><translate>Loading remote libraries...</translate></div> | ||||
|       <div class="ui text loader"><translate>Loading remote libraries…</translate></div> | ||||
|     </div> | ||||
|     <div v-else class="ui text container"> | ||||
|       <h1 class="ui header"><translate>Remote libraries</translate></h1> | ||||
|  |  | |||
		Ładowanie…
	
		Reference in New Issue
	
	 Eliot Berriot
						Eliot Berriot