diff --git a/changes/changelog.d/611.enhancement b/changes/changelog.d/611.enhancement new file mode 100644 index 0000000000000000000000000000000000000000..1c6121d67b3c8820d5f7892ddbfa0e1b80a7dff6 --- /dev/null +++ b/changes/changelog.d/611.enhancement @@ -0,0 +1 @@ +Documented keyboard shortcuts, list is now available by pressing "h" or in the footer (#611) diff --git a/changes/changelog.d/footer.enhancement b/changes/changelog.d/footer.enhancement new file mode 100644 index 0000000000000000000000000000000000000000..e1a83191859192cc897d05212d74ab636b48d2b4 --- /dev/null +++ b/changes/changelog.d/footer.enhancement @@ -0,0 +1 @@ +Restructured the footer, added useful links and removed unused content diff --git a/front/src/App.vue b/front/src/App.vue index 946a0621e25c8dc8459a3336bf48bdc703a58243..6a4bd746e5bf6bf8f5fae68033580cf3d2224505 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -25,64 +25,17 @@ <service-messages v-if="messages.length > 0" /> <router-view :key="$route.fullPath"></router-view> <div class="ui fitted divider"></div> - <div id="footer" class="ui vertical footer segment"> - <div class="ui container"> - <div class="ui stackable equal height stackable grid"> - <div class="three wide column"> - <h4 v-translate class="ui header">Links</h4> - <div class="ui link list"> - <router-link class="item" to="/about"> - <translate>About this instance</translate> - </router-link> - <a href="https://funkwhale.audio" class="item" target="_blank"><translate>Official website</translate></a> - <a href="https://docs.funkwhale.audio" class="item" target="_blank"><translate>Documentation</translate></a> - <a href="https://code.eliotberriot.com/funkwhale/funkwhale" class="item" target="_blank"> - <translate :translate-params="{version: version}" v-if="version">Source code (%{version})</translate> - <translate v-else>Source code</translate> - </a> - <a href="https://code.eliotberriot.com/funkwhale/funkwhale/issues" class="item" target="_blank"><translate>Issue tracker</translate></a> - <a @click="switchInstance" class="item" > - <translate>Use another instance</translate> - <template v-if="$store.state.instance.instanceUrl !== '/'"> - <br> - ({{ $store.state.instance.instanceUrl }}) - </template> - </a> - </div> - </div> - <div class="ten wide column"> - <h4 v-translate class="ui header">About Funkwhale</h4> - <p> - <translate>Funkwhale is a free and open-source project run by volunteers. You can help us improve the platform by reporting bugs, suggesting features and share the project with your friends!</translate> - </p> - <p> - <translate>The funkwhale logo was kindly designed and provided by Francis Gading.</translate> - </p> - </div> - <div class="three wide column"> - <h4 v-translate class="ui header">Options</h4> - <div class="ui form"> - <div class="ui field"> - <label><translate>Change language</translate></label> - <select class="ui dropdown" v-model="$language.current"> - <option v-for="(language, key) in $language.available" :value="key">{{ language }}</option> - </select> - </div> - </div> - <br> - <a target="_blank" href="https://translate.funkwhale.audio/engage/funkwhale/"> - <translate>Help us translate Funkwhale</translate> - </a> - </div> - - </div> - </div> - </div> + <app-footer :version="version" @show:shortcuts-modal="showShortcutsModal = !showShortcutsModal"></app-footer> <raven v-if="$store.state.instance.settings.raven.front_enabled.value" :dsn="$store.state.instance.settings.raven.front_dsn.value"> </raven> <playlist-modal v-if="$store.state.auth.authenticated"></playlist-modal> + <shortcuts-modal @update:show="showShortcutsModal = $event" :show="showShortcutsModal"></shortcuts-modal> + <GlobalEvents + @keydown.h.exact="showShortcutsModal = !showShortcutsModal" + /> + </template> </div> </template> @@ -92,29 +45,35 @@ import axios from 'axios' import _ from 'lodash' import {mapState} from 'vuex' import { WebSocketBridge } from 'django-channels' - +import GlobalEvents from '@/components/utils/global-events' import translations from '@/translations' import Sidebar from '@/components/Sidebar' +import AppFooter from '@/components/Footer' import Raven from '@/components/Raven' import ServiceMessages from '@/components/ServiceMessages' import PlaylistModal from '@/components/playlists/PlaylistModal' +import ShortcutsModal from '@/components/ShortcutsModal' export default { name: 'app', components: { Sidebar, + AppFooter, Raven, PlaylistModal, + ShortcutsModal, + GlobalEvents, ServiceMessages }, data () { return { bridge: null, nodeinfo: null, - instanceUrl: null + instanceUrl: null, + showShortcutsModal: false, } }, created () { @@ -410,4 +369,10 @@ button.reset { -webkit-appearance: none; text-align: inherit; } + +.ui.table > caption { + font-weight: bold; + padding: 0.5em; + text-align: left; +} </style> diff --git a/front/src/components/Footer.vue b/front/src/components/Footer.vue new file mode 100644 index 0000000000000000000000000000000000000000..127f8be74a9ef27f361a0d393b0c5049126443ad --- /dev/null +++ b/front/src/components/Footer.vue @@ -0,0 +1,97 @@ +<template> + <footer id="footer" class="ui vertical footer segment"> + <div class="ui container"> + <div class="ui stackable equal height stackable grid"> + <div class="four wide column"> + <h4 v-translate class="ui header"> + <translate :translate-params="{instanceName: instanceHostname}" >About %{instanceName}</translate> + </h4> + <div class="ui link list"> + <router-link class="item" to="/about"> + <translate>About page</translate> + </router-link> + <div class="item" v-if="version"> + <translate :translate-params="{version: version}" >Version %{version}</translate> + </div> + <a @click="switchInstance" class="item" > + <translate>Use another instance</translate> + </a> + </div> + <div class="ui form"> + <div class="ui field"> + <label><translate>Change language</translate></label> + <select class="ui dropdown" v-model="$language.current"> + <option v-for="(language, key) in $language.available" :key="key" :value="key">{{ language }}</option> + </select> + </div> + </div> + </div> + <div class="four wide column"> + <h4 v-translate class="ui header">Using Funkwhale</h4> + <div class="ui link list"> + <a href="https://docs.funkwhale.audio" class="item" target="_blank"><translate>Documentation</translate></a> + <a href="https://docs.funkwhale.audio/users/apps.html" class="item" target="_blank"><translate>Mobile and desktop apps</translate></a> + <div role="button" class="item" @click="$emit('show:shortcuts-modal')"><translate>Keyboard shortcuts</translate></div> + </div> + </div> + <div class="four wide column"> + <h4 v-translate class="ui header">Getting help</h4> + <div class="ui link list"> + <a href="https://socialhub.network/c/projects/funkwhale" class="item" target="_blank"><translate>Support forum</translate></a> + <a href="https://riot.im/app/#/room/#funkwhale-troubleshooting:matrix.org" class="item" target="_blank"><translate>Chat room</translate></a> + <a href="https://code.eliotberriot.com/funkwhale/funkwhale/issues" class="item" target="_blank"><translate>Issue tracker</translate></a> + </div> + </div> + <div class="four wide column"> + <h4 v-translate class="ui header">About Funkwhale</h4> + <div class="ui link list"> + <a href="https://funkwhale.audio" class="item" target="_blank"><translate>Official website</translate></a> + <a href="https://contribute.funkwhale.audio" class="item" target="_blank"><translate>Contribute</translate></a> + <a href="https://code.eliotberriot.com/funkwhale/funkwhale" class="item" target="_blank"><translate>Source code</translate></a> + </div> + <div class="ui hidden divider"></div> + <p> + <translate>The funkwhale logo was kindly designed and provided by Francis Gading.</translate> + </p> + </div> + </div> + </div> + </footer> +</template> + +<script> +import {mapState} from 'vuex' + + +export default { + props: ['version'], + methods: { + switchInstance () { + let confirm = window.confirm(this.$gettext('This will erase your local data and disconnect you, do you want to continue?')) + if (confirm) { + this.$store.commit('instance/instanceUrl', null) + } + }, + }, + computed: { + ...mapState({ + messages: state => state.ui.messages + }), + instanceHostname () { + let url = this.$store.state.instance.instanceUrl + let parser = document.createElement('a'); + parser.href = url + return parser.hostname + }, + suggestedInstances () { + let instances = [this.$store.getters['instance/defaultUrl'](), 'https://demo.funkwhale.audio'] + return instances + }, + } +} +</script> +<style scoped> + footer p { + color: grey; + } +</style> diff --git a/front/src/components/ShortcutsModal.vue b/front/src/components/ShortcutsModal.vue new file mode 100644 index 0000000000000000000000000000000000000000..2338658dd7b80a9d67c0e424f22b7b7f827270b1 --- /dev/null +++ b/front/src/components/ShortcutsModal.vue @@ -0,0 +1,95 @@ +<template> + <modal @update:show="$emit('update:show', $event)" :show="show"> + <header class="header"> + <translate>Keyboard shortcuts</translate> + </header> + <section class="scrolling content"> + <table + class="ui compact collapsing basic fixed single line table" + v-for="section in sections" + :key="section.title"> + <caption>{{ section.title }}</caption> + <tbody> + <tr v-for="shortcut in section.shortcuts" :key="shortcut.summary"> + <td>{{ shortcut.summary }}</td> + <td><span class="ui label">{{ shortcut.key }}</span></td> + </tr> + </tbody> + </table> + </section> + <footer class="actions"> + <div class="ui cancel button"><translate>Close</translate></div> + </footer> + </modal> +</template> + +<script> +import Modal from '@/components/semantic/Modal' + +export default { + props: ['show'], + components: { + Modal, + }, + computed: { + sections () { + return [ + { + title: this.$gettext('General shortcuts'), + shortcuts: [ + { + key: 'h', + summary: this.$gettext('Show available keyboard shortcuts') + } + ] + }, + // space.prevent.exact="togglePlay" + // ctrl.left.prevent.exact="previous" + // ctrl.right.prevent.exact="next" + // ctrl.down.prevent.exact="$store.commit('player/incrementVolume', -0.1)" + // ctrl.up.prevent.exact="$store.commit('player/incrementVolume', 0.1)" + // l.prevent.exact="$store.commit('player/toggleLooping')" + // s.prevent.exact="shuffle" + + { + title: this.$gettext('Audio player shortcuts'), + shortcuts: [ + { + key: 'space', + summary: this.$gettext('Pause/play the current track') + }, + { + key: 'ctrl left', + summary: this.$gettext('Play previous track') + }, + { + key: 'ctrl right', + summary: this.$gettext('Play next track') + }, + { + key: 'ctrl up', + summary: this.$gettext('Increase volume') + }, + { + key: 'ctrl down', + summary: this.$gettext('Decrease volume') + }, + { + key: 'l', + summary: this.$gettext('Toggle queue looping') + }, + { + key: 's', + summary: this.$gettext('Shuffle queue') + }, + ] + } + ] + } + } +} +</script> + +<!-- Add "scoped" attribute to limit CSS to this component only --> +<style scoped> +</style>