Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • 1.4.1-upgrade-release
  • 1121-download
  • 1218-smartplaylist_backend
  • 1373-login-form-move-reset-your-password-link
  • 1381-progress-bars
  • 1481
  • 1518-update-django-allauth
  • 1645
  • 1675-widget-improperly-configured-missing-resource-id
  • 1675-widget-improperly-configured-missing-resource-id-2
  • 1704-required-props-are-not-always-passed
  • 1716-add-frontend-tests-again
  • 1749-smtp-uri-configuration
  • 1930-first-upload-in-a-batch-always-fails
  • 1976-update-documentation-links-in-readme-files
  • 2054-player-layout
  • 2063-funkwhale-connection-interrupted-every-so-often-requires-network-reset-page-refresh
  • 2091-iii-6-improve-visuals-layout
  • 2151-refused-to-load-spa-manifest-json-2
  • 2154-add-to-playlist-pop-up-hidden-by-now-playing-screen
  • 2155-can-t-see-the-episode-list-of-a-podcast-as-an-anonymous-user-with-anonymous-access-enabled
  • 2156-add-management-command-to-change-file-ref-for-in-place-imported-files-to-s3
  • 2192-clear-queue-bug-when-random-shuffle-is-enabled
  • 2205-channel-page-pagination-link-dont-working
  • 2215-custom-logger-does-not-work-at-all-with-webkit-and-blink-based-browsers
  • 2228-troi-real-world-review
  • 2274-implement-new-upload-api
  • 2303-allow-users-to-own-tagged-items
  • 2395-far-right-filter
  • 2405-front-buttont-trigger-third-party-hook
  • 2408-troi-create-missing-tracks
  • 2416-revert-library-drop
  • 2422-trigger-libraries-follow-on-user-follow
  • 2429-fix-popover-auto-close
  • 2448-complete-tags
  • 2452-fetch-third-party-metadata
  • 2469-Fix-search-bar-in-ManageUploads
  • 2476-deep-upload-links
  • 2490-experiment-use-rstore
  • 2490-experimental-use-simple-data-store
  • 2490-fix-search-modal
  • 2490-search-modal
  • 2501-fix-compatibility-with-older-browsers
  • 2502-drop-uno-and-jquery
  • 2533-allow-followers-in-user-activiy-privacy-level
  • 2539-drop-ansible-installation-method-in-favor-of-docker
  • 2560-default-modal-width
  • 623-test
  • 653-enable-starting-embedded-player-at-a-specific-position-in-track
  • activitypub-overview
  • album-sliders
  • arne/2091-improve-visuals
  • back-option-for-edits
  • chore/2406-compose-modularity-scope
  • develop
  • develop-password-reset
  • env-file-cleanup
  • feat/2091-improve-visuals
  • feature/2481-vui-translations
  • fix-amd64-docker-build-gfortran
  • fix-front-node-version
  • fix-gitpod
  • fix-plugins-dev-setup
  • fix-rate-limit-serializer
  • fix-schema-channel-metadata-choices
  • flupsi/2803-improve-visuals
  • flupsi/2804-new-upload-process
  • funkwhale-fix_pwa_manifest
  • funkwhale-petitminion-2136-bug-fix-prune-skipped-upload
  • funkwhale-ui-buttons
  • georg/add-typescript
  • gitpod/test-1866
  • global-button-experiment
  • global-buttons
  • juniorjpdj/pkg-repo
  • manage-py-reference
  • merge-review
  • minimal-python-version
  • petitminion-develop-patch-84496
  • pin-mutagen-to-1.46
  • pipenv
  • plugins
  • plugins-v2
  • plugins-v3
  • pre-release/1.3.0
  • prune_skipped_uploads_docs
  • refactor/homepage
  • renovate/front-all-dependencies
  • renovate/front-major-all-dependencies
  • schema-updates
  • small-gitpod-improvements
  • spectacular_schema
  • stable
  • tempArne
  • ui-buttons
  • update-frontend-dependencies
  • upload-process-spec
  • user-concept-docs
  • v2-artists
  • vite-ws-ssl-compatible
  • 0.1
  • 0.10
  • 0.11
  • 0.12
  • 0.13
  • 0.14
  • 0.14.1
  • 0.14.2
  • 0.15
  • 0.16
  • 0.16.1
  • 0.16.2
  • 0.16.3
  • 0.17
  • 0.18
  • 0.18.1
  • 0.18.2
  • 0.18.3
  • 0.19.0
  • 0.19.0-rc1
  • 0.19.0-rc2
  • 0.19.1
  • 0.2
  • 0.2.1
  • 0.2.2
  • 0.2.3
  • 0.2.4
  • 0.2.5
  • 0.2.6
  • 0.20.0
  • 0.20.0-rc1
  • 0.20.1
  • 0.21
  • 0.21-rc1
  • 0.21-rc2
  • 0.21.1
  • 0.21.2
  • 0.3
  • 0.3.1
  • 0.3.2
  • 0.3.3
  • 0.3.4
  • 0.3.5
  • 0.4
  • 0.5
  • 0.5.1
  • 0.5.2
  • 0.5.3
  • 0.5.4
  • 0.6
  • 0.6.1
  • 0.7
  • 0.8
  • 0.9
  • 0.9.1
  • 1.0
  • 1.0-rc1
  • 1.0.1
  • 1.1
  • 1.1-rc1
  • 1.1-rc2
  • 1.1.1
  • 1.1.2
  • 1.1.3
  • 1.1.4
  • 1.2.0
  • 1.2.0-rc1
  • 1.2.0-rc2
  • 1.2.0-testing
  • 1.2.0-testing2
  • 1.2.0-testing3
  • 1.2.0-testing4
  • 1.2.1
  • 1.2.10
  • 1.2.2
  • 1.2.3
  • 1.2.4
  • 1.2.5
  • 1.2.6
  • 1.2.6-1
  • 1.2.7
  • 1.2.8
  • 1.2.9
  • 1.3.0
  • 1.3.0-rc1
  • 1.3.0-rc2
  • 1.3.0-rc3
  • 1.3.0-rc4
  • 1.3.0-rc5
  • 1.3.0-rc6
  • 1.3.1
  • 1.3.2
  • 1.3.3
  • 1.3.4
  • 1.4.0
  • 1.4.0-rc1
  • 1.4.0-rc2
  • 1.4.1
  • 2.0.0-alpha.1
  • 2.0.0-alpha.2
200 results

Target

Select target project
  • funkwhale/funkwhale
  • Luclu7/funkwhale
  • mbothorel/funkwhale
  • EorlBruder/funkwhale
  • tcit/funkwhale
  • JocelynDelalande/funkwhale
  • eneiluj/funkwhale
  • reg/funkwhale
  • ButterflyOfFire/funkwhale
  • m4sk1n/funkwhale
  • wxcafe/funkwhale
  • andybalaam/funkwhale
  • jcgruenhage/funkwhale
  • pblayo/funkwhale
  • joshuaboniface/funkwhale
  • n3ddy/funkwhale
  • gegeweb/funkwhale
  • tohojo/funkwhale
  • emillumine/funkwhale
  • Te-k/funkwhale
  • asaintgenis/funkwhale
  • anoadragon453/funkwhale
  • Sakada/funkwhale
  • ilianaw/funkwhale
  • l4p1n/funkwhale
  • pnizet/funkwhale
  • dante383/funkwhale
  • interfect/funkwhale
  • akhardya/funkwhale
  • svfusion/funkwhale
  • noplanman/funkwhale
  • nykopol/funkwhale
  • roipoussiere/funkwhale
  • Von/funkwhale
  • aurieh/funkwhale
  • icaria36/funkwhale
  • floreal/funkwhale
  • paulwalko/funkwhale
  • comradekingu/funkwhale
  • FurryJulie/funkwhale
  • Legolars99/funkwhale
  • Vierkantor/funkwhale
  • zachhats/funkwhale
  • heyjake/funkwhale
  • sn0w/funkwhale
  • jvoisin/funkwhale
  • gordon/funkwhale
  • Alexander/funkwhale
  • bignose/funkwhale
  • qasim.ali/funkwhale
  • fakegit/funkwhale
  • Kxze/funkwhale
  • stenstad/funkwhale
  • creak/funkwhale
  • Kaze/funkwhale
  • Tixie/funkwhale
  • IISergII/funkwhale
  • lfuelling/funkwhale
  • nhaddag/funkwhale
  • yoasif/funkwhale
  • ifischer/funkwhale
  • keslerm/funkwhale
  • flupe/funkwhale
  • petitminion/funkwhale
  • ariasuni/funkwhale
  • ollie/funkwhale
  • ngaumont/funkwhale
  • techknowlogick/funkwhale
  • Shleeble/funkwhale
  • theflyingfrog/funkwhale
  • jonatron/funkwhale
  • neobrain/funkwhale
  • eorn/funkwhale
  • KokaKiwi/funkwhale
  • u1-liquid/funkwhale
  • marzzzello/funkwhale
  • sirenwatcher/funkwhale
  • newer027/funkwhale
  • codl/funkwhale
  • Zwordi/funkwhale
  • gisforgabriel/funkwhale
  • iuriatan/funkwhale
  • simon/funkwhale
  • bheesham/funkwhale
  • zeoses/funkwhale
  • accraze/funkwhale
  • meliurwen/funkwhale
  • divadsn/funkwhale
  • Etua/funkwhale
  • sdrik/funkwhale
  • Soran/funkwhale
  • kuba-orlik/funkwhale
  • cristianvogel/funkwhale
  • Forceu/funkwhale
  • jeff/funkwhale
  • der_scheibenhacker/funkwhale
  • owlnical/funkwhale
  • jovuit/funkwhale
  • SilverFox15/funkwhale
  • phw/funkwhale
  • mayhem/funkwhale
  • sridhar/funkwhale
  • stromlin/funkwhale
  • rrrnld/funkwhale
  • nitaibezerra/funkwhale
  • jaller94/funkwhale
  • pcouy/funkwhale
  • eduxstad/funkwhale
  • codingHahn/funkwhale
  • captain/funkwhale
  • polyedre/funkwhale
  • leishenailong/funkwhale
  • ccritter/funkwhale
  • lnceballosz/funkwhale
  • fpiesche/funkwhale
  • Fanyx/funkwhale
  • markusblogde/funkwhale
  • Firobe/funkwhale
  • devilcius/funkwhale
  • freaktechnik/funkwhale
  • blopware/funkwhale
  • cone/funkwhale
  • thanksd/funkwhale
  • vachan-maker/funkwhale
  • bbenti/funkwhale
  • tarator/funkwhale
  • prplecake/funkwhale
  • DMarzal/funkwhale
  • lullis/funkwhale
  • hanacgr/funkwhale
  • albjeremias/funkwhale
  • xeruf/funkwhale
  • llelite/funkwhale
  • RoiArthurB/funkwhale
  • cloo/funkwhale
  • nztvar/funkwhale
  • Keunes/funkwhale
  • petitminion/funkwhale-petitminion
  • m-idler/funkwhale
  • SkyLeite/funkwhale
140 results
Select Git revision
  • 1121-download
  • 1218-smartplaylist_backend
  • 1288-user-me-can-be-created-but-cannot-be-edited
  • 1381-progress-bars
  • 1392-update-actor-cache-task
  • 1434-update-pyld
  • 1481
  • 1515-update-click
  • 1518-update-django-allauth
  • 1645
  • 1674-recently-added-radio-repair
  • 1711-ping_remote_instance
  • 1714-resolve-timeouts-on-domain-nodeinfo-fetch
  • 1717-stop-player-when-stop-radio
  • 1798-bulk-fetch-actor-data
  • 2010-fix_i18n_globally
  • 2136-bug-fix-prune-skipped-upload
  • 2275-quality-filter-backend
  • 2322-troi-frontend
  • 623-test
  • 639-ThirdPartyStream-poc
  • 653-enable-starting-embedded-player-at-a-specific-position-in-track
  • 762-domain_follow
  • album-sliders
  • back-option-for-edits
  • develop
  • feat/2091-improve-visuals
  • funkwhale-activityPub-overview
  • generate-swagger
  • master
  • pipenv
  • plugins
  • plugins-v2
  • plugins-v3
  • poetry
  • renovate/configure
  • spec-domain-follow
  • spec_test_for_issue_1
  • stable
  • test_typesens
  • testbranch
  • troi-recommendation-system-with-typenses
  • update-boto3
  • update-frontend-dependencies
  • update-uvicorn
  • 0.1
  • 0.10
  • 0.11
  • 0.12
  • 0.13
  • 0.14
  • 0.14.1
  • 0.14.2
  • 0.15
  • 0.16
  • 0.16.1
  • 0.16.2
  • 0.16.3
  • 0.17
  • 0.18
  • 0.18.1
  • 0.18.2
  • 0.18.3
  • 0.19.0
  • 0.19.0-rc1
  • 0.19.0-rc2
  • 0.19.1
  • 0.2
  • 0.2.1
  • 0.2.2
  • 0.2.3
  • 0.2.4
  • 0.2.5
  • 0.2.6
  • 0.20.0
  • 0.20.0-rc1
  • 0.20.1
  • 0.21
  • 0.21-rc1
  • 0.21-rc2
  • 0.21.1
  • 0.21.2
  • 0.3
  • 0.3.1
  • 0.3.2
  • 0.3.3
  • 0.3.4
  • 0.3.5
  • 0.4
  • 0.5
  • 0.5.1
  • 0.5.2
  • 0.5.3
  • 0.5.4
  • 0.6
  • 0.6.1
  • 0.7
  • 0.8
  • 0.9
  • 0.9.1
  • 1.0
  • 1.0-rc1
  • 1.0.1
  • 1.1
  • 1.1-rc1
  • 1.1-rc2
  • 1.1.1
  • 1.1.2
  • 1.1.3
  • 1.1.4
  • 1.2.0
  • 1.2.0-rc1
  • 1.2.0-rc2
  • 1.2.0-testing
  • 1.2.0-testing2
  • 1.2.0-testing3
  • 1.2.0-testing4
  • 1.2.1
118 results
Show changes
Showing
with 852 additions and 2925 deletions
...@@ -6,17 +6,17 @@ authors = ["Funkwhale Collective <contact@funkwhale.audio>"] ...@@ -6,17 +6,17 @@ authors = ["Funkwhale Collective <contact@funkwhale.audio>"]
license = "AGPLv3" license = "AGPLv3"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.9" python = "^3.11"
sphinx = "7.2.6" sphinx = "8.2.3"
sphinx_design = "0.5.0" sphinx_design = "0.6.1"
sphinx-copybutton = "==0.5.2" sphinx-copybutton = "==0.5.2"
sphinx-intl = "2.1.0" sphinx-intl = "2.1.0"
sphinx-rtd-theme = "==2.0.0" sphinx-rtd-theme = "==3.0.2"
sphinxcontrib-mermaid = "0.9.2" sphinxcontrib-mermaid = "1.0.0"
myst-parser = "2.0.0" myst-parser = "4.0.1"
django-environ = "==0.11.2" django-environ = "==0.11.2"
django = "==3.2.23" django = "==5.2.1"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
## Issue ## Issue
We now have playlist, use complained about library not being clearly defined. We now have playlists for sorting music and sharing, users complained about library not being clearly defined.
## Proposed solution ## Proposed solution
...@@ -14,7 +14,7 @@ A new endpoint to move upload from one library to another : `PATCH` on `api/v2/u ...@@ -14,7 +14,7 @@ A new endpoint to move upload from one library to another : `PATCH` on `api/v2/u
New `description` field on playlist, to inherit from the `description` field of Library New `description` field on playlist, to inherit from the `description` field of Library
Library Follows will be transformed to user follow. Library Follows will be transformed to user follows.
The schedule_scan function of the library still exist and allow federation of audio content The schedule_scan function of the library still exist and allow federation of audio content
During user creation, built-in libraries are generated automatically by `create_user_libraries` During user creation, built-in libraries are generated automatically by `create_user_libraries`
openapi: "3.0.3"
info:
description: "Interactive documentation for [Funkwhale](https://funkwhale.audio) API."
version: "2.0.0"
title: "Funkwhale API"
servers:
- url: "https://demo.funkwhale.audio"
description: "Demo server"
- url: "https://open.audio"
description: "Real server with real content"
- url: "https://{domain}"
description: "Custom server"
variables:
domain:
default: yourdomain
description: "Your Funkwhale Domain"
protocol:
enum:
- "http"
- "https"
default: "https"
tags:
- name: Instance
description: Information about the server
- name: Content
description: Information about content on the server
paths:
/api/v2/instance/nodeinfo/2.1:
get:
tags:
- Instance
summary: Retrieve nodeinfo data
description: Retrieve details about a Funkwhale server using the Nodeinfo standard
operationId: getNodeinfo
responses:
"200":
description: Successful operation
content:
application/json:
schema:
$ref: "#/components/schemas/Nodeinfo"
application/xml:
schema:
$ref: "#/components/schemas/Nodeinfo"
"401":
$ref: "#/components/responses/Unauthorized"
/api/v2/tags/podcasts:
get:
tags:
- Content
summary: Retrieve podcast categories
description: Retrieve a list of podcast categories and the number of uploads tagged with those categories
operationId: getTagsPodcasts
parameters:
- name: q
in: query
required: false
description: A free text field to filter category names
schema:
type: string
- name: page
in: query
required: false
description: The number of the result page you want to return
schema:
type: number
- name: page_size
in: query
required: false
description: The number of results to return on each page. Defaults to 50.
schema:
type: number
- name: ordering
in: query
required: false
description: |
The order in which results are presented. Preface with `-` to return items in descending order.
schema:
type: string
enum:
- "name"
- "creation_date"
- "tagged_items"
- "-name"
- "-creation_date"
- "-tagged_items"
responses:
"200":
description: Successful operation
content:
application/json:
schema:
$ref: "#/components/schemas/Categories"
application/xml:
schema:
$ref: "#/components/schemas/Categories"
"401":
$ref: "#/components/responses/Unauthorized"
/api/v2/tags/podcasts/{category}:
get:
tags:
- Content
summary: Retrieve podcast categories
description: Retrieve a list of podcast categories and the number of uploads tagged with those categories
operationId: getTagPodcasts
parameters:
- name: category
in: path
required: true
description: The category you want to return information about
schema:
type: string
responses:
"200":
description: Successful operation
content:
application/json:
schema:
$ref: "#/components/schemas/Category"
application/xml:
schema:
$ref: "#/components/schemas/Category"
"401":
$ref: "#/components/responses/Unauthorized"
/api/v2/tags/music:
get:
tags:
- Content
summary: Retrieve music genres
description: Retrieve a list of music genres and the number of uploads tagged with those categories
operationId: getTagsMusic
parameters:
- name: q
in: query
required: false
description: A free text field to filter genre names
schema:
type: string
- name: page
in: query
required: false
description: The number of the result page you want to return
schema:
type: number
- name: page_size
in: query
required: false
description: The number of results to return on each page. Defaults to 50.
schema:
type: number
- name: ordering
in: query
required: false
description: |
The order in which results are presented. Preface with `-` to return items in descending order.
schema:
type: string
enum:
- "name"
- "creation_date"
- "tagged_items"
- "-name"
- "-creation_date"
- "-tagged_items"
responses:
"200":
description: Successful operation
content:
application/json:
schema:
$ref: "#/components/schemas/Genres"
application/xml:
schema:
$ref: "#/components/schemas/Genres"
"401":
$ref: "#/components/responses/Unauthorized"
/api/v2/tags/music/{genre}:
get:
tags:
- Content
summary: Retrieve podcast categories
description: Retrieve a list of podcast categories and the number of uploads tagged with those categories
operationId: getTagMusic
parameters:
- name: genre
in: path
required: true
description: The genre you want to return information about
schema:
type: string
responses:
"200":
description: Successful operation
content:
application/json:
schema:
$ref: "#/components/schemas/Genre"
application/xml:
schema:
$ref: "#/components/schemas/Genre"
"401":
$ref: "#/components/responses/Unauthorized"
components:
responses:
Unauthorized:
description: Unauthorized
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example:
code: 401
message: User not authorized
application/xml:
schema:
$ref: "#/components/schemas/Error"
example:
code: 401
message: User not authorized
schemas:
Categories:
type: object
properties:
total:
type: number
next:
type: string
format: url
previous:
type: string
format: url
results:
type: array
items:
$ref: "#/components/schemas/Category"
example:
total: 5
next: https://demo.funkwhale.audio/api/v2/categories?page=2&page_size=2&q=crime
previous: null
results:
- category: "True Crime"
created_date: "2020-01-01T00:00:00.000Z"
tagged_items: 5
results_page: "https://demo.funkwhale.audio/library/categories/True%20Crime"
- category: "True Stories"
created_date: "2023-12-15T23:32:52.000Z"
tagged_items: 200
results_page: "https://demo.funkwhale.audio/library/categories/True%20Stories"
Category:
type: object
properties:
category:
type: string
created_date:
type: string
format: date-time
tagged_items:
type: number
results_page:
type: string
format: url
example:
category: "True Crime"
created_date: "2020-01-01T00:00:00.000Z"
tagged_items: 5
results_page: "https://demo.funkwhale.audio/library/categories/True%20Crime"
Genres:
type: object
properties:
total:
type: number
next:
type: string
format: url
previous:
type: string
format: url
results:
type: array
items:
$ref: "#/components/schemas/Genre"
example:
total: 5
next: https://demo.funkwhale.audio/api/v2/categories?page=2&page_size=2&q=rock
previous: null
results:
- genre: "Acoustic Rock"
created_date: "2020-01-01T00:00:00.000Z"
tagged_items: 5
results_page: "https://demo.funkwhale.audio/library/categories/Acoustic%20Rock"
- genre: "Surf Rock"
created_date: "2023-12-15T23:32:52.000Z"
tagged_items: 200
results_page: "https://demo.funkwhale.audio/library/categories/Surf%20Rock"
Genre:
type: object
properties:
genre:
type: string
created_date:
type: string
format: date-time
tagged_items:
type: number
results_page:
type: string
format: url
example:
genre: "Acoustic Rock"
created_date: "2020-01-01T00:00:00.000Z"
tagged_items: 5
results_page: "https://demo.funkwhale.audio/library/categories/Acoustic%20Rock"
Nodeinfo:
type: object
required:
- version
- software
- protocols
- services
- openRegistrations
- usage
- metadata
properties:
version:
type: string
enum:
- "2.1"
software:
type: object
required:
- name
- version
properties:
name:
type: string
enum:
- "Funkwhale"
version:
type: string
example: "1.4.0"
repository:
type: string
format: url
enum:
- "https://dev.funkwhale.audio/funkwhale/funkwhale"
homepage:
type: string
format: url
enum:
- "https://funkwhale.audio"
protocols:
type: array
minItems: 1
items:
type: string
enum:
- "activitypub"
- "buddycloud"
- "dfrn"
- "diaspora"
- "libertree"
- "ostatus"
- "pumpio"
- "tent"
- "xmpp"
- "zot"
example:
- "activitypub"
services:
type: object
required:
- inbound
- outbound
properties:
inbound:
type: array
items:
type: string
enum:
- "atom1.0"
- "gnusocial"
- "imap"
- "pnut"
- "pop3"
- "pumpio"
- "rss2.0"
- "twitter"
outbound:
type: array
items:
type: string
enum:
- "atom1.0"
- "blogger"
- "buddycloud"
- "diaspora"
- "dreamwidth"
- "drupal"
- "facebook"
- "friendica"
- "gnusocial"
- "google"
- "insanejournal"
- "libertree"
- "linkedin"
- "livejournal"
- "mediagoblin"
- "myspace"
- "pinterest"
- "pnut"
- "posterous"
- "pumpio"
- "redmatrix"
- "rss2.0"
- "smtp"
- "tent"
- "tumblr"
- "twitter"
- "wordpress"
- "xmpp"
openRegistrations:
type: boolean
usage:
type: object
required:
- users
properties:
users:
type: object
properties:
total:
type: integer
minimum: 0
activeHalfYear:
type: integer
minimum: 0
activeMonth:
type: integer
minimum: 0
localPosts:
type: integer
minimum: 0
localComments:
type: integer
minimum: 0
metadata:
type: object
properties:
actorId:
type: string
format: url
private:
type: boolean
shortDescription:
type: string
longDescription:
type: string
contactEmail:
type: string
format: email
nodeName:
type: string
banner:
type: string
format: url
nullable: true
defaultUploadQuota:
type: integer
supportedUploadExtensions:
type: array
items:
type: string
allowList:
type: object
properties:
enabled:
type: boolean
domains:
type: array
nullable: true
items:
type: string
funkwhaleSupportMessageEnabled:
type: boolean
instanceSupportMessage:
type: string
languages:
type: array
items:
type: string
location:
type: string
codeOfConduct:
type: string
format: url
content:
type: object
properties:
local:
type: object
properties:
artists:
type: number
releases:
type: number
recordings:
type: number
hoursOfContent:
type: number
example:
artists: 1000
releases: 10000
recordings: 150000
hoursOfContent: 7500
topMusicCategories:
type: array
items:
type: object
properties:
name:
type: string
count:
type: integer
minimum: 0
example:
- name: "rock"
count: 1256
- name: "jazz"
count: 604
- name: "classical"
count: 308
topPodcastCategories:
type: array
items:
type: object
properties:
name:
type: string
count:
type: integer
minimum: 0
example:
- name: "comedy"
count: 12
- name: "politics"
count: 4
- name: "nature"
count: 1
federation:
type: object
properties:
followedInstances:
type: integer
followingInstances:
type: integer
usage:
type: object
properties:
listenings:
type: integer
minimum: 0
downloads:
type: integer
minimum: 0
favorites:
type: object
properties:
tracks:
type: integer
minimum: 0
features:
type: array
items:
type: string
example:
- "channels"
- "podcasts"
- "collections"
- "audiobooks"
- "federation"
- "anonymousCanListen"
- "onlyMbidTaggedContent"
Error:
type: object
properties:
code:
type: string
message:
type: string
required:
- code
- message
securitySchemes:
oauth2:
type: oauth2
description: This API uses OAuth 2 with the Authorization Code flow. You can register an app using the /oauth/apps/ endpoint.
flows:
authorizationCode:
authorizationUrl: /authorize
tokenUrl: /api/v1/oauth/token/
refreshUrl: /api/v1/oauth/token/
scopes:
"read": "Read-only access to all user data"
"write": "Write-only access on all user data"
"read:edits": "Read-only access to edits"
"write:edits": "Write-only access to edits"
"read:favorites": "Read-only access to favorites"
"write:favorites": "Write-only access to favorits"
"read:filters": "Read-only to to content filters"
"write:filters": "Write-only access to content-filters"
"read:follows": "Read-only to follows"
"write:follows": "Write-only access to follows"
"read:libraries": "Read-only access to library and uploads"
"write:libraries": "Write-only access to libraries"
"read:listenings": "Read-only access to listening history"
"write:listenings": "Write-only access to listening history"
"read:notifications": "Read-only access to notifications"
"write:notifications": "Write-only access to notifications"
"read:playlists": "Read-only access to playlists"
"write:playlists": "Write-only access to playlists"
"read:profile": "Read-only access to profile data"
"write:profile": "Write-only access to profile data"
"read:radios": "Read-only access to radios"
"write:radios": "Write-only access to radios"
"read:reports": "Read-only access to reports"
"write:reports": "Write-only access to reports"
"read:security": "Read-only access security settings"
"write:security": "write-only access security settings"
security:
- oauth2: []
../../../api/funkwhale_api/common/schema.yml
\ No newline at end of file
## Playlist libraries to share audio files
### The Issue
- As a user I want to share a list of tracks privately to my friends
- As a user I want to have a single container to curate my content (not playlist and libraries, only playlists)
### Proposed Solution
The users can request access to the playlist content to the playlist owner
### Feature Behavior
Users will be able to click on a "Request access to playlist audios files" button. This is a `LibraryFollow` request of the `playlist.library`. Not to be confused with the playlist follow request (see #-followup)
#### Backend
##### Data model
`Playlist` one_to_one with `Library` through `library` field
`Upload` many_to_one with `Library` through `library` (reverse is `library.uploads`)
`Upload` has also a many_to_many with `Library` through `playlist_libraries` (the same upload can be share various time through various playlists). Reverse relation is `library.playlist_uploads`
We could migrate from O2M to M2M, but this is super complicated since : - it adds a lot of extra logic (you can't query the m2m if the instance is not save -> this generated problem to validate incoming AP objects) - having a built-in lib and playlist libs make verifications easier (only three built-in lib, playlist_lib are always private)
##### Workflow
Playlist activity -> library_scan(get the uploads) -> playlist_scan (set the upload.playlist_relation and create plts)
##### Federation
Since `Playlist` is the main object here, we use the `Playlist` activities to send the `Library` information on ActivitiPub.
There is no other reason to share the playlit.library to remote.
##### Migrations
1. Remote library are not changed
2. Local lib are not deleted but are assigned to a playlist
3. Libraries Follows are not touched
4. Remote want fetch local libs as always but they will need to update the data or fail (migrating uploads from `library` to `playlist_library`)
##### Done
- [x] `PlaylistViewSet` `add` `clear` `remove` update the uploads.playlist_libraries relationships
- [x] `PlaylistViewSet` `add` `clear` `remove` -> `schedule_scan` -> Update activity to remote -> playlist.library scan on remote
- [x] library and playlist scan delay are long (24h), force on ap update
- [x] make sure only owned upload are added to the playlist.library
- [x] update the "drop library" migrations to use the playlist.library instead of user follow
- [x] make sure user get the new libraries created after library drop
- [x] update the federation api : when we receive a fetch for a library the upload serializer need to know which lib (playlist lib or user lib)
- [x] Support library.playlist_uploads in library scan -> add playlist_uploads in items in library federation viewset
- [x] investigate library scan bug : don't delete old content of the lib (local cache?): we need to empty the playlist before the scan(not ideal but less work)
- [x] check actor has only have three built-in libs and upload.playlist_libraries is private after migration
- [x] Playlist discovery : fetch federation endpoint for playlists
- [ ] Seem like the federation fetch (either with fetch endpoint or retreive_ap_obj) is deleting the `privacy_level` since `audience` can only be public or null. Avoid `privacy_level` to be updated if it a local playlist.
### Follow up
- [ ] Add the frontend playlist button in the new ui
- [ ] Playlist discovery : display playlist fid in the frontend
- [ ] Document : The user that want to federate need to activate remote activities in it's user settings. Even if the library is public the playlist activities will not be sended to remote -> We need to implement a followers activity setting (#2362)
- [ ] allow users to change the upload to another built-in lib, make sure the upload is not delete (we would loose the playlist_library relation) but only updated
- [ ] Playlist discovery : add the playlist to my playlist collection = follow request to playlist
- [ ] Playlist Track activity (to avoid having to refetch the whole playlist)
- [ ] Add a popover button to force playlist scan ? or make playlist scan delay shorter ?
...@@ -10,7 +10,9 @@ Has an admin I can add plugins that support downloading tracks from third party ...@@ -10,7 +10,9 @@ Has an admin I can add plugins that support downloading tracks from third party
## Backend ## Backend
When a track queryset is called with `with_playable_uploads` if no upload is found we trigger `plugins.TRIGGER_THIRD_PARTY_UPLOAD`. When a radio or playlist queryset is called if no upload is found we trigger `plugins.TRIGGER_THIRD_PARTY_UPLOAD`.
RadioViewSet.tracks and PlaylistViewSet.tracks are concerned. These endpoints can be called a lot, `THIRD_PARTY_UPLOAD_MAX_UPLOADS` variable allows to limits the amount af requests that are sended to the tird party service.
`handle_stream` should filter the upload queryset to display manual upload before plugin upload `handle_stream` should filter the upload queryset to display manual upload before plugin upload
...@@ -21,9 +23,12 @@ Plugins registering `TRIGGER_THIRD_PARTY_UPLOAD` should : ...@@ -21,9 +23,12 @@ Plugins registering `TRIGGER_THIRD_PARTY_UPLOAD` should :
- trigger celery task. If not the queryset will take a long time to complete. - trigger celery task. If not the queryset will take a long time to complete.
- create an upload with an associated file - create an upload with an associated file
- delete the upload if no file is succefully downloaded - delete the upload if no file is succefully downloaded
- check if an upload has already been triggered to avoid overloading Celery
An example can be found in `funkwhale_api.contrib.archivedl` An example can be found in `funkwhale_api.contrib.archivedl`
To enable the archive-dl plugin : `FUNKWHALE_PLUGINS=funkwhale_api.contrib.archivedl`
## Follow up ## Follow up
-The frontend should update the track object if `TRIGGER_THIRD_PARTY_UPLOAD` -The frontend should update the track object if `TRIGGER_THIRD_PARTY_UPLOAD`
...@@ -32,3 +37,5 @@ An example can be found in `funkwhale_api.contrib.archivedl` ...@@ -32,3 +37,5 @@ An example can be found in `funkwhale_api.contrib.archivedl`
- trigger a channels group send so the frontend can update track qs when/if the upload is ready - trigger a channels group send so the frontend can update track qs when/if the upload is ready
- Third party track stream (do not download the file, only pass a stream) - Third party track stream (do not download the file, only pass a stream)
- Allow `THIRD_PARTY_UPLOAD_MAX_UPLOADS` to be set at the plugin level -> allow admin to set plugin conf in ui -> create PluginAdminViewSet
# Change your activity visibility # Change your activity visibility
Your **activity visibility** determines who can see your listening activity on Funkwhale. There are three visibility options: Your **activity visibility** determines who can see your activities on Funkwhale. There are three visibility options:
- **Nobody except me** – only you can see your listening activity. - **Nobody except me** – only you can see your listening activity.
- **Everyone on this instance** – users who have an account on the same {term}`pod` as you can see your listening activity. - **Everyone on this instance** – users who have an account on the same {term}`pod` as you can see your listening activity.
...@@ -36,3 +36,23 @@ To change your activity visibility: ...@@ -36,3 +36,23 @@ To change your activity visibility:
:::: ::::
That's it! You've updated your activity visibility. This change takes effect as soon as you update your settings. That's it! You've updated your activity visibility. This change takes effect as soon as you update your settings.
## Understand where your data goes
Funkwhale is a federated software. It means data is share acfross the networks and some of you personal data might be sended to. When your visibility settings is set to `followers` data will be send to each on of them and to each on of their instance. This means your data will leave your own instance to go to your followers instances. If you delete a user from your follower or if you change your activities visibility your instance should send a request to the remotes servers so they delete your data. But there is nothing we can to to force it to comply. So when you trust a followers, you're also trusting his instance.
## Your activities
Has a user you can produce the following activities:
- Playlist create
- Playlist update
- Playlist delete
- Listening create
- Listening delete
- Track Favorite create
- Track Favorite delete
- Track update
# Browser support targeting 95% coverage while enabling modern features
# This targets browsers that support ES2020+ and modern CSS features
# Cover 95% of global usage
> 1%
last 2 versions
not dead
# Exclude problematic browsers
not ie 11
not op_mini all
not android <= 4.4
not samsung <= 4
# Ensure modern browser support for ES2020+ features
chrome >= 87
firefox >= 78
safari >= 14
edge >= 88
# Mobile browsers
ios >= 14
and_chr >= 87
and_ff >= 78
...@@ -6,8 +6,7 @@ module.exports = { ...@@ -6,8 +6,7 @@ module.exports = {
extends: [ extends: [
'plugin:@intlify/vue-i18n/recommended', 'plugin:@intlify/vue-i18n/recommended',
'plugin:vue/vue3-recommended', 'plugin:vue/vue3-recommended',
'@vue/typescript/recommended', '@vue/typescript/recommended'
'@vue/standard'
], ],
globals: { globals: {
SharedArrayBuffer: 'readonly', SharedArrayBuffer: 'readonly',
...@@ -20,8 +19,14 @@ module.exports = { ...@@ -20,8 +19,14 @@ module.exports = {
ecmaVersion: 2020 ecmaVersion: 2020
}, },
plugins: [ plugins: [
'html',
'vue' 'vue'
], ],
ignorePatterns: [
'src/locales/*.json',
'dist/',
'stats.html'
],
rules: { rules: {
// NOTE: Nicer for the eye // NOTE: Nicer for the eye
'operator-linebreak': ['error', 'before'], 'operator-linebreak': ['error', 'before'],
...@@ -55,7 +60,10 @@ module.exports = { ...@@ -55,7 +60,10 @@ module.exports = {
'@typescript-eslint/no-this-alias': 'off', '@typescript-eslint/no-this-alias': 'off',
// TODO (wvffle): Remove after API Client // TODO (wvffle): Remove after API Client
'@typescript-eslint/no-explicit-any': 'off' '@typescript-eslint/no-explicit-any': 'off',
// Configure TypeScript style
'comma-dangle': ['error', 'never']
}, },
overrides: [ overrides: [
{ {
......
nodeLinker: node-modules
FROM --platform=$BUILDPLATFORM node:18-alpine AS builder FROM --platform=$BUILDPLATFORM node:22-alpine AS builder
RUN apk add --no-cache jq bash coreutils python3 build-base RUN apk add --no-cache jq bash coreutils python3 build-base
......
FROM node:18-alpine FROM node:22-alpine
# needed to compile translations # needed to compile translations
RUN apk add --no-cache jq bash coreutils python3 RUN apk add --no-cache jq bash coreutils git python3
EXPOSE 8080
WORKDIR /app/ WORKDIR /app/
COPY scripts/ ./scripts/
# Create node_modules directory to prevent it from being hidden by volume mounts
RUN mkdir -p node_modules
ADD scripts/ ./scripts/
ADD package.json yarn.lock ./ ADD package.json yarn.lock ./
RUN yarn install RUN yarn
COPY . . VOLUME /app
VOLUME /app/node_modules
EXPOSE 8080
CMD ["yarn", "serve"] CMD ["yarn", "dev", "--host"]
# Funkwhale Frontend
Please follow the instructions in [Set up your development environment — funkwhale 1.4.0 documentation](https://docs.funkwhale.audio/developer/setup/index.html).
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0" />
<meta name="generator" content="Funkwhale"> <meta name="generator" content="Funkwhale" />
<title>Funkwhale</title> <title>Funkwhale</title>
<meta name="description" content="Your free and federated audio platform"> <meta name="description" content="Your free and federated audio platform" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png?v=1"> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png?v=1" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png?v=1"> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png?v=1" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png?v=1"> <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png?v=1" />
<link rel="mask-icon" href="/safari-pinned-tab.svg?v=1" color="#009fe3"> <link rel="mask-icon" href="/safari-pinned-tab.svg?v=1" color="#009fe3" />
<link rel="shortcut icon" href="/favicon.ico?v=1"> <link rel="shortcut icon" href="/favicon.ico?v=1" />
<meta name="apple-mobile-web-app-title" content="Funkwhale"> <meta name="apple-mobile-web-app-title" content="Funkwhale" />
<meta name="application-name" content="Funkwhale"> <meta name="application-name" content="Funkwhale" />
<meta name="msapplication-TileColor" content="#009fe3"> <meta name="msapplication-TileColor" content="#009fe3" />
<meta name="theme-color" content="#f2711c"> <meta name="theme-color" content="#f2711c" />
<style> <style>
#fake-app { #fake-app {
width: 100vw; width: 100vw;
...@@ -68,7 +67,7 @@ ...@@ -68,7 +67,7 @@
</style> </style>
</head> </head>
<body id="body"> <body id="body" style="margin:0">
<div id="fake-app"> <div id="fake-app">
<div id="fake-sidebar"> <div id="fake-sidebar">
<div id="orange-square"></div> <div id="orange-square"></div>
...@@ -97,5 +96,4 @@ ...@@ -97,5 +96,4 @@
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>
{ {
"name": "front", "name": "front",
"version": "0.1.0", "version": "0.1.0",
"type": "module",
"private": true, "private": true,
"description": "Funkwhale front-end", "description": "Funkwhale front-end",
"author": "Funkwhale Collective <contact@funkwhale.audio>", "author": "Funkwhale Collective <contact@funkwhale.audio>",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"dev:docs": "VP_DOCS=true vitepress dev ui-docs",
"build": "vite build --mode development", "build": "vite build --mode development",
"build:deployment": "vite build", "build:deployment": "vite build",
"build:docs": "VP_DOCS=true vitepress build ui-docs",
"serve:docs": "VP_DOCS=true vitepress serve ui-docs",
"serve": "vite preview", "serve": "vite preview",
"test": "vitest run", "test": "vitest run",
"test:unit": "vitest run --coverage", "test:unit": "vitest run --coverage",
"test:generate-mock-server": "msw-auto-mock ../docs/schema.yml -o test/msw-server.ts --node", "test:generate-mock-server": "msw-auto-mock ../docs/schema.yml -o test/msw-server.ts --node",
"lint": "eslint --cache --cache-strategy content --ext .ts,.js,.vue,.json,.html src test cypress public/embed.html", "lint": "yarn lint:es && yarn lint:tsc",
"lint:tsc": "vue-tsc --noEmit --incremental && tsc --noEmit --incremental -p cypress", "lint:es": "eslint --max-warnings 0 --cache --cache-strategy content --ext .ts,.js,.vue,.json,.html,.cjs . cypress public/embed.html src test ui-docs",
"fix-fomantic-css": "scripts/fix-fomantic-css.sh", "lint:tsc": "vue-tsc --noEmit --incremental && tsc --noEmit --incremental --project tsconfig.json",
"postinstall": "yarn run fix-fomantic-css" "generate-types-from-local-schema": "yarn run openapi-typescript ../api/funkwhale_api/common/schema.yml -o src/generated/types.ts",
"generate-types-from-remote-schema": "yarn run openapi-typescript https://docs.funkwhale.audio/develop/swagger/schema.yml -o src/generated/types.ts",
"fmt:es": "yarn lint:es --fix",
"fmt:html": "node --experimental-strip-types node_modules/prettier/bin/prettier.cjs index.html public/embed.html --write"
}, },
"dependencies": { "dependencies": {
"@funkwhale/ui": "0.2.2",
"@sentry/tracing": "7.47.0", "@sentry/tracing": "7.47.0",
"@sentry/vue": "7.47.0", "@sentry/vue": "7.47.0",
"@tauri-apps/api": "2.0.0-beta.1", "@tauri-apps/api": "2.0.0-beta.1",
"@types/jsmediatags": "3.9.6",
"@vue/runtime-core": "3.3.11", "@vue/runtime-core": "3.3.11",
"@vueuse/core": "10.3.0", "@vueuse/components": "10.6.1",
"@vueuse/integrations": "10.3.0", "@vueuse/core": "10.6.1",
"@vueuse/math": "10.3.0", "@vueuse/integrations": "10.6.1",
"@vueuse/router": "10.3.0", "@vueuse/math": "10.6.1",
"@vueuse/router": "10.6.1",
"axios": "1.7.2", "axios": "1.7.2",
"axios-auth-refresh": "3.3.6", "axios-auth-refresh": "3.3.6",
"butterchurn": "3.0.0-beta.4", "butterchurn": "3.0.0-beta.4",
"butterchurn-presets": "3.0.0-beta.4", "butterchurn-presets": "3.0.0-beta.4",
"diff": "5.1.0", "diff": "5.1.0",
"dompurify": "3.0.8", "dompurify": "3.2.4",
"focus-trap": "7.2.0", "focus-trap": "7.2.0",
"fomantic-ui-css": "2.9.3",
"idb-keyval": "6.2.1", "idb-keyval": "6.2.1",
"jsmediatags": "3.9.7",
"lodash-es": "4.17.21", "lodash-es": "4.17.21",
"lru-cache": "10.2.0", "lru-cache": "10.2.0",
"magic-regexp": "0.8.0",
"moment": "2.29.4", "moment": "2.29.4",
"music-metadata-browser": "2.5.10",
"nanoid": "5.0.4",
"pinia": "2.1.7",
"showdown": "2.1.0", "showdown": "2.1.0",
"stacktrace-js": "2.0.2", "stacktrace-js": "2.0.2",
"standardized-audio-context": "25.3.60", "standardized-audio-context": "25.3.60",
"string-similarity-js": "2.1.4",
"text-clipper": "2.2.0", "text-clipper": "2.2.0",
"transliteration": "2.3.5", "transliteration": "2.3.5",
"type-fest": "4.30.1",
"universal-cookie": "4.0.4", "universal-cookie": "4.0.4",
"vite-plugin-pwa": "0.14.4", "vite-plugin-pwa": "0.14.4",
"vue": "3.3.11", "vue": "3.5.13",
"vue-dompurify-html": "5.2.0",
"vue-gettext": "2.1.12", "vue-gettext": "2.1.12",
"vue-i18n": "9.9.1", "vue-i18n": "9.9.1",
"vue-router": "4.2.5", "vue-router": "4.2.5",
...@@ -61,12 +76,12 @@ ...@@ -61,12 +76,12 @@
}, },
"devDependencies": { "devDependencies": {
"@faker-js/faker": "8.4.1", "@faker-js/faker": "8.4.1",
"@iconify/vue": "4.1.1",
"@intlify/eslint-plugin-vue-i18n": "2.0.0", "@intlify/eslint-plugin-vue-i18n": "2.0.0",
"@intlify/unplugin-vue-i18n": "2.0.0", "@intlify/unplugin-vue-i18n": "2.0.0",
"@tauri-apps/cli": "^2.0.2", "@tauri-apps/cli": "^2.0.2",
"@types/diff": "5.0.9", "@types/diff": "5.0.9",
"@types/dompurify": "3.0.5", "@types/dompurify": "3.0.5",
"@types/jquery": "3.5.29",
"@types/lodash-es": "4.17.12", "@types/lodash-es": "4.17.12",
"@types/moxios": "0.4.17", "@types/moxios": "0.4.17",
"@types/qs": "6.9.10", "@types/qs": "6.9.10",
...@@ -74,18 +89,20 @@ ...@@ -74,18 +89,20 @@
"@types/showdown": "2.0.6", "@types/showdown": "2.0.6",
"@types/vue-virtual-scroller": "npm:@earltp/vue-virtual-scroller", "@types/vue-virtual-scroller": "npm:@earltp/vue-virtual-scroller",
"@typescript-eslint/eslint-plugin": "7.1.0", "@typescript-eslint/eslint-plugin": "7.1.0",
"@vitejs/plugin-vue": "5.0.3", "@vitejs/plugin-vue": "5.1.4",
"@vitest/coverage-v8": "1.3.1", "@vitest/coverage-v8": "1.3.1",
"@vue-macros/volar": "0.13.3", "@vue-macros/common": "1.15.1",
"@vue-macros/volar": "0.17.2",
"@vue/compiler-sfc": "3.3.11", "@vue/compiler-sfc": "3.3.11",
"@vue/eslint-config-standard": "8.0.1", "@vue/eslint-config-standard": "8.0.1",
"@vue/eslint-config-typescript": "12.0.0", "@vue/eslint-config-typescript": "12.0.0",
"@vue/test-utils": "2.2.7", "@vue/test-utils": "2.4.1",
"@vue/tsconfig": "0.5.1", "@vue/tsconfig": "0.6.0",
"autoprefixer": "10.4.21",
"cypress": "13.6.4", "cypress": "13.6.4",
"eslint": "8.57.0", "eslint": "8.57.0",
"eslint-config-standard": "17.1.0", "eslint-config-standard": "17.1.0",
"eslint-plugin-html": "8.0.0", "eslint-plugin-html": "8.1.2",
"eslint-plugin-import": "2.29.1", "eslint-plugin-import": "2.29.1",
"eslint-plugin-n": "16.6.2", "eslint-plugin-n": "16.6.2",
"eslint-plugin-node": "11.1.0", "eslint-plugin-node": "11.1.0",
...@@ -95,20 +112,27 @@ ...@@ -95,20 +112,27 @@
"jsonc-eslint-parser": "2.4.0", "jsonc-eslint-parser": "2.4.0",
"msw": "2.2.1", "msw": "2.2.1",
"msw-auto-mock": "0.18.0", "msw-auto-mock": "0.18.0",
"openapi-typescript": "7.6.0",
"patch-package": "8.0.0", "patch-package": "8.0.0",
"postcss": "8.5.6",
"rollup-plugin-visualizer": "5.9.0", "rollup-plugin-visualizer": "5.9.0",
"sass": "1.57.1", "sass": "1.68.0",
"sinon": "15.0.2", "sinon": "15.0.2",
"standardized-audio-context-mock": "9.6.32", "standardized-audio-context-mock": "9.6.32",
"typescript": "5.3.3", "typescript": "5.3.3",
"unplugin-vue-macros": "2.4.6", "unplugin-vue-macros": "2.14.5",
"utility-types": "3.10.0", "utility-types": "3.10.0",
"vite": "5.2.12", "vite": "5.2.12",
"vite-plugin-node-polyfills": "0.17.0",
"vite-plugin-pwa": "0.14.4",
"vite-plugin-vue-devtools": "^7.5.2",
"vitepress": "1.5.0",
"vitest": "1.3.1", "vitest": "1.3.1",
"vue-tsc": "1.8.27", "vue-tsc": "3.0.5",
"workbox-core": "6.5.4", "workbox-core": "6.5.4",
"workbox-precaching": "6.5.4", "workbox-precaching": "6.5.4",
"workbox-routing": "6.5.4", "workbox-routing": "6.5.4",
"workbox-strategies": "6.5.4" "workbox-strategies": "6.5.4"
} },
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
} }
export default {
plugins: process.env.NODE_ENV === 'development' ? {
// Skip autoprefixer in development - modern dev browsers don't need prefixes
} : {
autoprefixer: {
overrideBrowserslist: [
'> 1%',
'last 2 versions',
'not dead',
'not ie 11',
'not op_mini all',
'chrome >= 87',
'firefox >= 78',
'safari >= 14',
'edge >= 88',
'ios >= 14'
]
}
}
}
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0" />
<meta name="generator" content="Funkwhale"> <meta name="generator" content="Funkwhale" />
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico" />
<title>Funkwhale Widget</title> <title>Funkwhale Widget</title>
<link rel="stylesheet" href="/embed.css"> <link rel="stylesheet" href="/embed.css" />
<script type="module"> <script type="module">
import { createApp, reactive, nextTick } from 'https://unpkg.com/petite-vue@0.4.1?module' import { createApp, reactive, nextTick } from 'https://unpkg.com/petite-vue@0.4.1?module'
...@@ -25,7 +24,7 @@ ...@@ -25,7 +24,7 @@
const id = params.get('id') const id = params.get('id')
// Error // Error
let error = reactive({ value: false }) const error = reactive({ value: false })
if (!SUPPORTED_TYPES.includes(type)) { if (!SUPPORTED_TYPES.includes(type)) {
error.value = `The embed widget doesn't support this media type: ${type}.` error.value = `The embed widget doesn't support this media type: ${type}.`
} }
...@@ -38,7 +37,7 @@ ...@@ -38,7 +37,7 @@
try { try {
baseUrl = new URL(baseUrl).origin baseUrl = new URL(baseUrl).origin
} catch (err) { } catch (err) {
console.error(err) // console.error(err)
error.value = `The embed widget couldn't read the provided instance URL: ${baseUrl}.` error.value = `The embed widget couldn't read the provided instance URL: ${baseUrl}.`
} }
...@@ -147,7 +146,7 @@ ...@@ -147,7 +146,7 @@
// NOTE: If we already have some tracks, let's fail silently // NOTE: If we already have some tracks, let's fail silently
if (tracks.length > 0) { if (tracks.length > 0) {
console.error(error.value) // console.error(error.value)
error.value = false error.value = false
} }
...@@ -181,7 +180,7 @@ ...@@ -181,7 +180,7 @@
// NOTE: Fetch tracks only if there is no error // NOTE: Fetch tracks only if there is no error
if (error.value === false) { if (error.value === false) {
fetchTracks().catch(err => { fetchTracks().catch(err => {
console.error(err) // console.error(err)
error.value = `An unknown error occurred while loading this ${type}.` error.value = `An unknown error occurred while loading this ${type}.`
}) })
} }
...@@ -388,29 +387,43 @@ ...@@ -388,29 +387,43 @@
</head> </head>
<template id="logo-template"> <template id="logo-template">
<a <a title="Funkwhale" href="https://funkwhale.audio" target="_blank" rel="noopener noreferrer" class="logo-link" tabindex="-1">
title="Funkwhale"
href="https://funkwhale.audio"
target="_blank"
rel="noopener noreferrer"
class="logo-link"
tabindex="-1"
>
<img src="/logo-white.svg" /> <img src="/logo-white.svg" />
</a> </a>
</template> </template>
<template id="icon-template"> <template id="icon-template">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="icon" fill="currentColor" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="icon" fill="currentColor" viewBox="0 0 16 16">
<path v-if="icon === 'pause'" d="M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5zm5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5z" /> <path
<path v-else-if="icon === 'play'" d="m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z" /> v-if="icon === 'pause'"
<path v-else-if="icon === 'prev'" d="M4 4a.5.5 0 0 1 1 0v3.248l6.267-3.636c.54-.313 1.232.066 1.232.696v7.384c0 .63-.692 1.01-1.232.697L5 8.753V12a.5.5 0 0 1-1 0V4z" /> d="M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5zm5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5z"
<path v-else-if="icon === 'next'" d="M12.5 4a.5.5 0 0 0-1 0v3.248L5.233 3.612C4.693 3.3 4 3.678 4 4.308v7.384c0 .63.692 1.01 1.233.697L11.5 8.753V12a.5.5 0 0 0 1 0V4z" /> />
<path v-else-if="icon === 'mute'" d="M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zm7.137 2.096a.5.5 0 0 1 0 .708L12.207 8l1.647 1.646a.5.5 0 0 1-.708.708L11.5 8.707l-1.646 1.647a.5.5 0 0 1-.708-.708L10.793 8 9.146 6.354a.5.5 0 1 1 .708-.708L11.5 7.293l1.646-1.647a.5.5 0 0 1 .708 0z" /> <path
v-else-if="icon === 'play'"
d="m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z"
/>
<path
v-else-if="icon === 'prev'"
d="M4 4a.5.5 0 0 1 1 0v3.248l6.267-3.636c.54-.313 1.232.066 1.232.696v7.384c0 .63-.692 1.01-1.232.697L5 8.753V12a.5.5 0 0 1-1 0V4z"
/>
<path
v-else-if="icon === 'next'"
d="M12.5 4a.5.5 0 0 0-1 0v3.248L5.233 3.612C4.693 3.3 4 3.678 4 4.308v7.384c0 .63.692 1.01 1.233.697L11.5 8.753V12a.5.5 0 0 0 1 0V4z"
/>
<path
v-else-if="icon === 'mute'"
d="M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zm7.137 2.096a.5.5 0 0 1 0 .708L12.207 8l1.647 1.646a.5.5 0 0 1-.708.708L11.5 8.707l-1.646 1.647a.5.5 0 0 1-.708-.708L10.793 8 9.146 6.354a.5.5 0 1 1 .708-.708L11.5 7.293l1.646-1.647a.5.5 0 0 1 .708 0z"
/>
<g v-else-if="icon === 'volume'"> <g v-else-if="icon === 'volume'">
<path d="M11.536 14.01A8.473 8.473 0 0 0 14.026 8a8.473 8.473 0 0 0-2.49-6.01l-.708.707A7.476 7.476 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303l.708.707z" /> <path
<path d="M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.483 5.483 0 0 1 11.025 8a5.483 5.483 0 0 1-1.61 3.89l.706.706z" /> d="M11.536 14.01A8.473 8.473 0 0 0 14.026 8a8.473 8.473 0 0 0-2.49-6.01l-.708.707A7.476 7.476 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303l.708.707z"
<path d="M8.707 11.182A4.486 4.486 0 0 0 10.025 8a4.486 4.486 0 0 0-1.318-3.182L8 5.525A3.489 3.489 0 0 1 9.025 8 3.49 3.49 0 0 1 8 10.475l.707.707zM6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06z" /> />
<path
d="M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.483 5.483 0 0 1 11.025 8a5.483 5.483 0 0 1-1.61 3.89l.706.706z"
/>
<path
d="M8.707 11.182A4.486 4.486 0 0 0 10.025 8a4.486 4.486 0 0 0-1.318-3.182L8 5.525A3.489 3.489 0 0 1 9.025 8 3.49 3.49 0 0 1 8 10.475l.707.707zM6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06z"
/>
</g> </g>
</svg> </svg>
</template> </template>
...@@ -447,25 +460,14 @@ ...@@ -447,25 +460,14 @@
<span v-scope="Icon({ icon: 'next' })"></span> <span v-scope="Icon({ icon: 'next' })"></span>
</button> </button>
<input <input v-model.number="player.seek" v-range="player.seek" @input="player.seekTime" type="range" step="0.1" />
v-model.number="player.seek"
v-range="player.seek"
@input="player.seekTime"
type="range"
step="0.1"
/>
<button @click="volume.mute"> <button @click="volume.mute">
<span v-if="volume.level === 0" v-scope="Icon({ icon: 'mute' })"></span> <span v-if="volume.level === 0" v-scope="Icon({ icon: 'mute' })"></span>
<span v-else v-scope="Icon({ icon: 'volume' })"></span> <span v-else v-scope="Icon({ icon: 'volume' })"></span>
</button> </button>
<input <input v-model.number="volume.level" v-range="volume.level" type="range" step="0.1" />
v-model.number="volume.level"
v-range="volume.level"
type="range"
step="0.1"
/>
</div> </div>
<span v-scope="Logo()" class="logo-wrapper"></span> <span v-scope="Logo()" class="logo-wrapper"></span>
...@@ -483,21 +485,11 @@ ...@@ -483,21 +485,11 @@
@keyup.enter="player.play(index)" @keyup.enter="player.play(index)"
tabindex="0" tabindex="0"
> >
<td> <td> {{ index + 1 }} </td>
{{ index + 1 }} <td :title="track.title"> {{ track.title }} </td>
</td> <td :title="track.artist.name"> {{ track.artist.name }} </td>
<td :title="track.title"> <td :title="track.album?.title"> {{ track.album?.title }} </td>
{{ track.title }} <td> {{ formatDuration(track.sources?.[0].duration ?? 0) }} </td>
</td>
<td :title="track.artist.name">
{{ track.artist.name }}
</td>
<td :title="track.album?.title">
{{ track.album?.title }}
</td>
<td>
{{ formatDuration(track.sources?.[0].duration ?? 0) }}
</td>
</tr> </tr>
</table> </table>
</div> </div>
...@@ -508,10 +500,9 @@ ...@@ -508,10 +500,9 @@
:key="source.mimetype + source.listen_url" :key="source.mimetype + source.listen_url"
:type="source.mimetype" :type="source.mimetype"
:src="source.listen_url" :src="source.listen_url"
> />
</audio> </audio>
</template> </template>
</main> </main>
</body> </body>
</html> </html>
#!/usr/bin/env python3
"""
This scripts handles all the heavy-lifting of parsing CSS files from ``fomantic-ui-css`` and:
1. Replace hardcoded values by their CSS vars counterparts, for easier theming
2. Strip unused styles and icons to reduce the final size of CSS
Updated files are not modified in place, but instead copied to another directory (``fomantic-ui-css/tweaked``), in order
to allow easy comparison detection of changes.
If you change this file, you'll need to run ``yarn run fix-fomantic-css`` manually for the changes
to be picked up. If the ``NOSTRIP`` environment variable is set, the second step will be skipped.
"""
import argparse
import os
STRIP_UNUSED = "NOSTRIP" not in os.environ
# Perform a blind replacement of some strings in all fomantic CSS files
GLOBAL_REPLACES = [
# some selectors are repeated in the stylesheet, for some reason
(".ui.ui.ui.ui", ".ui"),
(".ui.ui.ui", ".ui"),
(".ui.ui", ".ui"),
(".icon.icon.icon.icon", ".icon"),
(".icon.icon.icon", ".icon"),
(".icon.icon", ".icon"),
# actually useful stuff
("'Lato', 'Helvetica Neue', Arial, Helvetica, sans-serif", "var(--font-family)"),
(".orange", ".vibrant"),
("#F2711C", "var(--vibrant-color)"),
("#FF851B", "var(--vibrant-color)"),
("#f26202", "var(--vibrant-hover-color)"),
("#e76b00", "var(--vibrant-hover-color)"),
("#cf590c", "var(--vibrant-active-color)"),
("#f56100", "var(--vibrant-active-color)"),
("#e76b00", "var(--vibrant-active-color)"),
("#e55b00", "var(--vibrant-focus-color)"),
("#f17000", "var(--vibrant-focus-color)"),
(".teal", ".accent"),
("#00B5AD", "var(--accent-color)"),
("#009c95", "var(--accent-hover-color)"),
("#00827c", "var(--accent-active-color)"),
("#008c86", "var(--accent-focus-color)"),
(".green", ".success"),
("#21BA45", "var(--success-color)"),
("#2ECC40", "var(--success-color)"),
("#16ab39", "var(--success-hover-color)"),
("#1ea92e", "var(--success-hover-color)"),
("#198f35", "var(--success-active-color)"),
("#25a233", "var(--success-active-color)"),
("#0ea432", "var(--success-focus-color)"),
("#19b82b", "var(--success-focus-color)"),
(".blue", ".primary"),
("#2185D0", "var(--primary-color)"),
("#54C8FF", "var(--primary-color)"),
("#54C8FF", "var(--primary-color)"),
("#1678c2", "var(--primary-hover-color)"),
("#21b8ff", "var(--primary-hover-color)"),
("#1a69a4", "var(--primary-active-color)"),
("#0d71bb", "var(--primary-focus-color)"),
("#2bbbff", "var(--primary-focus-color)"),
(".yellow", ".warning"),
("#FBBD08", "var(--warning-color)"),
("#FFE21F", "var(--warning-color)"),
("#eaae00", "var(--warning-hover-color)"),
("#ebcd00", "var(--warning-hover-color)"),
("#cd9903", "var(--warning-active-color)"),
("#ebcd00", "var(--warning-active-color)"),
("#daa300", "var(--warning-focus-color)"),
("#f5d500", "var(--warning-focus-color)"),
(".red.", ".danger."),
("#DB2828", "var(--danger-color)"),
("#FF695E", "var(--danger-color)"),
("#d01919", "var(--danger-hover-color)"),
("#ff392b", "var(--danger-hover-color)"),
("#b21e1e", "var(--danger-active-color)"),
("#ca1010", "var(--danger-focus-color)"),
("#ff4335", "var(--danger-focus-color)"),
]
def discard_unused_icons(rule):
"""
Add an icon to this list if you want to use it in the app.
"""
used_icons = [
".angle",
".arrow",
".at",
".ban",
".bell",
".book",
".bookmark",
".check",
".clock",
".close",
".cloud",
".code",
".comment",
".copy",
".copyright",
".danger",
".database",
".delete",
".disc",
".down angle",
".download",
".dropdown",
".edit",
".ellipsis",
".eraser",
".external",
".eye",
".feed",
".file",
".folder",
".forward",
".globe",
".hashtag",
".headphones",
".heart",
".home",
".hourglass",
".info",
".layer",
".lines",
".link",
".list",
".loading",
".lock",
".minus",
".mobile",
".music",
".paper",
".pause",
".pencil",
".play",
".plus",
".podcast",
".question",
".question ",
".random",
".redo",
".refresh",
".repeat",
".rss",
".search",
".server",
".share",
".shield",
".sidebar",
".sign",
".spinner",
".step",
".stream",
".track",
".trash",
".undo",
".upload",
".user",
".users",
".volume",
".wikipedia",
".wrench",
".x",
".key",
".cog",
".life.ring",
".language",
".palette",
".sun",
".moon",
".gitlab",
".chevron",
".right",
".left",
".compress",
".expand",
".image",
]
if ":before" not in rule["lines"][0]:
return False
return not match(rule, used_icons)
"""
Below is the main configuration object that is used for fine-grained replacement of properties
in component files. It also handles removal of unused selectors.
Example config for a component:
REPLACEMENTS = {
# applies to fomantic-ui-css/components/component-name.css
"component-name": {
# Discard any CSS rule matching one of the selectors listed below
# matching is done using a simple string search, so ``.pink`` will remove
# rules applied to ``.pink``, ``.pink.button`` and `.pinkdark`
"skip": [
".unused.variation",
".pink",
],
# replace some CSS properties values in specific selectors
(".inverted", ".dark"): [
("background", "var(--inverted-background)"),
("color", "var(--inverted-color)"),
],
(".active"): [
("font-size", "var(--active-font-size)"),
],
}
}
Given the previous config, the following style sheet:
.. code-block:: css
.unused.variation {
color: yellow;
}
.primary {
color: white;
}
.primary.pink {
color: pink;
}
.inverted.primary {
background: black;
color: white;
border-top: 1px solid red;
}
.inverted.primary.active {
font-size: 12px;
}
Would be converted to:
.. code-block:: css
.primary {
color: white;
}
.inverted.primary {
background: var(--inverted-background);
color: var(--inverted-color);
border-top: 1px solid red;
}
.inverted.primary.active {
font-size: var(--active-font-size);
}
"""
REPLACEMENTS = {
"site": {
("a",): [
("color", "var(--link-color)"),
("text-decoration", "var(--link-text-decoration)"),
],
("a:hover",): [
("color", "var(--link-hover-color)"),
("text-decoration", "var(--link-hover-text-decoration)"),
],
("body",): [
("background", "var(--site-background)"),
("color", "var(--text-color)"),
],
(
"::-webkit-selection",
"::-moz-selection",
"::selection",
): [
("color", "var(--text-selection-color)"),
("background-color", "var(--text-selection-background)"),
],
(
"textarea::-webkit-selection",
"input::-webkit-selection",
"textarea::-moz-selection",
"input::-moz-selection",
"textarea::selection",
"input::selection",
): [
("color", "var(--input-selection-color)"),
("background-color", "var(--input-selection-background)"),
],
},
"button": {
"skip": [
".vertical",
".animated",
".active",
".olive",
".brown",
".teal",
".violet",
".purple",
".brown",
".grey",
".black",
".positive",
".negative",
".secondary",
".tertiary",
".facebook",
".twitter",
".google.plus",
".vk",
".linkedin",
".instagram",
".youtube",
".whatsapp",
".telegram",
],
(".ui.orange.button", ".ui.orange.button:hover"): [
("background-color", "var(--button-orange-background)")
],
(".ui.basic.button",): [
("background", "var(--button-basic-background)"),
("color", "var(--button-basic-color)"),
("box-shadow", "var(--button-basic-box-shadow)"),
],
(".ui.basic.button:hover",): [
("background", "var(--button-basic-hover-background)"),
("color", "var(--button-basic-hover-color)"),
("box-shadow", "var(--button-basic-hover-box-shadow)"),
],
(".ui.basic.button:focus",): [
("background", "var(--button-basic-hover-background)"),
("color", "var(--button-basic-hover-color)"),
("box-shadow", "var(--button-basic-hover-box-shadow)"),
],
},
"card": {
"skip": [
".inverted",
".olive",
".brown",
".teal",
".violet",
".purple",
".brown",
".grey",
".pink",
".black",
".vibrant",
".success",
".warning",
".danger",
".primary",
".secondary",
".horizontal",
".raised",
]
},
"checkbox": {
(
".ui.toggle.checkbox label",
".ui.toggle.checkbox input:checked ~ label",
'.ui.checkbox input[type="checkbox"]',
".ui.checkbox input:focus ~ label",
".ui.toggle.checkbox input:focus:checked ~ label",
".ui.checkbox input:active ~ label",
): [
("color", "var(--form-label-color)"),
],
(".ui.toggle.checkbox label:before",): [
("background", "var(--input-background)"),
],
},
"divider": {
(".ui.divider:not(.vertical):not(.horizontal)",): [
("border-top", "var(--divider)"),
("border-bottom", "var(--divider)"),
],
(".ui.divider",): [
("color", "var(--text-color)"),
],
},
"dimmer": {
(".ui.inverted.dimmer",): [
("background-color", "var(--dimmer-background)"),
("color", "var(--dropdown-color)"),
],
},
"dropdown": {
"skip": [
".error",
".info",
".success",
".warning",
],
(
".ui.selection.dropdown",
".ui.selection.visible.dropdown > .text:not(.default)",
".ui.dropdown .menu",
): [
("background", "var(--dropdown-background)"),
("color", "var(--dropdown-color)"),
],
(".ui.dropdown .menu > .item",): [
("color", "var(--dropdown-item-color)"),
],
(".ui.dropdown .menu > .item:hover",): [
("color", "var(--dropdown-item-hover-color)"),
("background", "var(--dropdown-item-hover-background)"),
],
(".ui.dropdown .menu .selected.item",): [
("color", "var(--dropdown-item-selected-color)"),
("background", "var(--dropdown-item-selected-background)"),
],
(".ui.dropdown .menu > .header:not(.ui)",): [
("color", "var(--dropdown-header-color)"),
],
(".ui.dropdown .menu > .divider",): [
("border-top", "var(--divider)"),
],
},
"form": {
"skip": [
".inverted",
".success",
".warning",
".error",
".info",
],
('.ui.form input[type="text"]', ".ui.form select", ".ui.input textarea"): [
("background", "var(--input-background)"),
("color", "var(--input-color)"),
],
(
'.ui.form input[type="text"]:focus',
".ui.form select:focus",
".ui.form textarea:focus",
): [
("background", "var(--input-focus-background)"),
("color", "var(--input-focus-color)"),
],
(
".ui.form ::-webkit-input-placeholder",
".ui.form :-ms-input-placeholder",
".ui.form ::-moz-placeholder",
): [
("color", "var(--input-placeholder-color)"),
],
(
".ui.form :focus::-webkit-input-placeholder",
".ui.form :focus:-ms-input-placeholder",
".ui.form :focus::-moz-placeholder",
): [
("color", "var(--input-focus-placeholder-color)"),
],
(
".ui.form .field > label",
".ui.form .inline.fields .field > label",
): [
("color", "var(--form-label-color)"),
],
},
"grid": {
"skip": [
"wide tablet",
"screen",
"mobile only",
"tablet only",
"computer only",
"computer reversed",
"tablet reversed",
"wide computer",
"wide mobile",
"wide tablet",
"vertically",
".celled",
".doubling",
".olive",
".brown",
".teal",
".violet",
".purple",
".brown",
".grey",
".black",
".positive",
".negative",
".secondary",
".tertiary",
".danger",
".vibrant",
".warning",
".primary",
".success",
".justified",
".centered",
]
},
"icon": {"skip": discard_unused_icons},
"input": {
(".ui.input > input",): [
("background", "var(--input-background)"),
("color", "var(--input-color)"),
],
(".ui.input > input:focus",): [
("background", "var(--input-focus-background)"),
("color", "var(--input-focus-color)"),
],
(
".ui.input > input::-webkit-input-placeholder",
".ui.input > input::-moz-placeholder",
".ui.input > input:-ms-input-placeholder",
): [
("color", "var(--input-placeholder-color)"),
],
(
".ui.input > input:focus::-webkit-input-placeholder",
".ui.input > input:focus::-moz-placeholder",
".ui.input > input:focus:-ms-input-placeholder",
): [
("color", "var(--input-focus-placeholder-color)"),
],
},
"item": {
(".ui.divided.items > .item",): [
("border-top", "var(--divider)"),
],
(".ui.items > .item > .content",): [
("color", "var(--text-color)"),
],
(".ui.items > .item .extra",): [
("color", "var(--really-discrete-text-color)"),
],
},
"header": {
"skip": [
".inverted",
".block",
".olive",
".brown",
".teal",
".violet",
".purple",
".brown",
".grey",
".black",
".pink",
],
(".ui.header",): [
("color", "var(--header-color)"),
],
(".ui.header .sub.header",): [
("color", "var(--header-color)"),
],
},
"label": {
"skip": [
".olive",
".brown",
".teal",
".violet",
".purple",
".brown",
".grey",
".black",
".positive",
".negative",
".secondary",
".tertiary",
".facebook",
".twitter",
".google.plus",
".vk",
".linkedin",
".instagram",
".youtube",
".whatsapp",
".telegram",
".corner",
"ribbon",
"pointing",
"attached",
],
},
"list": {
"skip": [
".mini",
".tiny",
".small",
".large",
".big",
".huge",
".massive",
".celled",
".horizontal",
".bulleted",
".ordered",
".suffixed",
".inverted",
".fitted",
"aligned",
],
(".ui.list .list > .item a.header", ".ui.list .list > a.item"): [
("color", "var(--link-color)"),
("text-decoration", "var(--link-text-decoration)"),
],
("a:hover", ".ui.list .list > a.item:hover"): [
("color", "var(--link-hover-color)"),
("text-decoration", "var(--link-hover-text-decoration)"),
],
},
"loader": {
"skip": [
".olive",
".brown",
".teal",
".violet",
".purple",
".brown",
".grey",
".black",
".pink",
".primary",
".vibrant",
".warning",
".success",
".danger",
".elastic",
],
(".ui.inverted.dimmer > .ui.loader",): [
("color", "var(--dimmer-color)"),
],
},
"message": {
"skip": [
".olive",
".brown",
".teal",
".violet",
".purple",
".brown",
".grey",
".black",
".pink",
".vibrant",
".primary",
".secondary",
".floating",
],
},
"menu": {
"skip": [
".inverted.pointing",
".olive",
".brown",
".teal",
".violet",
".purple",
".brown",
".grey",
".black",
".vertical.tabular",
".primary.menu",
".pink.menu",
".vibrant.menu",
".warning.menu",
".success.menu",
".danger.menu",
".fitted",
"fixed",
],
(".ui.menu .item",): [
("color", "var(--menu-item-color)"),
],
(".ui.vertical.inverted.menu .menu .item", ".ui.inverted.menu .item"): [
("color", "var(--inverted-menu-item-color)"),
],
(".inverted-ui.menu .active.item",): [
("color", "var(--menu-inverted-active-item-color)"),
],
(".ui.secondary.pointing.menu .active.item",): [
("color", "var(--secondary-menu-active-item-color)"),
],
(
".ui.secondary.pointing.menu a.item:hover",
".ui.secondary.pointing.menu .active.item:hover",
): [
("color", "var(--secondary-menu-hover-item-color)"),
],
(".ui.menu .ui.dropdown .menu > .item",): [
("color", "var(--dropdown-item-color) !important"),
],
(".ui.menu .ui.dropdown .menu > .item:hover",): [
("color", "var(--dropdown-item-hover-color) !important"),
("background", "var(--dropdown-item-hover-background) !important"),
],
(".ui.menu .dropdown.item .menu",): [
("color", "var(--dropdown--color)"),
("background", "var(--dropdown-background)"),
],
(".ui.menu .ui.dropdown .menu > .active.item",): [
("color", "var(--dropdown-item-selected-color)"),
("background", "var(--dropdown-item-selected-background) !important"),
],
},
"modal": {
(".ui.modal", ".ui.modal > .actions", ".ui.modal > .content"): [
("background", "var(--modal-background)"),
("border-bottom", "var(--divider)"),
("border-top", "var(--divider)"),
],
(".ui.modal > .close.inside",): [
("color", "var(--text-color)"),
],
(".ui.modal > .header",): [
("color", "var(--header-color)"),
("background", "var(--modal-background)"),
("border-bottom", "var(--divider)"),
("border-top", "var(--divider)"),
],
},
"search": {
(
".ui.search > .results",
".ui.search > .results .result",
".ui.category.search > .results .category .results",
".ui.category.search > .results .category",
".ui.category.search > .results .category > .name",
".ui.search > .results > .message .header",
".ui.search > .results > .message .description",
): [
("background", "var(--dropdown-background)"),
("color", "var(--dropdown-item-color)"),
],
(
".ui.search > .results .result .title",
".ui.search > .results .result .description",
): [
("color", "var(--dropdown-item-color)"),
],
(".ui.search > .results .result:hover",): [
("color", "var(--dropdown-item-hover-color)"),
("background", "var(--dropdown-item-hover-background)"),
],
},
"segment": {
"skip": [
".stacked",
".horizontal.segment",
".inverted.segment",
".circular",
".piled",
],
},
"sidebar": {
(".ui.left.visible.sidebar",): [
("box-shadow", "var(--sidebar-box-shadow)"),
]
},
"statistic": {
(".ui.statistic > .value", ".ui.statistic > .label"): [
("color", "var(--text-color)"),
],
},
"progress": {
(".ui.progress.success > .label",): [
("color", "var(--text-color)"),
],
},
"table": {
"skip": [
".marked",
".olive",
".brown",
".teal",
".violet",
".purple",
".brown",
".grey",
".black",
".padded",
".column.table",
".inverted",
".definition",
".error",
".negative",
".structured",
"tablet stackable",
],
(
".ui.table",
".ui.table > thead > tr > th",
): [
("color", "var(--text-color)"),
("background", "var(--table-background)"),
],
(".ui.table > tr > td", ".ui.table > tbody + tbody tr:first-child > td"): [
("border-top", "var(--table-border)"),
],
},
}
def match(rule, skip):
if hasattr(skip, "__call__"):
return skip(rule)
for s in skip:
for rs in rule["selectors"]:
if s in rs:
return True
return False
def rules_from_media_query(rule):
internal = rule["lines"][1:-1]
return parse_rules("\n".join(internal))
def wraps(rule, internal_rules):
return {
"lines": [rule["lines"][0]]
+ [line for r in internal_rules for line in r["lines"]]
+ ["}"]
}
def set_vars(component_name, rules):
"""
Given rules parsed via ``parse_rules``, replace properties values when needed
using ``REPLACEMENTS`` and ``GLOBAL_REPLACES``.
Also remove unused styles if STRIP_UNUSED is set to True.
"""
final_rules = []
try:
conf = REPLACEMENTS[component_name]
except KeyError:
return rules
selectors = list(conf.keys()) + list()
skip = None
if STRIP_UNUSED:
skip = conf.get("skip", [])
try:
skip = set(skip)
except TypeError:
pass
for rule in rules:
if rule["lines"][0].startswith("@media"):
# manual handling of media queries, because our parser is really
# simplistic
internal_rules = rules_from_media_query(rule)
internal_rules = set_vars(component_name, internal_rules)
rule = wraps(rule, internal_rules)
if len(rule["lines"]) > 2:
final_rules.append(rule)
continue
if skip and match(rule, skip):
# discard rule entirely
continue
matching = []
for s in selectors:
if set(s) & set(rule["selectors"]):
matching.append(s)
if not matching:
# no replacements to apply, keep rule as is
final_rules.append(rule)
continue
new_rule = {"lines": []}
for m in matching:
# the block match one of our replacement rules, so we loop on each line
# and replace values if needed.
replacements = conf[m]
for line in rule["lines"]:
for property, new_value in replacements:
if line.strip().startswith(f"{property}:"):
new_property = f"{property}: {new_value};"
indentation = " " * (len(line) - len(line.lstrip(" ")))
line = indentation + new_property
break
new_rule["lines"].append(line)
final_rules.append(new_rule)
return final_rules
def parse_rules(text):
"""
Really basic CSS parsers that stores selectors and corresponding properties. Only works
because the source files have coma-separated selectors (one per line), and one
property/value per line.
Returns a list of dictionaries, each dictionarry containing the selectors and
lines of of each block.
"""
rules = []
current_rule = None
opened_brackets = 0
current_selector = []
for line in text.splitlines():
if not current_rule and line.endswith(","):
current_selector.append(line.rstrip(",").strip())
elif line.endswith(" {"):
# for media queries
opened_brackets += 1
if not current_rule:
current_selector.append(line.rstrip("{").strip())
current_rule = {
"lines": [",\n".join(current_selector) + " {"],
"selectors": current_selector,
}
else:
current_rule["lines"].append(line)
elif current_rule:
current_rule["lines"].append(line)
if line.strip() == "}":
opened_brackets -= 1
if not opened_brackets:
# move on to next rule
rules.append(current_rule)
current_rule = None
current_selector = []
return rules
def serialize_rules(rules):
"""
Convert rules back to valid CSS.
"""
lines = []
for rule in rules:
for line in rule["lines"]:
lines.append(line)
return "\n".join(lines)
def iter_components(dir):
for dname, dirs, files in os.walk(dir):
for fname in files:
if fname.endswith(".min.css"):
continue
if fname.endswith(".js"):
continue
if "semantic" in fname:
continue
if fname.endswith(".css"):
yield os.path.join(dname, fname)
def replace_vars(source, dest):
components = list(sorted(iter_components(os.path.join(source, "components"))))
for c in components:
with open(c) as f:
text = f.read()
for s, r in GLOBAL_REPLACES:
text = text.replace(s, r)
text = text.replace(s.lower(), r)
text = text.replace(s.upper(), r)
rules = parse_rules(text)
name = c.split("/")[-1].split(".")[0]
updated_rules = set_vars(name, rules)
text = serialize_rules(updated_rules)
with open(os.path.join(dest, f"{name}.css"), "w") as f:
f.write(text)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Replace hardcoded values by CSS vars and strip unused rules"
)
parser.add_argument(
"source", help="Source path of the fomantic-ui-less distribution to fix"
)
parser.add_argument(
"dest", help="Destination directory where fixed files should be written"
)
args = parser.parse_args()
replace_vars(source=args.source, dest=args.dest)
#!/usr/bin/env bash
set -eux
cd "$(dirname "$0")/.." # change into base directory
FOMANTIC_SRC_PATH="node_modules/fomantic-ui-css"
find "$FOMANTIC_SRC_PATH/components" -name "*.min.css" -delete
mkdir -p "$FOMANTIC_SRC_PATH/tweaked"
echo 'Removing google font…'
sed -i '/@import url(/d' "$FOMANTIC_SRC_PATH/components/site.css"
echo "Replacing hardcoded values by CSS vars…"
scripts/fix-fomantic-css.py "$FOMANTIC_SRC_PATH" "$FOMANTIC_SRC_PATH/tweaked"
echo 'Fixing jQuery import…'
# shellcheck disable=SC2046
sed -i '1s/^import jQuery from "jquery"//' $(find "$FOMANTIC_SRC_PATH" -name '*.js')
# shellcheck disable=SC2046
sed -i '1s/^/import jQuery from "jquery"\n/' $(find "$FOMANTIC_SRC_PATH" -name '*.js')
<script setup lang="ts"> <script setup lang="ts">
import type { QueueTrack } from '~/composables/audio/queue' import { watchEffect, computed, onMounted, nextTick } from 'vue'
import { useIntervalFn, useStyleTag, useToggle, useWindowSize } from '@vueuse/core' import { type QueueTrack, useQueue } from '~/composables/audio/queue'
import { computed, nextTick, onMounted, watchEffect, defineAsyncComponent } from 'vue'
import { useQueue } from '~/composables/audio/queue'
import { useStore } from '~/store' import { useStore } from '~/store'
import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
import useLogger from '~/composables/useLogger' import useLogger from '~/composables/useLogger'
import { useStyleTag, useIntervalFn } from '@vueuse/core'
import { color } from '~/composables/color'
import { generateTrackCreditStringFromQueue } from '~/utils/utils' import { generateTrackCreditStringFromQueue } from '~/utils/utils'
const ChannelUploadModal = defineAsyncComponent(() => import('~/components/channels/UploadModal.vue')) import '~/style/funkwhale.scss'
const PlaylistModal = defineAsyncComponent(() => import('~/components/playlists/PlaylistModal.vue'))
const FilterModal = defineAsyncComponent(() => import('~/components/moderation/FilterModal.vue')) import PlaylistModal from '~/components/playlists/PlaylistModal.vue'
const ReportModal = defineAsyncComponent(() => import('~/components/moderation/ReportModal.vue')) import FilterModal from '~/components/moderation/FilterModal.vue'
const ServiceMessages = defineAsyncComponent(() => import('~/components/ServiceMessages.vue')) import ReportModal from '~/components/moderation/ReportModal.vue'
const ShortcutsModal = defineAsyncComponent(() => import('~/components/ShortcutsModal.vue')) import ServiceMessages from '~/components/ServiceMessages.vue'
const AudioPlayer = defineAsyncComponent(() => import('~/components/audio/Player.vue')) import AudioPlayer from '~/components/audio/Player.vue'
const Sidebar = defineAsyncComponent(() => import('~/components/Sidebar.vue')) import Queue from '~/components/Queue.vue'
const Queue = defineAsyncComponent(() => import('~/components/Queue.vue')) import Sidebar from '~/ui/components/Sidebar.vue'
import ShortcutsModal from '~/ui/modals/Shortcuts.vue'
import LanguagesModal from '~/ui/modals/Language.vue'
import SearchModal from '~/ui/modals/Search.vue'
import UploadModal from '~/ui/modals/Upload.vue'
import Loader from '~/components/ui/Loader.vue'
// Fake content
onMounted(async () => {
await nextTick()
document.getElementById('fake-app')?.remove()
})
const logger = useLogger() const logger = useLogger()
logger.debug('App setup()') logger.debug('App setup()')
...@@ -28,7 +36,7 @@ logger.debug('App setup()') ...@@ -28,7 +36,7 @@ logger.debug('App setup()')
const store = useStore() const store = useStore()
// Tracks // Tracks
const { currentTrack, tracks } = useQueue() const { currentTrack } = useQueue()
const getTrackInformationText = (track: QueueTrack | undefined) => { const getTrackInformationText = (track: QueueTrack | undefined) => {
if (!track) { if (!track) {
return null return null
...@@ -50,10 +58,6 @@ watchEffect(() => { ...@@ -50,10 +58,6 @@ watchEffect(() => {
}) })
// Styles // Styles
const customStylesheets = computed(() => {
return store.state.instance.frontSettings.additionalStylesheets ?? []
})
useStyleTag(computed(() => store.state.instance.settings.ui.custom_css.value)) useStyleTag(computed(() => store.state.instance.settings.ui.custom_css.value))
// Fake content // Fake content
...@@ -68,12 +72,6 @@ useIntervalFn(() => { ...@@ -68,12 +72,6 @@ useIntervalFn(() => {
store.commit('ui/computeLastDate') store.commit('ui/computeLastDate')
}, 1000 * 60) }, 1000 * 60)
// Shortcuts
const [showShortcutsModal, toggleShortcutsModal] = useToggle(false)
onKeyboardShortcut('h', () => toggleShortcutsModal())
const { width } = useWindowSize()
// Fetch user data on startup // Fetch user data on startup
// NOTE: We're not checking if we're authenticated in the store, // NOTE: We're not checking if we're authenticated in the store,
// because we want to learn if we are authenticated at all // because we want to learn if we are authenticated at all
...@@ -81,50 +79,84 @@ store.dispatch('auth/fetchUser') ...@@ -81,50 +79,84 @@ store.dispatch('auth/fetchUser')
</script> </script>
<template> <template>
<div <div class="funkwhale responsive">
:key="store.state.instance.instanceUrl" <Sidebar style="grid-area: sidebar;" />
:class="{ <RouterView
'has-bottom-player': tracks.length > 0, v-slot="{ Component }"
'queue-focused': store.state.ui.queueFocused v-bind="color({}, ['default', 'solid'])()"
}" :class="$style.layout"
style="grid-area: main;"
> >
<!-- here, we display custom stylesheets, if any --> <Transition
<link v-if="Component"
v-for="url in customStylesheets" mode="out-in"
:key="url"
rel="stylesheet"
property="stylesheet"
:href="url"
> >
<KeepAlive :max="10">
<sidebar
:width="width"
@show:shortcuts-modal="toggleShortcutsModal"
/>
<service-messages />
<transition name="queue">
<queue v-show="store.state.ui.queueFocused" />
</transition>
<router-view v-slot="{ Component }">
<template v-if="Component">
<keep-alive :max="1">
<Suspense> <Suspense>
<component :is="Component" /> <component :is="Component" />
<template #fallback> <template #fallback>
<!-- TODO (wvffle): Add loader --> <Loader />
{{ $t('App.loading') }}
</template> </template>
</Suspense> </Suspense>
</keep-alive> </KeepAlive>
</template> </Transition>
</router-view> <transition name="queue">
<Queue v-show="store.state.ui.queueFocused" />
<audio-player /> </transition>
<playlist-modal v-if="store.state.auth.authenticated" /> </RouterView>
<channel-upload-modal v-if="store.state.auth.authenticated" />
<filter-modal v-if="store.state.auth.authenticated" />
<report-modal />
<shortcuts-modal v-model:show="showShortcutsModal" />
</div> </div>
<AudioPlayer
class="funkwhale"
v-bind="color({}, ['default', 'solid'])()"
/>
<ServiceMessages />
<LanguagesModal />
<ShortcutsModal />
<PlaylistModal v-if="store.state.auth.authenticated" />
<FilterModal v-if="store.state.auth.authenticated" />
<ReportModal />
<UploadModal v-if="store.state.auth.authenticated" />
<SearchModal />
</template> </template>
<style scoped>
.responsive {
display: grid !important;
grid-template-columns: 300px 1fr;
grid-template-rows: min-content auto 4rem;
min-height: 100vh;
grid-template-areas:
"sidebar sidebar"
"main main"
"player player";
@media screen and (min-width: 1024px) {
grid-template-areas:
"sidebar main"
"sidebar main"
"player player";
}
}
</style>
<style>
html, body {
background-color: var(--background-color);
}
/* Make inert pages (behind modals) unscrollable */
body:has(#app[inert="true"]) {
overflow: hidden;
}
</style>
<style module>
.layout {
padding: 32px;
/* Make space for the play bar */
padding-bottom: 5rem;
transition: all .3s;
}
</style>