Showing preview only (5,254K chars total). Download the full file or copy to clipboard to get everything.
Repository: FreeTubeApp/FreeTube
Branch: development
Commit: e7d2685692c9
Files: 446
Total size: 4.9 MB
Directory structure:
gitextract_w3ol9anw/
├── .babelrc
├── .editorconfig
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yaml
│ │ ├── config.yml
│ │ └── feature_request.yaml
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── auto-merge.yml
│ ├── dependabot.yml
│ ├── issue-labeler.yml
│ ├── pr-labeler.yml
│ └── workflows/
│ ├── autoMerge.yml
│ ├── build.yml
│ ├── calibreapp-image-actions.yml
│ ├── codeql.yml
│ ├── conflicts.yml
│ ├── flatpak.yml
│ ├── label-issue.yml
│ ├── label-pr.yml
│ ├── linter.yml
│ ├── no-response.yml
│ ├── release.yml
│ ├── remove-outdated-labels.yml
│ ├── stale.yml
│ └── updateSite.yml
├── .gitignore
├── .stylelintignore
├── .vscode/
│ ├── extensions.json
│ ├── launch.json
│ └── settings.json
├── .whitesource
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── _icons/
│ └── iconMac.icns
├── _scripts/
│ ├── ProcessLocalesPlugin.js
│ ├── _undefinedDefaultExport.mjs
│ ├── build.mjs
│ ├── clean.mjs
│ ├── dev-runner.js
│ ├── ebuilder.config.mjs
│ ├── eslint-rules/
│ │ ├── plugin.mjs
│ │ └── prefer-use-i18n-polyfill-rule.mjs
│ ├── findMissingTemplates.mjs
│ ├── getInstances.js
│ ├── getRegions.mjs
│ ├── getShakaLocales.js
│ ├── injectAllowedPaths.mjs
│ ├── mime-db-shrinking-loader.js
│ ├── patch-shaka-player-loader.js
│ ├── sigFrameConfig.js
│ ├── webpack.botGuardScript.config.js
│ ├── webpack.main.config.js
│ ├── webpack.preload.config.js
│ ├── webpack.renderer.config.js
│ └── webpack.web.config.js
├── eslint.config.mjs
├── jsconfig.json
├── lefthook-local.yml.example
├── lefthook.yml
├── package.json
├── src/
│ ├── botGuardScript.js
│ ├── constants.js
│ ├── data/
│ │ └── .gitkeep
│ ├── datastores/
│ │ ├── handlers/
│ │ │ ├── base.js
│ │ │ ├── electron.js
│ │ │ ├── index.js
│ │ │ └── web.js
│ │ └── index.js
│ ├── index.ejs
│ ├── main/
│ │ ├── ImageCache.js
│ │ ├── externalPlayer.js
│ │ ├── index.js
│ │ ├── poTokenGenerator.js
│ │ └── utils.js
│ ├── preload/
│ │ ├── interface.js
│ │ ├── main.js
│ │ └── preload-interface.d.ts
│ └── renderer/
│ ├── App.css
│ ├── App.vue
│ ├── components/
│ │ ├── ChannelAbout/
│ │ │ ├── ChannelAbout.css
│ │ │ └── ChannelAbout.vue
│ │ ├── ChannelDetails/
│ │ │ ├── ChannelDetails.css
│ │ │ └── ChannelDetails.vue
│ │ ├── ChannelHome/
│ │ │ ├── ChannelHome.css
│ │ │ └── ChannelHome.vue
│ │ ├── CommentSection/
│ │ │ ├── CommentSection.css
│ │ │ └── CommentSection.vue
│ │ ├── DataSettings/
│ │ │ ├── DataSettings.css
│ │ │ └── DataSettings.vue
│ │ ├── DistractionSettings/
│ │ │ ├── DistractionSettings.css
│ │ │ └── DistractionSettings.vue
│ │ ├── ExperimentalSettings/
│ │ │ ├── ExperimentalSettings.css
│ │ │ └── ExperimentalSettings.vue
│ │ ├── ExternalPlayerSettings.vue
│ │ ├── FtAgeRestricted/
│ │ │ ├── FtAgeRestricted.css
│ │ │ └── FtAgeRestricted.vue
│ │ ├── FtAutoGrid/
│ │ │ ├── FtAutoGrid.css
│ │ │ └── FtAutoGrid.vue
│ │ ├── FtAutoLoadNextPageWrapper.vue
│ │ ├── FtButton/
│ │ │ ├── FtButton.css
│ │ │ └── FtButton.vue
│ │ ├── FtChannelBubble/
│ │ │ ├── FtChannelBubble.css
│ │ │ └── FtChannelBubble.vue
│ │ ├── FtCheckboxList/
│ │ │ ├── FtCheckboxList.css
│ │ │ └── FtCheckboxList.vue
│ │ ├── FtCommunityPoll/
│ │ │ ├── FtCommunityPoll.css
│ │ │ └── FtCommunityPoll.vue
│ │ ├── FtCommunityPost/
│ │ │ ├── FtCommunityPost.scss
│ │ │ └── FtCommunityPost.vue
│ │ ├── FtCreatePlaylistPrompt/
│ │ │ ├── FtCreatePlaylistPrompt.css
│ │ │ └── FtCreatePlaylistPrompt.vue
│ │ ├── FtElementList/
│ │ │ ├── FtElementList.css
│ │ │ └── FtElementList.vue
│ │ ├── FtIconButton/
│ │ │ ├── FtIconButton.scss
│ │ │ └── FtIconButton.vue
│ │ ├── FtInput/
│ │ │ ├── FtInput.css
│ │ │ └── FtInput.vue
│ │ ├── FtInputTags/
│ │ │ ├── FtInputTags.css
│ │ │ └── FtInputTags.vue
│ │ ├── FtKeyboardShortcutPrompt/
│ │ │ ├── FtKeyboardShortcutPrompt.css
│ │ │ └── FtKeyboardShortcutPrompt.vue
│ │ ├── FtListChannel/
│ │ │ ├── FtListChannel.scss
│ │ │ └── FtListChannel.vue
│ │ ├── FtListHashtag/
│ │ │ ├── FtListHashtag.scss
│ │ │ └── FtListHashtag.vue
│ │ ├── FtListLazyWrapper/
│ │ │ ├── FtListLazyWrapper.css
│ │ │ └── FtListLazyWrapper.vue
│ │ ├── FtListPlaylist/
│ │ │ ├── FtListPlaylist.scss
│ │ │ └── FtListPlaylist.vue
│ │ ├── FtListVideoLazy.vue
│ │ ├── FtListVideoNumbered/
│ │ │ ├── FtListVideoNumbered.css
│ │ │ └── FtListVideoNumbered.vue
│ │ ├── FtLoader/
│ │ │ ├── FtLoader.css
│ │ │ └── FtLoader.vue
│ │ ├── FtLogoFull/
│ │ │ ├── FtLogoFull.css
│ │ │ └── FtLogoFull.vue
│ │ ├── FtNotificationBanner/
│ │ │ ├── FtNotificationBanner.css
│ │ │ └── FtNotificationBanner.vue
│ │ ├── FtPlaylistAddVideoPrompt/
│ │ │ ├── FtPlaylistAddVideoPrompt.css
│ │ │ └── FtPlaylistAddVideoPrompt.vue
│ │ ├── FtPlaylistSelector/
│ │ │ ├── FtPlaylistSelector.scss
│ │ │ └── FtPlaylistSelector.vue
│ │ ├── FtProfileBubble/
│ │ │ ├── FtProfileBubble.css
│ │ │ └── FtProfileBubble.vue
│ │ ├── FtProfileChannelList/
│ │ │ ├── FtProfileChannelList.css
│ │ │ └── FtProfileChannelList.vue
│ │ ├── FtProfileEdit/
│ │ │ ├── FtProfileEdit.css
│ │ │ └── FtProfileEdit.vue
│ │ ├── FtProfileFilterChannelsList/
│ │ │ ├── FtProfileFilterChannelsList.css
│ │ │ └── FtProfileFilterChannelsList.vue
│ │ ├── FtProfileSelector/
│ │ │ ├── FtProfileSelector.css
│ │ │ └── FtProfileSelector.vue
│ │ ├── FtProgressBar/
│ │ │ ├── FtProgressBar.css
│ │ │ └── FtProgressBar.vue
│ │ ├── FtPrompt/
│ │ │ ├── FtPrompt.css
│ │ │ └── FtPrompt.vue
│ │ ├── FtRadioButton/
│ │ │ ├── FtRadioButton.css
│ │ │ └── FtRadioButton.vue
│ │ ├── FtRefreshWidget/
│ │ │ ├── FtRefreshWidget.scss
│ │ │ └── FtRefreshWidget.vue
│ │ ├── FtSearchFilters/
│ │ │ ├── FtSearchFilters.css
│ │ │ └── FtSearchFilters.vue
│ │ ├── FtSelect/
│ │ │ ├── FtSelect.css
│ │ │ └── FtSelect.vue
│ │ ├── FtSettingsMenu/
│ │ │ ├── FtSettingsMenu.css
│ │ │ └── FtSettingsMenu.vue
│ │ ├── FtSettingsSection/
│ │ │ ├── FtSettingsSection.scss
│ │ │ └── FtSettingsSection.vue
│ │ ├── FtShareButton/
│ │ │ ├── FtShareButton.css
│ │ │ └── FtShareButton.vue
│ │ ├── FtSlider/
│ │ │ ├── FtSlider.css
│ │ │ └── FtSlider.vue
│ │ ├── FtSponsorBlockCategory/
│ │ │ ├── FtSponsorBlockCategory.css
│ │ │ └── FtSponsorBlockCategory.vue
│ │ ├── FtSubscribeButton/
│ │ │ ├── FtSubscribeButton.css
│ │ │ └── FtSubscribeButton.vue
│ │ ├── FtTimestampCatcher.vue
│ │ ├── FtToast/
│ │ │ ├── FtToast.css
│ │ │ └── FtToast.vue
│ │ ├── FtToggleSwitch/
│ │ │ ├── FtToggleSwitch.scss
│ │ │ └── FtToggleSwitch.vue
│ │ ├── FtTooltip/
│ │ │ ├── FtTooltip.css
│ │ │ └── FtTooltip.vue
│ │ ├── GeneralSettings/
│ │ │ ├── GeneralSettings.css
│ │ │ └── GeneralSettings.vue
│ │ ├── ParentalControlSettings.vue
│ │ ├── PasswordDialog/
│ │ │ ├── PasswordDialog.css
│ │ │ └── PasswordDialog.vue
│ │ ├── PasswordSettings/
│ │ │ ├── PasswordSettings.css
│ │ │ └── PasswordSettings.vue
│ │ ├── PlayerSettings/
│ │ │ ├── PlayerSettings.css
│ │ │ └── PlayerSettings.vue
│ │ ├── PlaylistInfo/
│ │ │ ├── PlaylistInfo.scss
│ │ │ └── PlaylistInfo.vue
│ │ ├── PrivacySettings.vue
│ │ ├── ProxySettings/
│ │ │ ├── ProxySettings.css
│ │ │ └── ProxySettings.vue
│ │ ├── SideNav/
│ │ │ ├── SideNav.css
│ │ │ └── SideNav.vue
│ │ ├── SideNavMoreOptions/
│ │ │ ├── SideNavMoreOptions.css
│ │ │ └── SideNavMoreOptions.vue
│ │ ├── SponsorBlockSettings.vue
│ │ ├── SubscriptionSettings/
│ │ │ ├── SubscriptionSettings.css
│ │ │ └── SubscriptionSettings.vue
│ │ ├── SubscriptionsLive.vue
│ │ ├── SubscriptionsPosts.vue
│ │ ├── SubscriptionsShorts.vue
│ │ ├── SubscriptionsTabUi/
│ │ │ ├── SubscriptionsTabUi.css
│ │ │ └── SubscriptionsTabUi.vue
│ │ ├── SubscriptionsVideos.vue
│ │ ├── ThemeSettings.vue
│ │ ├── TopNav/
│ │ │ ├── TopNav.scss
│ │ │ └── TopNav.vue
│ │ ├── WatchVideoChapters/
│ │ │ ├── WatchVideoChapters.css
│ │ │ └── WatchVideoChapters.vue
│ │ ├── WatchVideoDescription/
│ │ │ ├── WatchVideoDescription.css
│ │ │ └── WatchVideoDescription.vue
│ │ ├── WatchVideoInfo/
│ │ │ ├── WatchVideoInfo.css
│ │ │ └── WatchVideoInfo.vue
│ │ ├── WatchVideoLiveChat/
│ │ │ ├── WatchVideoLiveChat.css
│ │ │ └── WatchVideoLiveChat.vue
│ │ ├── WatchVideoRecommendations/
│ │ │ ├── WatchVideoRecommendations.css
│ │ │ └── WatchVideoRecommendations.vue
│ │ ├── ft-card/
│ │ │ ├── ft-card.css
│ │ │ ├── ft-card.js
│ │ │ └── ft-card.vue
│ │ ├── ft-flex-box/
│ │ │ ├── ft-flex-box.css
│ │ │ ├── ft-flex-box.js
│ │ │ └── ft-flex-box.vue
│ │ ├── ft-list-video/
│ │ │ ├── ft-list-video.js
│ │ │ ├── ft-list-video.scss
│ │ │ └── ft-list-video.vue
│ │ ├── ft-shaka-video-player/
│ │ │ ├── ft-shaka-video-player.css
│ │ │ ├── ft-shaka-video-player.js
│ │ │ ├── ft-shaka-video-player.vue
│ │ │ └── player-components/
│ │ │ ├── AudioTrackSelection.js
│ │ │ ├── AutoplayToggle.js
│ │ │ ├── FullWindowButton.js
│ │ │ ├── LegacyQualitySelection.js
│ │ │ ├── ScreenshotButton.js
│ │ │ ├── SkipButton.js
│ │ │ ├── StatsButton.js
│ │ │ └── TheatreModeButton.js
│ │ └── watch-video-playlist/
│ │ ├── watch-video-playlist.css
│ │ ├── watch-video-playlist.js
│ │ └── watch-video-playlist.vue
│ ├── composables/
│ │ ├── colors.js
│ │ └── use-i18n-polyfill.js
│ ├── directives/
│ │ └── vSaferHtml.js
│ ├── fontawesome-minimal.js
│ ├── helpers/
│ │ ├── api/
│ │ │ ├── PlayerCache.js
│ │ │ ├── invidious.js
│ │ │ └── local.js
│ │ ├── channels.js
│ │ ├── colors.js
│ │ ├── player/
│ │ │ ├── EbmlParser.js
│ │ │ ├── Mp4SegmentIndexParser.js
│ │ │ ├── SabrManifestParser.js
│ │ │ ├── SabrSchemePlugin.js
│ │ │ ├── WebmSegmentIndexParser.js
│ │ │ └── utils.js
│ │ ├── playlists.js
│ │ ├── sponsorblock.js
│ │ ├── strings.js
│ │ ├── subscriptions.js
│ │ └── utils.js
│ ├── i18n/
│ │ └── index.js
│ ├── main.js
│ ├── router/
│ │ └── index.js
│ ├── scss-partials/
│ │ ├── _ft-list-item.scss
│ │ └── _utils.scss
│ ├── sigFrameScript.js
│ ├── store/
│ │ ├── index.js
│ │ └── modules/
│ │ ├── history.js
│ │ ├── invidious.js
│ │ ├── player.js
│ │ ├── playlists.js
│ │ ├── profiles.js
│ │ ├── search-history.js
│ │ ├── settings.js
│ │ ├── subscription-cache.js
│ │ └── utils.js
│ ├── themes.css
│ └── views/
│ ├── About/
│ │ ├── About.css
│ │ └── About.vue
│ ├── Channel/
│ │ ├── Channel.css
│ │ └── Channel.vue
│ ├── Hashtag/
│ │ ├── Hashtag.css
│ │ └── Hashtag.vue
│ ├── History/
│ │ ├── History.css
│ │ └── History.vue
│ ├── Playlist/
│ │ ├── Playlist.scss
│ │ └── Playlist.vue
│ ├── Popular/
│ │ ├── Popular.css
│ │ └── Popular.vue
│ ├── Post.vue
│ ├── ProfileSettings/
│ │ ├── ProfileSettings.css
│ │ └── ProfileSettings.vue
│ ├── SearchPage/
│ │ ├── SearchPage.css
│ │ └── SearchPage.vue
│ ├── Settings/
│ │ ├── Settings.css
│ │ └── Settings.vue
│ ├── SubscribedChannels/
│ │ ├── SubscribedChannels.css
│ │ └── SubscribedChannels.vue
│ ├── Subscriptions/
│ │ ├── Subscriptions.css
│ │ └── Subscriptions.vue
│ ├── Trending/
│ │ ├── Trending.css
│ │ └── Trending.vue
│ ├── UserPlaylists/
│ │ ├── UserPlaylists.css
│ │ └── UserPlaylists.vue
│ └── Watch/
│ ├── Watch.js
│ ├── Watch.scss
│ └── Watch.vue
├── static/
│ ├── .gitkeep
│ ├── external-player-map.json
│ ├── geolocations/
│ │ ├── ar.json
│ │ ├── be.json
│ │ ├── bg.json
│ │ ├── ca.json
│ │ ├── cs.json
│ │ ├── da.json
│ │ ├── de-DE.json
│ │ ├── el.json
│ │ ├── en-GB.json
│ │ ├── en-US.json
│ │ ├── es-AR.json
│ │ ├── es-MX.json
│ │ ├── es.json
│ │ ├── et.json
│ │ ├── eu.json
│ │ ├── fa.json
│ │ ├── fi.json
│ │ ├── fr-FR.json
│ │ ├── gl.json
│ │ ├── he.json
│ │ ├── hr.json
│ │ ├── hu.json
│ │ ├── id.json
│ │ ├── is.json
│ │ ├── it.json
│ │ ├── ja.json
│ │ ├── ko.json
│ │ ├── lt.json
│ │ ├── nb-NO.json
│ │ ├── nl.json
│ │ ├── nn.json
│ │ ├── pl.json
│ │ ├── pt-BR.json
│ │ ├── pt-PT.json
│ │ ├── pt.json
│ │ ├── ro.json
│ │ ├── ru.json
│ │ ├── sk.json
│ │ ├── sl.json
│ │ ├── sr.json
│ │ ├── sv.json
│ │ ├── ta.json
│ │ ├── tr.json
│ │ ├── uk.json
│ │ ├── vi.json
│ │ ├── zh-CN.json
│ │ └── zh-TW.json
│ ├── invidious-instances.json
│ ├── locales/
│ │ ├── activeLocales.json
│ │ ├── af.yaml
│ │ ├── ar.yaml
│ │ ├── as.yaml
│ │ ├── awa.yaml
│ │ ├── az.yaml
│ │ ├── be.yaml
│ │ ├── bg.yaml
│ │ ├── bn.yaml
│ │ ├── br.yaml
│ │ ├── bs.yaml
│ │ ├── ca.yaml
│ │ ├── ckb.yaml
│ │ ├── cs.yaml
│ │ ├── cy.yaml
│ │ ├── da.yaml
│ │ ├── de-DE.yaml
│ │ ├── el.yaml
│ │ ├── en-GB.yaml
│ │ ├── en-US.yaml
│ │ ├── eo.yaml
│ │ ├── es-AR.yaml
│ │ ├── es-MX.yaml
│ │ ├── es.yaml
│ │ ├── et.yaml
│ │ ├── eu.yaml
│ │ ├── fa.yaml
│ │ ├── fi.yaml
│ │ ├── fil.yaml
│ │ ├── fr-FR.yaml
│ │ ├── gl.yaml
│ │ ├── gsw.yaml
│ │ ├── he.yaml
│ │ ├── hi.yaml
│ │ ├── hr.yaml
│ │ ├── hu.yaml
│ │ ├── id.yaml
│ │ ├── is.yaml
│ │ ├── it.yaml
│ │ ├── ja.yaml
│ │ ├── ka.yaml
│ │ ├── km.yaml
│ │ ├── ko.yaml
│ │ ├── ku.yaml
│ │ ├── la.yaml
│ │ ├── lt.yaml
│ │ ├── lv.yaml
│ │ ├── ms.yaml
│ │ ├── my.yaml
│ │ ├── nb-NO.yaml
│ │ ├── ne.yaml
│ │ ├── nl.yaml
│ │ ├── nn.yaml
│ │ ├── or.yaml
│ │ ├── pl.yaml
│ │ ├── pt-BR.yaml
│ │ ├── pt-PT.yaml
│ │ ├── pt.yaml
│ │ ├── ro.yaml
│ │ ├── ru.yaml
│ │ ├── sat.yaml
│ │ ├── si.yaml
│ │ ├── sk.yaml
│ │ ├── sl.yaml
│ │ ├── sm.yaml
│ │ ├── sq.yaml
│ │ ├── sr.yaml
│ │ ├── sv.yaml
│ │ ├── ta.yaml
│ │ ├── ti.yaml
│ │ ├── tok.yaml
│ │ ├── tr.yaml
│ │ ├── uk.yaml
│ │ ├── ur.yaml
│ │ ├── uz.yaml
│ │ ├── vi.yaml
│ │ ├── vls.yaml
│ │ ├── xh.yaml
│ │ ├── zh-CN.yaml
│ │ └── zh-TW.yaml
│ ├── manifest.json
│ └── pwabuilder-sw.js
└── stylelint.config.mjs
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"presets": [
[
"@babel/env",
{
"targets": {
"chrome": "130",
"node": "20.9.0"
}
}
]
]
}
================================================
FILE: .editorconfig
================================================
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yaml
================================================
name: Bug Report
description: Report an issue or unexpected behavior that occurs within the application
title: "[Bug]: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
**README: Before You Submit Your Issue**
- Issues are not a place to go ask support questions or start discussions. Please ask support questions or start discussions on the [discussions page](https://github.com/FreeTubeApp/FreeTube/discussions).
- type: checkboxes
attributes:
label: Guidelines
description: Please ensure you've completed all of the following.
options:
- label: I have encountered this bug in the [latest release of FreeTube](https://github.com/FreeTubeApp/FreeTube/releases).
required: true
- label: I have encountered this bug in the [official downloads of FreeTube](https://github.com/FreeTubeApp/FreeTube#official-downloads).
required: true
- label: I have [searched the issue tracker for open and closed issues](https://github.com/FreeTubeApp/FreeTube/issues?q=is%3Aissue+sort%3Arelevance-desc) that are similar to the bug report I want to file, without success.
required: true
- label: I have searched the [documentation](https://docs.freetubeapp.io/) for information that matches the description of the bug I want to file, without success.
required: true
- label: This issue contains only one bug.
required: true
- label: I have read and agree to follow the [rules](https://docs.freetubeapp.io/community/rules/).
required: true
- type: textarea
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: dropdown
attributes:
label: 'Issue Labels'
description: Please select a label that fits this bug report. Choose multiple, if applicable.
multiple: true
options:
- accessibility issue
- API issue
- causes crash
- content not loading
- data loss
- feature stopped working
- inconsistent behavior
- keyboard control not working
- only happens in developer mode
- race condition
- text/string issue
- usability issue
- visual bug
validations:
required: true
- type: input
attributes:
label: FreeTube Version
description: |
If using releases, enter the version.
If using nightly builds, enter commit hash or build number, you can find it via about page in the FreeTube application.
placeholder: v0.14.0, 8c4278
validations:
required: true
- type: input
attributes:
label: Operating System Version
description: What operating system version are you using? On Windows, click Start button > Settings > System > About. On macOS, click the Apple Menu > About This Mac. On Linux, use lsb_release or uname -a.
placeholder: "e.g. Windows 10 version 1909, macOS Catalina 10.15.7, or Ubuntu 20.04"
validations:
required: true
- type: dropdown
attributes:
label: Installation Method
description: When you select an unofficial installation method, you must have verified that the bug is also present in one of the official installation methods. Please make sure you uninstall the unofficial installation before installing one of the official installations. If you can't reproduce this in one of the official installation methods, you should report the bug to the maintainer of the unofficial installation method you used.
options:
- .AppImage
- .deb
- .dmg
- .exe
- Flathub
- .pacman
- Portable
- .rpm
- .zip / .7z
- .apk (FreeTubeAndroid Unofficial)
- .apk (Alpine Linux Package Unofficial)
- AUR (Unofficial)
- Chocolatey (Unofficial)
- Homebrew (Unofficial)
- Nix (Unofficial)
- PortableApps (Unofficial)
- Scoop (Unofficial)
- Snapcraft (Unofficial)
- WAPT (Unofficial)
- winget (Unofficial)
- other
validations:
required: true
- type: dropdown
attributes:
label: Primary API used
description: What is the primary API being used?
multiple: false
options:
- Local API
- Invidious API
validations:
required: true
- type: input
attributes:
label: 'Last Known Working FreeTube Version (If Any)'
description: What is the last version of FreeTube this worked in, if applicable?
placeholder: v0.14.0
- type: upload
id: screenshots
attributes:
label: Upload screenshots or videos
description: If applicable, add screenshots or videos to help explain your problem.
validations:
required: false
- type: textarea
attributes:
label: Additional Information
description: |
Add additional information here.
You may drag-and-drop log files here, or paste the log file in code blocks.
- type: checkboxes
attributes:
label: Nightly Build
description: Please ensure you've completed the following, if applicable.
options:
- label: I have encountered this bug in the latest [nightly build](https://docs.freetubeapp.io/development/nightly-builds).
required: false
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: Discussions
url: https://github.com/FreeTubeApp/FreeTube/discussions/categories/general
about: View discussions or start one yourself
- name: Questions
url: https://github.com/FreeTubeApp/FreeTube/discussions/categories/q-a
about: Ask and answer questions
- name: Matrix Community
url: https://matrix.to/#/#freetube:matrix.org
about: 'Join our Matrix chatroom - "Note: Bugs and Feature requests should be made on GitHub and not in the Matrix room"'
- name: Translate FreeTube
url: https://hosted.weblate.org/engage/free-tube/
about: Help translate FreeTube on Weblate
- name: FreeTube Documentation
url: https://docs.freetubeapp.io/
about: View the Documentation to find all relevant information about FreeTube
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yaml
================================================
name: Feature request
description: Suggest an idea for FreeTube which you would like to see in a future release
title: "[Feature Request]: "
labels: "enhancement"
body:
- type: markdown
attributes:
value: |
**README: Before You Submit Your Issue**
- Issues are not a place to go ask support questions or start discussions. Please ask support questions or start discussions on the [discussions page](https://github.com/FreeTubeApp/FreeTube/discussions).
- type: checkboxes
attributes:
label: Guidelines
description: Please ensure you've completed all of the following.
options:
- label: I have [searched the issue tracker for open and closed issues](https://github.com/FreeTubeApp/FreeTube/issues?q=is%3Aissue+sort%3Arelevance-desc) that are similar to the feature request I want to file, without success.
required: true
- label: I have searched the [documentation](https://docs.freetubeapp.io/) for information that matches the description of the feature request I want to file, without success.
required: true
- label: This issue contains only one feature request.
required: true
- label: I have read and agree to follow the [rules](https://docs.freetubeapp.io/community/rules/).
required: true
- type: textarea
attributes:
label: Problem Description
description: Please add a clear and concise description of the problem you are seeking to solve with this feature request.
validations:
required: true
- type: textarea
attributes:
label: Proposed Solution
description: Describe the solution you'd like in a clear and concise manner.
validations:
required: true
- type: textarea
attributes:
label: Alternatives Considered
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: true
- type: dropdown
attributes:
label: 'Issue Labels'
description: Please select a label that fits this feature request. Choose multiple, if applicable.
multiple: true
options:
- display more information to user
- ease of use improvement
- improvement to existing feature
- new feature
- new keyboard shortcut
- new optional setting
- support for external software
- visual improvement
validations:
required: true
- type: input
attributes:
label: FreeTube Version
description: |
If using releases, enter the version.
If using nightly builds, enter commit hash or build number, you can find it via about page in the FreeTube application.
placeholder: v0.14.0, 8c4278
validations:
required: true
- type: input
attributes:
label: Operating System Version
description: What operating system version are you using? On Windows, click Start button > Settings > System > About. On macOS, click the Apple Menu > About This Mac. On Linux, use lsb_release or uname -a.
placeholder: "e.g. Windows 10 version 1909, macOS Catalina 10.15.7, or Ubuntu 20.04"
validations:
required: true
- type: dropdown
attributes:
label: Installation Method
description: If you are using an unofficial installation method, please verify that the requested feature is not already available in one of the official installation methods. If the feature only relates to an unofficial installation, please direct the request to the maintainer of that installation method instead.
options:
- .AppImage
- .deb
- .dmg
- .exe
- Flathub
- .pacman
- Portable
- .rpm
- .zip / .7z
- .apk (FreeTubeAndroid Unofficial)
- .apk (Alpine Linux Package Unofficial)
- AUR (Unofficial)
- Chocolatey (Unofficial)
- Homebrew (Unofficial)
- Nix (Unofficial)
- PortableApps (Unofficial)
- Scoop (Unofficial)
- Snapcraft (Unofficial)
- WAPT (Unofficial)
- winget (Unofficial)
- other
validations:
required: true
- type: upload
id: screenshots
attributes:
label: Upload screenshots or videos
description: If applicable, add screenshots or videos to help explain your request.
validations:
required: false
- type: textarea
attributes:
label: Additional Information
description: |
Add any other context about the feature request here.
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
<!-- Thanks for sending a pull request! Make sure to follow the contributing guidelines. -->
<!-- Important note, we may remove your pull request if you do not use this provided PR template correctly. -->
<!-- Do not create PR's with AI! (PRs created mainly with AI will be closed. They waste our team's time. We ban repeat offenders.) -->
## Pull Request Type
<!-- Please select what type of pull request this is: [x] -->
- [ ] Bugfix
- [ ] Feature Implementation
- [ ] Documentation
- [ ] Other
## Related issue
<!-- Please link the issue your pull request is referring to. -->
<!-- If this pull request fully resolves the relevant issue, put "closes" before the issue number. -->
<!-- Example: "closes #123456". -->
## Description
<!-- Please write a clear and concise description of what the pull request does. -->
## Screenshots <!-- If appropriate -->
<!-- Please add before and after screenshots if there is a visible change. -->
## Testing
<!-- How can reviewers verify that the PR produces correct results? -->
<!-- Please provide instructions so that others can ensure that your pull request would produce correct results. For examples see, https://github.com/FreeTubeApp/FreeTube/pull/5743, https://github.com/FreeTubeApp/FreeTube/pull/7349, https://github.com/FreeTubeApp/FreeTube/pull/5125, https://github.com/FreeTubeApp/FreeTube/pull/7338 -->
## Desktop
<!-- Please complete the following information-->
- **OS:**
- **OS Version:**
- **FreeTube version:**
## Additional context
<!-- Add any other context about the pull request here. -->
================================================
FILE: .github/auto-merge.yml
================================================
minApprovals:
COLLABORATOR: 2
maxRequestedChanges:
COLLABORATOR: 0
mergeMethod: squash
requiredBaseBranches:
- development
- test
reportStatus: true
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
labels:
- "PR: waiting for review"
- "PR: dependencies"
open-pull-requests-limit: 15
groups:
babel:
patterns:
- "@babel/*"
- "babel-*"
eslint:
patterns:
- "eslint"
- "eslint-*"
- "@eslint/*"
- "vue-eslint-parser"
- "neostandard"
- "@intlify/eslint-plugin-vue-i18n"
- "@stylistic/eslint-plugin"
stylelint:
patterns:
- "stylelint"
- "stylelint-*"
- "postcss"
- "postcss-*"
- "@double-great/stylelint-a11y"
fortawesome:
patterns:
- "@fortawesome/*"
webpack:
patterns:
- "css-loader"
- "mini-css-extract-plugin"
- "sass"
- "sass-loader"
- "webpack"
- "webpack-*"
- "*-webpack-plugin"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
labels:
- "PR: waiting for review"
- "PR: dependencies"
================================================
FILE: .github/issue-labeler.yml
================================================
'B: visual':
- '(visual bug)'
'B: Unofficial Download':
- '(AUR \(Unofficial\)|Chocolatey \(Unofficial\)|\.apk \(FreeTubeAndroid Unofficial\)|Homebrew \(Unofficial\)|PortableApps \(Unofficial\)|WAPT \(Unofficial\)|winget \(Unofficial\)|Scoop \(Unofficial\)|Snapcraft \(Unofficial\)|Nix \(Unofficial\))'
'B: keyboard control':
- '(keyboard control not working)'
'B: text/string':
- '(text/string issue)'
'B: content not loading':
- '(content not loading)'
'B: accessibility':
- '(accessibility issue)'
'B: usability':
- '(usability issue)'
'B: crash':
- '(causes crash)'
'B: feature stopped working':
- '(feature stopped working)'
'B: inconsistent behavior':
- '(inconsistent behavior)'
'B: data loss':
- '(data loss)'
'B: race condition':
- '(race condition)'
'B: API issue':
- '(API issue)'
'B: developer mode':
- '(only happens in developer mode)'
'E: improvement existing feature':
- '(improvement to existing feature)'
'E: new optional setting':
- '(new optional setting)'
'E: visual improvement':
- '(visual improvement)'
'E: display more information':
- '(display more information to user)'
'E: ease of use improvement':
- '(ease of use improvement)'
'E: support external software':
- '(support for external software)'
'E: new feature':
- '(new feature)'
'E: keyboard shortcut':
- '(new keyboard shortcut)'
================================================
FILE: .github/pr-labeler.yml
================================================
'PR: waiting for review':
- changed-files:
- any-glob-to-any-file: '**'
'PR: dependencies':
- any:
- changed-files:
- any-glob-to-any-file: ['yarn.lock', 'package.json']
================================================
FILE: .github/workflows/autoMerge.yml
================================================
name: Auto Merge PR
on:
pull_request_target:
types: [opened, reopened, auto_merge_disabled, ready_for_review]
permissions: {}
jobs:
build:
if: |
github.event.pull_request.state == 'open' &&
!github.event.pull_request.draft &&
(contains(github.event.pull_request.base.ref, 'development') || contains(github.event.pull_request.base.ref, 'RC'))
runs-on: ubuntu-slim
permissions:
contents: write
steps:
- name: Auto Merge PR
shell: bash
env:
GH_TOKEN: ${{ secrets.PUSH_TOKEN }}
PR_URL: ${{ github.event.pull_request.html_url }}
run: gh pr merge "$PR_URL" --auto --squash
================================================
FILE: .github/workflows/build.yml
================================================
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Build
on:
push:
branches: [ master, development, '**-RC' ]
workflow_dispatch:
permissions: {}
jobs:
build:
# As this action runs on every push to the default branch and uses quite a lot of resources (both minutes and caches),
# only run it in the FreeTubeApp/FreeTube repository to avoid unnecessary GitHub Actions usage/billing in forks.
# Still allow manually triggering the workflow.
# If a fork does need this workflow to run on every push to the default branch, they can change this condition in their fork to include their repository.
if: github.repository == 'FreeTubeApp/FreeTube' || github.event_name == 'workflow_dispatch'
permissions:
actions: write
contents: read
strategy:
matrix:
node-version: [24.x]
runtime:
- linux-x64
- linux-armv7l
- linux-arm64
- win-x64
- win-arm64
- osx-x64
- osx-arm64
include:
- runtime: linux-x64
os: ubuntu-latest
- runtime: linux-armv7l
os: ubuntu-latest
- runtime: linux-arm64
os: ubuntu-latest
- runtime: osx-x64
os: macOS-latest
- runtime: osx-arm64
os: macOS-latest
- runtime: win-x64
os: windows-latest
- runtime: win-arm64
os: windows-latest
runs-on: ${{ matrix.os }}
steps:
- name: 'Use faster D: drive for yarn cache on Windows'
if: startsWith(matrix.os, 'windows')
shell: cmd
run: yarn config set cache-folder D:\ft_yarn_cache
- name: Get yarn cache directory
id: cache_dir
shell: bash
run: |
{
echo 'cache_dir<<EOF'
yarn cache dir
echo EOF
} >> "$GITHUB_OUTPUT"
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f #v6.3.0
with:
node-version: ${{ matrix.node-version }}
package-manager-cache: false
- name: Restore yarn cache
id: restore_cache
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3
with:
key: ${{ format('node-cache-{0}-{1}-yarn-{2}', runner.os, runner.arch, hashFiles('yarn.lock')) }}
path: ${{ steps.cache_dir.outputs.cache_dir }}
- run: yarn run ci
shell: bash
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: '1'
- run: yarn run lint
shell: bash
- name: Set version number
id: versionNumber
shell: bash
run: |
original_version="$(jq -r '.version' package.json)"
version="$original_version"
lower_case_ref="$(echo -E "$GITHUB_REF" | tr '[:upper:]' '[:lower:]')"
if [[ "$lower_case_ref" == *development* ]]; then
version="${original_version}-nightly-${GITHUB_RUN_NUMBER}"
elif [[ "$lower_case_ref" == *rc* ]]; then
version="${original_version}-RC-${GITHUB_RUN_NUMBER}"
fi
package_json="$(jq --arg version "$version" '.version = $version' package.json)"
echo -E "$package_json" > package.json
echo "version=${version}" >> "$GITHUB_OUTPUT"
- name: Install libarchive-tools
if: startsWith(matrix.os, 'ubuntu')
shell: bash
run: |
sudo apt update
sudo apt -y install libarchive-tools
- name: Build x64 with Node.js ${{ matrix.node-version}}
if: contains(matrix.runtime, 'x64')
shell: bash
run: yarn run build
- name: Build ARMv7l with Node.js ${{ matrix.node-version}}
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
shell: bash
run: yarn run build:arm32
- name: Build ARM64 with Node.js ${{ matrix.node-version}}
if: contains(matrix.runtime, 'arm64')
shell: bash
run: yarn run build:arm64
- name: Convert X64 AppImage to static runtime
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
shell: bash
env:
VERSION: ${{ steps.versionNumber.outputs.version }}
run: |
sudo apt install desktop-file-utils
cd build
appimage="FreeTube-${VERSION}.AppImage"
wget "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage" -O ./appimagetool.AppImage
chmod +x ./"$appimage" ./appimagetool.AppImage
./"$appimage" --appimage-extract && rm -f ./"$appimage"
./appimagetool.AppImage --comp zstd --mksquashfs-opt -Xcompression-level --mksquashfs-opt 20 \
-n ./squashfs-root ./"$appimage"
rm -rf ./squashfs-root ./appimagetool.AppImage
- name: Upload Linux .zip x64 Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}-linux-x64-portable.zip
path: build/freetube-${{ steps.versionNumber.outputs.version }}.zip
- name: Upload Linux .7z x64 Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}-linux-x64-portable.7z
path: build/freetube-${{ steps.versionNumber.outputs.version }}.7z
- name: Upload Linux .zip ARMv7l Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}-linux-armv7l-portable.zip
path: build/freetube-${{ steps.versionNumber.outputs.version }}-armv7l.zip
- name: Upload Linux .7z ARMv7l Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}-linux-armv7l-portable.7z
path: build/freetube-${{ steps.versionNumber.outputs.version }}-armv7l.7z
- name: Upload Linux .zip ARM64 Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}-linux-arm64-portable.zip
path: build/freetube-${{ steps.versionNumber.outputs.version }}-arm64.zip
- name: Upload Linux .7z ARM64 Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}-linux-arm64-portable.7z
path: build/freetube-${{ steps.versionNumber.outputs.version }}-arm64.7z
- name: Upload .deb x64 Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
with:
name: freetube_${{ steps.versionNumber.outputs.version }}_amd64.deb
path: build/freetube_${{ steps.versionNumber.outputs.version }}_amd64.deb
- name: Upload .deb ARMv7l Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
with:
name: freetube_${{ steps.versionNumber.outputs.version }}_armv7l.deb
path: build/freetube_${{ steps.versionNumber.outputs.version }}_armv7l.deb
- name: Upload .deb ARM64 Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
with:
name: freetube_${{ steps.versionNumber.outputs.version }}_arm64.deb
path: build/freetube_${{ steps.versionNumber.outputs.version }}_arm64.deb
- name: Upload AppImage x64 Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}-amd64.AppImage
path: build/FreeTube-${{ steps.versionNumber.outputs.version }}.AppImage
- name: Upload AppImage ARMv7l Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}-armv7l.AppImage
path: build/FreeTube-${{ steps.versionNumber.outputs.version }}-armv7l.AppImage
- name: Upload AppImage ARM64 Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}-arm64.AppImage
path: build/FreeTube-${{ steps.versionNumber.outputs.version }}-arm64.AppImage
- name: Upload .rpm x64 Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}.amd64.rpm
path: build/freetube-${{ steps.versionNumber.outputs.version }}.x86_64.rpm
# rpm are not built for armv7l
- name: Upload .rpm ARM64 Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}.arm64.rpm
path: build/freetube-${{ steps.versionNumber.outputs.version }}.aarch64.rpm
- name: Upload Pacman .pacman x64 Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}-amd64.pacman
path: build/freetube-${{ steps.versionNumber.outputs.version }}.pacman
- name: Upload Windows x64 .exe Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}-setup-x64.exe
path: build/freetube Setup ${{ steps.versionNumber.outputs.version }}.exe
- name: Upload Windows x64 Portable Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}-win-x64-portable.exe
path: build/freetube ${{ steps.versionNumber.outputs.version }}.exe
- name: Upload Windows x64 .zip Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}-win-x64-portable.zip
path: build/freetube-${{ steps.versionNumber.outputs.version }}-win.zip
- name: Upload Windows x64 .7z Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}-win-x64-portable.7z
path: build/freetube-${{ steps.versionNumber.outputs.version }}-win.7z
- name: Upload Windows arm64 .exe Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}-setup-arm64.exe
path: build/freetube Setup ${{ steps.versionNumber.outputs.version }}.exe
- name: Upload Windows arm64 Portable Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}-win-arm64-portable.exe
path: build/freetube ${{ steps.versionNumber.outputs.version }}.exe
- name: Upload Windows arm64 .zip Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}-win-arm64-portable.zip
path: build/freetube-${{ steps.versionNumber.outputs.version }}-arm64-win.zip
- name: Upload Windows arm64 .7z Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}-win-arm64-portable.7z
path: build/freetube-${{ steps.versionNumber.outputs.version }}-arm64-win.7z
- name: Upload Mac x64 .dmg Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}-mac-x64.dmg
path: build/freetube-${{ steps.versionNumber.outputs.version }}.dmg
- name: Upload Mac x64 .zip Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}-mac-x64.zip
path: build/freetube-${{ steps.versionNumber.outputs.version }}-mac.zip
- name: Upload Mac x64 .7z Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-x64')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}-mac-x64.7z
path: build/freetube-${{ steps.versionNumber.outputs.version }}-mac.7z
- name: Upload Mac arm64 .dmg Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}-mac-arm64.dmg
path: build/freetube-${{ steps.versionNumber.outputs.version }}-arm64.dmg
- name: Upload Mac arm64 .zip Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}-mac-arm64.zip
path: build/freetube-${{ steps.versionNumber.outputs.version }}-arm64-mac.zip
- name: Upload Mac arm64 .7z Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f #v7.0.0
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-arm64')
with:
name: freetube-${{ steps.versionNumber.outputs.version }}-mac-arm64.7z
path: build/freetube-${{ steps.versionNumber.outputs.version }}-arm64-mac.7z
- name: Save yarn cache
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3
# Only save the cache if we weren't able to restore an existing one above
if: steps.restore_cache.outputs.cache-primary-key != steps.restore_cache.outputs.cache-matched-key
with:
key: ${{ steps.restore_cache.outputs.cache-primary-key }}
path: ${{ steps.cache_dir.outputs.cache_dir }}
================================================
FILE: .github/workflows/calibreapp-image-actions.yml
================================================
# Compress images on demand (workflow_dispatch), and at 12am every Sunday (schedule).
# Open a Pull Request if any images can be compressed.
name: Compress Images
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * 0'
permissions: {}
jobs:
build:
# As this action runs on a schedule, only run it in the FreeTubeApp/FreeTube repository to avoid unnecessary GitHub Actions usage/billing in forks.
# Still allow the workflow to be manually triggered.
# If a fork does need this workflow, they can change this condition in their fork to include their repository.
if: github.repository == 'FreeTubeApp/FreeTube' || github.event_name == 'workflow_dispatch'
name: calibreapp/image-actions
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout Repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- name: Compress Images
id: calibre
uses: calibreapp/image-actions@d9c8ee5c3dc52ae4622c82ead88d658f4b16b65f
with:
githubToken: ${{ secrets.GITHUB_TOKEN }}
compressOnly: true
- name: Create New Pull Request If Needed
if: steps.calibre.outputs.markdown != ''
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 #v8.1.0
with:
title: Compressed Images Nightly
branch-suffix: timestamp
commit-message: Compressed Images
body: ${{ steps.calibre.outputs.markdown }}
================================================
FILE: .github/workflows/codeql.yml
================================================
name: "CodeQL"
on:
push:
branches: [ "development" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "development" ]
schedule:
- cron: '36 3 * * 5'
permissions: {}
jobs:
analyze:
# As this action runs on a schedule, only run it in the FreeTubeApp/FreeTube repository to avoid unnecessary GitHub Actions usage/billing in forks.
# We still allow it to run if it was triggered by a pull request or a push as active forks should care about security.
# If a fork does need this workflow to run on a schedule, they can change this condition in their fork to include their repository.
if: github.repository == 'FreeTubeApp/FreeTube' || github.event_name != 'schedule'
name: Analyze (${{ matrix.language }})
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: javascript-typescript
build-mode: none
# https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
category: "/language:${{matrix.language}}"
================================================
FILE: .github/workflows/conflicts.yml
================================================
name: "Conflicts"
on:
# So that PRs touching the same files as the push are updated
push:
# So that the `dirtyLabel` is removed if conflicts are resolve
# We recommend `pull_request_target` so that github secrets are available.
# In `pull_request` we wouldn't be able to change labels of fork PRs
pull_request_target:
types: [synchronize]
permissions: {}
jobs:
main:
runs-on: ubuntu-slim
permissions:
pull-requests: write
steps:
- name: check if prs are dirty
uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 #v3.0.3
with:
dirtyLabel: "PR: merge conflicts / rebase needed"
removeOnDirtyLabel: "PR: waiting for review"
repoToken: "${{ secrets.GITHUB_TOKEN }}"
commentOnDirty: "This pull request has conflicts, please resolve those before we can evaluate the pull request."
commentOnClean: "Conflicts have been resolved. A maintainer will review the pull request shortly."
================================================
FILE: .github/workflows/flatpak.yml
================================================
# This is a basic workflow that is manually triggered
name: Create Flatpak PR
# Controls when the action will run. Workflow runs when manually triggered using the UI
# or API.
on:
workflow_dispatch:
# Disable running automatically as we now use flathub's external data checker
# to automatically open pull requests when we release updates
# release:
# types: [published]
permissions: {}
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
repository: flathub/io.freetubeapp.FreeTube
token: ${{ secrets.FLATHUB_TOKEN }}
persist-credentials: true
- name: Get Repo Release List
uses: moustacheful/github-api-exec-action@2135aaccb1220f81e6fa4f14c90cc20efba069fe #v0
id: list_results
with:
# Command to execute, (e.g: `pulls.create`), see https://octokit.github.io/rest.js/ for available commands
command: repos.listReleases
payload: >
{
"owner": "FreeTubeApp",
"repo": "FreeTube"
}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install xmlstarlet
shell: bash
run: sudo apt -y install xmlstarlet
- name: Create Version Variable
uses: bluwy/substitute-string-action@70ff0b17357670ffd3fee68e95b6de9963b66bad #v3.0.0
id: sub
with:
_input-text: ${{ fromJson(steps.list_results.outputs.result)[0].tag_name }}
-beta: ''
v: ''
- name: Create Release Branch
shell: bash
env:
VERSION: ${{ steps.sub.outputs.result }}
run: |
git checkout -b "release-v${VERSION}"
git push --set-upstream origin "release-v${VERSION}"
- name: Download x64 Release
uses: fabriciobastian/download-release-asset-action@35e62aa5a2fe4a6ec79004f6052918349013ae4f #v1.0.6
with:
version: v${{ steps.sub.outputs.result }}-beta
repository: FreeTubeApp/FreeTube
file: freetube-${{ steps.sub.outputs.result }}-beta-linux-x64-portable.zip
- name: Download ARM Release
uses: fabriciobastian/download-release-asset-action@35e62aa5a2fe4a6ec79004f6052918349013ae4f #v1.0.6
with:
version: v${{ steps.sub.outputs.result }}-beta
repository: FreeTubeApp/FreeTube
file: freetube-${{ steps.sub.outputs.result }}-beta-linux-arm64-portable.zip
- name: Set x64 Hash Variable
id: hash_x64
shell: bash
env:
VERSION: ${{ steps.sub.outputs.result }}
run: |
echo 'HASH_X64<<EOF' >> "$GITHUB_OUTPUT"
sha256sum "freetube-${VERSION}-beta-linux-x64-portable.zip" | awk '{print $1}' >> "$GITHUB_OUTPUT"
echo 'EOF' >> "$GITHUB_OUTPUT"
- name: Set ARM Hash Variable
id: hash_arm64
shell: bash
env:
VERSION: ${{ steps.sub.outputs.result }}
run: |
echo 'HASH_ARM64<<EOF' >> "$GITHUB_OUTPUT"
sha256sum "freetube-${VERSION}-beta-linux-arm64-portable.zip" | awk '{print $1}' >> "$GITHUB_OUTPUT"
echo 'EOF' >> "$GITHUB_OUTPUT"
- name: Set Date Variable
id: current_date
shell: bash
run: |
echo 'CURRENT_DATE<<EOF' >> "$GITHUB_OUTPUT"
date +"%Y-%m-%d" >> "$GITHUB_OUTPUT"
echo 'EOF' >> "$GITHUB_OUTPUT"
- name: Update x64 File Location in yml File
shell: bash
env:
VERSION: steps.sub.outputs.result
run: |
yq -i '.modules[0].sources[0].url = "https://github.com/FreeTubeApp/FreeTube/releases/download/v" + strenv(VERSION) + "-beta/freetube-" + strenv(VERSION) + "-beta-linux-x64-portable.zip"' io.freetubeapp.FreeTube.yml
- name: Update x64 Hash in yml File
shell: bash
env:
HASH_X64: ${{ steps.hash_x64.outputs.HASH_X64 }}
run: |
yq -i '.modules[0].sources[0].sha256 = strenv(HASH_X64)' io.freetubeapp.FreeTube.yml
- name: Update ARM File Location in yml File
shell: bash
env:
VERSION: steps.sub.outputs.result
run: |
yq -i '.modules[0].sources[1].url = "https://github.com/FreeTubeApp/FreeTube/releases/download/v" + strenv(VERSION) + "-beta/freetube-" + strenv(VERSION) + "-beta-linux-arm64-portable.zip"' io.freetubeapp.FreeTube.yml
- name: Update ARM Hash in yml File
shell: bash
env:
HASH_ARM64: ${{ steps.hash_arm64.outputs.HASH_ARM64 }}
run: |
yq -i '.modules[0].sources[1].sha256 = strenv(HASH_ARM64)' io.freetubeapp.FreeTube.yml
- name: Add Patch Notes to XML File
shell: bash
env:
CURRENT_DATE: ${{ steps.current_date.outputs.current_date }}
VERSION: ${{ steps.sub.outputs.result }}
run: xmlstarlet ed -L -i /component/releases/release[1] -t elem -n releaseTMP -v "" -i //releaseTMP -t attr -n version -v "${VERSION} Beta" -i //releaseTMP -t attr -n date -v "${CURRENT_DATE}" -s //releaseTMP -t elem -n url -v "" -s //releaseTMP/url -t text -n "" -v "https://github.com/FreeTubeApp/FreeTube/releases/tag/v${VERSION}-beta" -r //releaseTMP -v "release" io.freetubeapp.FreeTube.metainfo.xml
- name: Remove Release Files
shell: bash
env:
VERSION: ${{ steps.sub.outputs.result }}
run: |
rm "freetube-${VERSION}-beta-linux-x64-portable.zip"
rm "freetube-${VERSION}-beta-linux-arm64-portable.zip"
- name: Commit Files
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 #v7.1.0
with:
# Optional but recommended
# Defaults to "Apply automatic changes"
commit_message: Update files for v${{ steps.sub.outputs.result }}
# Optional options appended to `git-commit`
# See https://git-scm.com/docs/git-commit for a list of available options
commit_options: '--no-verify --signoff'
# Optional: Disable dirty check and always try to create a commit and push
skip_dirty_check: true
- name: Create PR
shell: bash
env:
GH_TOKEN: ${{ secrets.FLATHUB_TOKEN }}
VERSION: ${{ steps.sub.outputs.result }}
run: gh pr create --title "Release v${VERSION}" --body "This is an automated PR for the v${VERSION} release. This PR will be updated and merged once testing is complete."
================================================
FILE: .github/workflows/label-issue.yml
================================================
name: "Issue Labeler"
on:
issues:
types: [opened]
permissions: {}
jobs:
triage:
runs-on: ubuntu-slim
permissions:
contents: read
issues: write
steps:
- uses: github/issue-labeler@c1b0f9f52a63158c4adc09425e858e87b32e9685 #v3.4
with:
configuration-path: .github/issue-labeler.yml
enable-versioned-regex: 0
repo-token: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .github/workflows/label-pr.yml
================================================
name: "Pull Request Labeler"
on:
pull_request_target:
types: [opened, reopened, ready_for_review]
permissions: {}
jobs:
triage:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-slim
if: ${{ !github.event.pull_request.draft }}
steps:
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b #v6.0.1
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
configuration-path: .github/pr-labeler.yml
================================================
FILE: .github/workflows/linter.yml
================================================
# This is a basic workflow to help you get started with Actions
name: Linter
# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
pull_request:
branches: [ master, development, '**-RC' ]
permissions: {}
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
lint:
# The type of runner that the job will run on
runs-on: ubuntu-latest
permissions:
contents: read
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- name: Get yarn cache directory
id: cache_dir
shell: bash
run: |
{
echo 'cache_dir<<EOF'
yarn cache dir
echo EOF
} >> "$GITHUB_OUTPUT"
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
- name: Use Node.js 24.x
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f #v6.3.0
with:
node-version: 24.x
package-manager-cache: false
# This workflow runs on pull requests which are untrusted
# so we only restore the yarn cache, we don't save it to avoid cache poisoning
- name: Restore yarn cache
id: restore_cache
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3
with:
key: ${{ format('node-cache-{0}-{1}-yarn-{2}', runner.os, runner.arch, hashFiles('yarn.lock')) }}
path: ${{ steps.cache_dir.outputs.cache_dir }}
- run: yarn run ci
shell: bash
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: '1'
- run: yarn run lint
shell: bash
# let's verify that webpack is able to package the project
- run: yarn run pack
shell: bash
# verify that webpack is able to package the project using the web config
- run: yarn run pack:web
shell: bash
================================================
FILE: .github/workflows/no-response.yml
================================================
name: No Response
# Both `issue_comment` and `scheduled` event types are required for this Action
# to work properly.
on:
issue_comment:
types: [created]
schedule:
# Run daily at midnight.
- cron: '0 0 * * *'
permissions: {}
jobs:
noResponse:
# As this action runs on a schedule, only run it in the FreeTubeApp/FreeTube repository to avoid unnecessary GitHub Actions usage/billing in forks.
# If a fork does need this workflow, they can change this condition in their fork to include their repository.
if: github.repository == 'FreeTubeApp/FreeTube'
runs-on: ubuntu-slim
permissions:
issues: write
steps:
- uses: lee-dohm/no-response@9bb0a4b5e6a45046f00353d5de7d90fb8bd773bb #v0.5.0
with:
token: ${{ github.token }}
closeComment: >
This issue has been automatically closed because there has been no response to our request for more information from the original author.
With only the information that is currently in the issue, we don't have enough information to take action.
Please reach out if you have or find the answers we need so that we can investigate further.
daysUntilClose: 7
responseRequiredLabel: "U: Waiting for Response from Author"
================================================
FILE: .github/workflows/release.yml
================================================
# This is a basic workflow that is manually triggered
name: Upload Release
# Controls when the action will run. Workflow runs when manually triggered using the UI
# or API.
on:
workflow_dispatch:
inputs:
releaseId:
type: string
required: true
description: Release ID
permissions: {}
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
build:
permissions:
contents: write
strategy:
matrix:
node-version: [24.x]
runtime:
- linux-x64
- linux-armv7l
- linux-arm64
- win-x64
- win-arm64
- osx-x64
- osx-arm64
include:
- runtime: linux-x64
os: ubuntu-latest
- runtime: linux-armv7l
os: ubuntu-latest
- runtime: linux-arm64
os: ubuntu-latest
- runtime: osx-x64
os: macOS-latest
- runtime: osx-arm64
os: macOS-latest
- runtime: win-x64
os: windows-latest
- runtime: win-arm64
os: windows-latest
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
persist-credentials: false
# Do not restore the yarn cache, to avoid cache poisoning affecting releases
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f #v6.3.0
with:
node-version: ${{ matrix.node-version }}
package-manager-cache: false
- run: yarn run ci
shell: bash
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: '1'
- run: yarn run lint
shell: bash
- name: Get Version Number
id: getPackageInfo
shell: bash
run: |
jq --raw-output '"version=" + .version' package.json >> "$GITHUB_OUTPUT"
- name: Install libarchive-tools
if: startsWith(matrix.os, 'ubuntu')
shell: bash
run: |
sudo apt update
sudo apt -y install libarchive-tools
- name: Build x64 with Node.js ${{ matrix.node-version}}
if: contains(matrix.runtime, 'x64')
shell: bash
run: yarn run build
- name: Build ARMv7l with Node.js ${{ matrix.node-version}}
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
shell: bash
run: yarn run build:arm32
- name: Build ARM64 with Node.js ${{ matrix.node-version}}
if: contains(matrix.runtime, 'arm64')
shell: bash
run: yarn run build:arm64
- name: Convert X64 AppImage to static runtime and add update information
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
shell: bash
env:
VERSION: ${{ steps.getPackageInfo.outputs.version }}
run: |
sudo apt install desktop-file-utils
cd build
appimage="FreeTube-${VERSION}.AppImage"
wget "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage" -O ./appimagetool.AppImage
chmod +x ./"$appimage" ./appimagetool.AppImage
update_information='gh-releases-zsync|FreeTubeApp|FreeTube|latest-all|freetube-*-amd64.AppImage.zsync'
./"$appimage" --appimage-extract && rm -f ./"$appimage"
./appimagetool.AppImage --comp zstd --mksquashfs-opt -Xcompression-level --mksquashfs-opt 20 \
-u "$update_information" -n ./squashfs-root ./"$appimage"
rm -rf ./squashfs-root ./appimagetool.AppImage
- name: Convert ARMv7l AppImage to static runtime and add update information
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
shell: bash
env:
VERSION: ${{ steps.getPackageInfo.outputs.version }}
run: |
sudo apt install desktop-file-utils
cd build
appimage="FreeTube-${VERSION}.AppImage"
wget "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage" -O ./appimagetool.AppImage
wget "https://github.com/AppImage/type2-runtime/releases/download/continuous/runtime-x86_64" -O runtime
chmod +x ./"$appimage" ./appimagetool.AppImage ./runtime
update_information='gh-releases-zsync|FreeTubeApp|FreeTube|latest-all|freetube-*-armv7l.AppImage.zsync'
TARGET_APPIMAGE="$appimage" ./runtime --appimage-extract && rm -f ./"$appimage"
ARCH=armhf ./appimagetool.AppImage --comp zstd --mksquashfs-opt -Xcompression-level --mksquashfs-opt 20 \
-u "$update_information" -n ./squashfs-root ./"$appimage"
rm -rf ./squashfs-root ./appimagetool.AppImage ./runtime
- name: Convert ARM64 AppImage to static runtime and add update information
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
shell: bash
env:
VERSION: ${{ steps.getPackageInfo.outputs.version }}
run: |
sudo apt install desktop-file-utils
cd build
appimage="FreeTube-${VERSION}.AppImage"
wget "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage" -O ./appimagetool.AppImage
wget "https://github.com/AppImage/type2-runtime/releases/download/continuous/runtime-x86_64" -O runtime
chmod +x ./"$appimage" ./appimagetool.AppImage ./runtime
update_information='gh-releases-zsync|FreeTubeApp|FreeTube|latest-all|freetube-*-arm64.AppImage.zsync'
TARGET_APPIMAGE="$appimage" ./runtime --appimage-extract && rm -f ./"$appimage"
ARCH=aarch64 ./appimagetool.AppImage --comp zstd --mksquashfs-opt -Xcompression-level --mksquashfs-opt 20 \
-u "$update_information" -n ./squashfs-root ./"$appimage"
rm -rf ./squashfs-root ./appimagetool.AppImage ./runtime
- name: Upload Linux .zip x64 Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-linux-x64-portable.zip
asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}.zip
asset_content_type: application/zip
- name: Upload Linux .7z x64 Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-linux-x64-portable.7z
asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}.7z
asset_content_type: application/x-7z-compressed
- name: Upload Linux .zip ARMv7l Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-linux-armv7l-portable.zip
asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}-armv7l.zip
asset_content_type: application/zip
- name: Upload Linux .7z ARMv7l Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-linux-armv7l-portable.7z
asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}-armv7l.7z
asset_content_type: application/x-7z-compressed
- name: Upload Linux .zip ARM64 Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-linux-arm64-portable.zip
asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}-arm64.zip
asset_content_type: application/zip
- name: Upload Linux .7z ARM64 Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-linux-arm64-portable.7z
asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}-arm64.7z
asset_content_type: application/x-7z-compressed
- name: Upload Linux .deb x64 Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube_${{ steps.getPackageInfo.outputs.version }}_beta_amd64.deb
asset_path: build/freetube_${{ steps.getPackageInfo.outputs.version }}_amd64.deb
asset_content_type: application/vnd.debian.binary-package
- name: Upload Linux .deb ARMv7l Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube_${{ steps.getPackageInfo.outputs.version }}_beta_armv7l.deb
asset_path: build/freetube_${{ steps.getPackageInfo.outputs.version }}_armv7l.deb
asset_content_type: application/vnd.debian.binary-package
- name: Upload Linux .deb ARM64 Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube_${{ steps.getPackageInfo.outputs.version }}_beta_arm64.deb
asset_path: build/freetube_${{ steps.getPackageInfo.outputs.version }}_arm64.deb
asset_content_type: application/vnd.debian.binary-package
- name: Upload AppImage x64 Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-amd64.AppImage
asset_path: build/FreeTube-${{ steps.getPackageInfo.outputs.version }}.AppImage
asset_content_type: application/vnd.appimage
- name: Upload AppImage .zsync x64 Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-amd64.AppImage.zsync
asset_path: build/FreeTube-${{ steps.getPackageInfo.outputs.version }}.AppImage.zsync
asset_content_type: application/x-zsync
- name: Upload AppImage ARMv7l Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-armv7l.AppImage
asset_path: build/FreeTube-${{ steps.getPackageInfo.outputs.version }}-armv7l.AppImage
asset_content_type: application/vnd.appimage
- name: Upload AppImage .zsync ARMv7l Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-armv7l.AppImage.zsync
asset_path: build/FreeTube-${{ steps.getPackageInfo.outputs.version }}-armv7l.AppImage.zsync
asset_content_type: application/x-zsync
- name: Upload AppImage ARM64 Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-arm64.AppImage
asset_path: build/FreeTube-${{ steps.getPackageInfo.outputs.version }}-arm64.AppImage
asset_content_type: application/vnd.appimage
- name: Upload AppImage .zsync ARM64 Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-arm64.AppImage.zsync
asset_path: build/FreeTube-${{ steps.getPackageInfo.outputs.version }}-arm64.AppImage.zsync
asset_content_type: application/x-zsync
- name: Upload Linux .rpm x64 Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta.amd64.rpm
asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}.x86_64.rpm
asset_content_type: application/x-rpm
# rpm are not built for armv7l
- name: Upload Linux .rpm ARM64 Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta.arm64.rpm
asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}.aarch64.rpm
asset_content_type: application/x-rpm
- name: Upload Pacman .pacman x64 Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-amd64.pacman
asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}.pacman
asset_content_type: application/x-zstd-compressed-tar
- name: Upload Windows x64 .exe Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-setup-x64.exe
asset_path: build/freetube Setup ${{ steps.getPackageInfo.outputs.version }}.exe
asset_content_type: application/x-ms-dos-executable
- name: Upload Windows x64 portable Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-win-x64-portable.exe
asset_path: build/FreeTube ${{ steps.getPackageInfo.outputs.version }}.exe
asset_content_type: application/x-ms-dos-executable
- name: Upload Windows x64 .zip Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-win-x64-portable.zip
asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}-win.zip
asset_content_type: application/zip
- name: Upload Windows x64 .7z Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-win-x64-portable.7z
asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}-win.7z
asset_content_type: application/x-7z-compressed
- name: Upload Windows arm64 .exe Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-arm64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-setup-arm64.exe
asset_path: build/freetube Setup ${{ steps.getPackageInfo.outputs.version }}.exe
asset_content_type: application/x-ms-dos-executable
- name: Upload Windows arm64 portable Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-arm64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-win-arm64-portable.exe
asset_path: build/FreeTube ${{ steps.getPackageInfo.outputs.version }}.exe
asset_content_type: application/x-ms-dos-executable
- name: Upload Windows arm64 .zip Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-arm64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-win-arm64-portable.zip
asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}-arm64-win.zip
asset_content_type: application/zip
- name: Upload Windows arm64 .7z Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-arm64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-win-arm64-portable.7z
asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}-arm64-win.7z
asset_content_type: application/x-7z-compressed
- name: Upload Mac x64 .dmg Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-x64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-mac-x64.dmg
asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}.dmg
asset_content_type: application/x-apple-diskimage
- name: Upload Mac x64 .zip Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-x64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-mac-x64.zip
asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}-mac.zip
asset_content_type: application/zip
- name: Upload Mac x64 .7z Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-x64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-mac-x64.7z
asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}-mac.7z
asset_content_type: application/x-7z-compressed
- name: Upload Mac arm64 .dmg Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-arm64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-mac-arm64.dmg
asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}-arm64.dmg
asset_content_type: application/x-apple-diskimage
- name: Upload Mac arm64 .zip Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-arm64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-mac-arm64.zip
asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}-arm64-mac.zip
asset_content_type: application/x-apple-diskimage
- name: Upload Mac arm64 .7z Release
uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 #v1.0.2
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-arm64')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/FreeTubeApp/FreeTube/releases/${{ inputs.releaseId }}/assets{?name,label}
asset_name: freetube-${{ steps.getPackageInfo.outputs.version }}-beta-mac-arm64.7z
asset_path: build/freetube-${{ steps.getPackageInfo.outputs.version }}-arm64-mac.7z
asset_content_type: application/x-7z-compressed
================================================
FILE: .github/workflows/remove-outdated-labels.yml
================================================
name: Remove outdated labels
on:
pull_request_target:
types:
- closed
- converted_to_draft
- ready_for_review
permissions: {}
jobs:
remove-closed-pr-labels:
name: Remove closed and merged pull request labels
if: github.event.action == 'closed'
runs-on: ubuntu-slim
permissions:
pull-requests: write
steps:
- name: Remove labels
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_URL: ${{ github.event.pull_request.html_url }}
run: |
labels='PR: waiting for review,'
labels+='PR: WIP,'
labels+='PR: changes requested,'
labels+='PR: merge conflicts / rebase needed,'
labels+='PR/Issue: dependent,'
labels+='PR: stale'
gh pr edit "$PR_URL" --remove-label "$labels"
remove-draft-pr-labels:
name: Remove labels from draft pull requests
if: |
github.event.action == 'converted_to_draft' && contains(github.event.pull_request.labels.*.name, 'PR: waiting for review')
runs-on: ubuntu-slim
permissions:
pull-requests: write
steps:
- name: Remove label
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_URL: ${{ github.event.pull_request.html_url }}
run: |
gh pr edit "$PR_URL" --remove-label 'PR: waiting for review'
remove-ready-pr-labels:
name: Remove labels when draft pr is marked ready for review
if: |
github.event.action == 'ready_for_review' && contains(github.event.pull_request.labels.*.name, 'PR: WIP')
runs-on: ubuntu-slim
permissions:
pull-requests: write
steps:
- name: Remove label
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_URL: ${{ github.event.pull_request.html_url }}
run: |
gh pr edit "$PR_URL" --remove-label 'PR: WIP'
================================================
FILE: .github/workflows/stale.yml
================================================
name: 'Close stale PRs'
on:
schedule:
- cron: '30 1 * * *'
permissions: {}
jobs:
stale:
# As this action runs on a schedule, only run it in the FreeTubeApp/FreeTube repository to avoid unnecessary GitHub Actions usage/billing in forks.
# If a fork does need this workflow, they can change this condition in their fork to include their repository.
if: github.repository == 'FreeTubeApp/FreeTube'
runs-on: ubuntu-slim
permissions:
actions: write
pull-requests: write
steps:
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f #v10.2.0
with:
stale-pr-message: 'This PR is stale because it has been open 14 days with no activity. Remove stale label or comment or this will be closed in 14 days.'
close-pr-message: 'This PR was closed because it has been stalled for 14 days with no activity.'
days-before-pr-stale: 14
days-before-pr-close: 14
stale-pr-label: 'PR: stale'
exempt-pr-labels: 'PR: WIP'
# actions/stale doesn't have a way of running only on pull requests
# so set the issue thresholds to -1 so it at least doesn't try to label or close them
days-before-stale: -1
days-before-close: -1
================================================
FILE: .github/workflows/updateSite.yml
================================================
# This is a basic workflow that is manually triggered
name: Update Site Version Number
# Controls when the action will run. Workflow runs when manually triggered using the UI
# or API.
on:
workflow_dispatch:
release:
types: [published]
permissions: {}
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
repository: FreeTubeApp/FreeTubeApp.io
token: ${{ secrets.FLATHUB_TOKEN }}
persist-credentials: true
- name: Get Repo Release List
uses: moustacheful/github-api-exec-action@2135aaccb1220f81e6fa4f14c90cc20efba069fe #v0
id: list_results
with:
# Command to execute, (e.g: `pulls.create`), see https://octokit.github.io/rest.js/ for available commands
command: repos.listReleases
payload: >
{
"owner": "FreeTubeApp",
"repo": "FreeTube"
}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create Current Version Variable
uses: bluwy/substitute-string-action@70ff0b17357670ffd3fee68e95b6de9963b66bad #v3.0.0
id: current
with:
_input-text: ${{ fromJson(steps.list_results.outputs.result)[0].tag_name }}
-beta: ''
v: ''
- name: Create Previous Version Variable
uses: bluwy/substitute-string-action@70ff0b17357670ffd3fee68e95b6de9963b66bad #v3.0.0
id: previous
with:
_input-text: ${{ fromJson(steps.list_results.outputs.result)[1].tag_name }}
-beta: ''
v: ''
- name: Set Master Branch
# Currently the default branch is master, but if that changes later, then this acts as a failsafe.
shell: bash
run: |
git checkout master
- name: Update index.php
shell: bash
env:
CURRENT: ${{ steps.current.outputs.result }}
PREVIOUS: ${{ steps.previous.outputs.result }}
run: |
sed -i "s/${PREVIOUS}/${CURRENT}/g" src/index.php
- name: Commit Files
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 #v7.1.0
with:
# Optional but recommended
# Defaults to "Apply automatic changes"
commit_message: Update version number to v${{ steps.current.outputs.result }}
# Optional options appended to `git-commit`
# See https://git-scm.com/docs/git-commit for a list of available options
commit_options: '--no-verify --signoff'
# Optional: Disable dirty check and always try to create a commit and push
skip_dirty_check: true
================================================
FILE: .gitignore
================================================
.DS_Store
dist/electron/*
storyboards/*
dashFiles/*
static/dashFiles
static/storyboards
static/dashFiles/*
static/storyboards/*
dist/web/*
build/*
!build/icons
coverage
node_modules/
npm-debug.log
npm-debug.log.*
thumbs.db
!.gitkeep
data/tmp/
.tmp/
tmp/
.cache
dist
coverage
__coverage__
csak-timelog.json
.idea/
debug/
# Lefthook
lefthook-local.yml
locale-errors.json
================================================
FILE: .stylelintignore
================================================
src/data/
src/datastores/
src/main/
src/renderer/videoJS.css
dist/
static/
node_modules/
================================================
FILE: .vscode/extensions.json
================================================
{
"recommendations": [
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint",
"stylelint.vscode-stylelint",
"syler.sass-indented",
"redhat.vscode-yaml",
"vue.volar",
"eamodio.gitlens"
]
}
================================================
FILE: .vscode/launch.json
================================================
{
"version": "0.2.0",
"configurations": [
{
"name": "dev-runner (Electron)",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/_scripts/dev-runner.js",
"args": ["--remote-debug"]
},
{
"name": "Attach to renderer process (Electron)",
"type": "chrome",
"request": "attach",
"port": 9223,
"webRoot": "http://localhost:9080",
"sourceMapPathOverrides": {
"webpack://freetube/./~/*": "${workspaceFolder}/node_modules/*",
"webpack://freetube/./*": "${workspaceFolder}/*"
}
}
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"stylelint.packageManager": "yarn",
"stylelint.snippet": [
"css",
"less",
"postcss",
"sass",
"scss"
],
"stylelint.validate": [
"css",
"less",
"postcss",
"scss"
],
"eslint.packageManager": "yarn",
"eslint.probe": [
"javascript",
"vue",
"json",
"jsonc",
"yml",
"yaml"
],
"eslint.validate": [
"javascript",
"vue",
"json",
"jsonc",
"yml",
"yaml"
],
"javascript.preferences.importModuleSpecifier": "relative"
}
================================================
FILE: .whitesource
================================================
##########################################################
#### WhiteSource "Bolt for Github" configuration file ####
##########################################################
# Configuration #
#---------------#
ws.repo.scan=true
vulnerable.check.run.conclusion.level=failure
================================================
FILE: CONTRIBUTING.md
================================================
# Code Contributions
## Before starting to code
Please follow these guidelines before starting to code you feature or bugfix.
* If you want to implement a bugfix or feature request from an existing issue, please comment on that issue that you will work on it. This helps us to coordinate what needs to be done and what not.
* If you want to implement a feature request without an existing issue, please create an issue, so we know what you are working on and discuss the feature.
* For major feature implementations make sure you are able to maintain your code in the future in regard to bugs and possible code conflicts in future updates. Optionally you could join the [Matrix](https://matrix.to/#/#freetube:matrix.org) channel, so you will hear instantly if something needs to be worked on.
* Do not open pull requests that are primarily written by AI. It's a waste of our team's time. Repeat offenders will be banned.
## Before your Pull Request
Please follow these guidelines before sending your pull request and making contributions.
* When you submit a pull request, you agree that your code is published under the [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.html)
* Please link the issue you are referring to.
* Do not include non-free software or modules with your code.
* Make sure your pull request is set up to merge your branch to FreeTube's development branch.
* Make sure your branch is up to date with the development branch before submitting your pull request.
* Stick to a similar style of code already in the project. Please look at current code to get an idea on how to do this.
* Follow [ES6](https://rse.github.io/es6-features/) standards in your code. Ex: Use `let` and `const` instead of `var`. Do not use `function(response){//code}` for callbacks, use `(response) => {//code}`.
* Comment your code when necessary. Follow the [JavaScript Documentation and Comments Standard](https://www.drupal.org/docs/develop/standards/javascript/javascript-api-documentation-and-comment-standards) for functions.
* Please follow proper Vue structure when creating new code / components. Use existing code as well as the [Vue.js Guide](https://vuejs.org/guide/introduction.html) for reference.
* Please test your code. Make sure new features work as well as existing core features such as watching videos or loading subscriptions. New features need to work with both the Local API as well as the Invidious API
* Please make sure your code does not violate any standards set by our linter. It's up to you to make fixes whenever necessary. You can run `yarn run lint` to check locally and `yarn run lint-fix` to automatically fix smaller issues.
* Please limit the amount of Node Modules that you introduce into the project. Only include them when **absolutely necessary** for your code to work (Ex: Using nedb for databases) or if a module provides similar functionality to what you are trying to achieve (Ex: Using autolinker to create links to outside URLs instead of writing the functionality myself).
* Please try to stay involved with the community and maintain your code. We are only a handful of developers working on FreeTube in our spare time. We do not have time to work on everything, and it would be nice if you can maintain your code when necessary.
# Setting up Your Environment
Check out the [wiki](https://docs.freetubeapp.io/development/getting-started/) page to learn how to set up your environment and get started.
================================================
FILE: LICENSE
================================================
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
================================================
FILE: README.md
================================================
<p align="center">
<img alt="" src="/_icons/logoColor.svg" width=500 align="center">
</p>
FreeTube is an open source desktop YouTube player built with privacy in mind.
Use YouTube without advertisements and prevent Google from tracking you with their cookies and JavaScript.
Available for Windows (10 and later), Mac (macOS 12 and later) & Linux thanks to Electron.
<p align="center"><a href="https://github.com/FreeTubeApp/FreeTube/releases">Download FreeTube</a></p>
<p align="center">
<a href="https://github.com/FreeTubeApp/FreeTube/actions/workflows/build.yml">
<img alt='Build status' src="https://github.com/FreeTubeApp/FreeTube/actions/workflows/build.yml/badge.svg?branch=development" />
</a>
<a href="https://hosted.weblate.org/engage/free-tube/">
<img src="https://hosted.weblate.org/widgets/free-tube/-/svg-badge.svg" alt="Translation status" />
</a>
</p>
<hr>
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#how-does-it-work">How does it work?</a> • <a href="#features">Features</a> • <a href="#download-links">Download Links</a> • <a href="#contributing">Contributing</a> • <a href="#localization">Localization</a> • <a href="#contact">Contact</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
<p align="center"><a href="https://freetubeapp.io/">Website</a> • <a href="https://blog.freetubeapp.io/">Blog</a> • <a href="https://docs.freetubeapp.io/">Documentation</a> • <a href="https://docs.freetubeapp.io/faq/">FAQ</a> • <a href="https://github.com/FreeTubeApp/FreeTube/discussions">Discussions</a></p>
<hr>
> [!NOTE]
> FreeTube is currently in Beta. While it should work well for most users, there are still bugs and missing features that need to be addressed.
>
> If you have an idea or if you found a bug, please submit a [GitHub issue](https://github.com/FreeTubeApp/FreeTube/issues/new/choose) so that we can track it. Please [search the existing issues](https://github.com/FreeTubeApp/FreeTube/issues?q=is%3Aissue+sort%3Arelevance-desc) before submitting to prevent duplicates!
## Screenshots
| The main FreeTube window |
|--------------------------------------------------------------------------------------------------|
| |
| Watching a video |
|--------------------------------------------------------------------------------------------------|
| |
| Settings |
|--------------------------------------------------------------------------------------------------|
| |
## How does it work?
FreeTube uses a built in extractor to grab and serve data / videos. The [Invidious API](https://github.com/iv-org/invidious) can also optionally be used. FreeTube does not use any official APIs to obtain data. While YouTube can still see your video requests, it can no
longer track you using cookies or JavaScript. Your subscriptions, playlists and history are stored locally on your computer and never sent out.
> [!IMPORTANT]
> Using a VPN or Tor is highly recommended to hide your IP while using FreeTube.
## Features
* Watch videos without ads
* Use YouTube without Google tracking you using cookies and JavaScript
* Two extractor APIs to choose from (Built in or Invidious)
* Subscribe to channels without an account
* Connect to an externally setup proxy such as Tor
* View and search your local subscriptions, playlists and history
* Organize your subscriptions into "Profiles" to create a more focused feed
* Export & import subscriptions
* YouTube Trending
* YouTube Chapters
* Most popular videos page based on the set Invidious instance
* SponsorBlock
* DeArrow
* Open videos from your browser directly into FreeTube (with extension)
* Watch videos using an external player
* Full Theme support
* Make a screenshot of a video
* Multiple windows
* Mini Player (Picture-in-Picture)
* Keyboard shortcuts
* Option to show only family friendly content
* Show/hide functionality or elements within the app using the distraction free settings
* View channel posts
### Browser Extensions
The following extensions open YouTube links directly in FreeTube:
- [LibRedirect](https://libredirect.github.io/)
- [RedirectTube](https://github.com/MStankiewiczOfficial/RedirectTube)
LibRedirect automatically redirect YouTube links to FreeTube.
> [!IMPORTANT]
> To ensure proper functionality, select FreeTube as Frontend in the Services settings of the extension.
RedirectTube, doesn’t automatically open YouTube links in FreeTube. Instead, it adds buttons to the toolbar and context menu, which you can click to open videos in FreeTube manually.
- Download LibRedirect from [Mozilla Add-ons](https://addons.mozilla.org/firefox/addon/libredirect/) (for Firefox based-browsers) or [developer's website](https://libredirect.github.io/download_chromium.html) (for Chrome and Chromium-based browsers).
- Download RedirectTube from [Mozilla Add-ons](https://addons.mozilla.org/firefox/addon/redirecttube/) (for Firefox based-browsers).
> [!NOTE]
> These extensions do not work on Linux portable builds!
>
> If you have issues with the extension working with FreeTube, please create an issue in this repository instead of the extension repository.
## Download Links
### Official Downloads
> [!CAUTION]
> FreeTube is only supported on Windows 10 and later, macOS 12 and above, and various Linux distributions. Installing it on unsupported systems may result in unexpected issues.
* [GitHub Releases](https://github.com/FreeTubeApp/FreeTube/releases)
* [FreeTube Website](https://freetubeapp.io/#download)
* Flatpak on Flathub: [Download](https://flathub.org/apps/details/io.freetubeapp.FreeTube) and [Source Code](https://github.com/flathub/io.freetubeapp.FreeTube)
#### Automated Builds (Nightly / Weekly)
> [!WARNING]
> Use these builds at your own risk. These are pre-release versions and are only intended for people that want to test changes early and are willing to accept that things could break from one build to another.
Builds are automatically created from changes to our development branch via [GitHub Actions](https://github.com/FreeTubeApp/FreeTube/actions?query=workflow%3ABuild).
The first build with a green check mark is the latest build.
> [!IMPORTANT]
> You will need to have a GitHub account to download these builds.
### Unofficial Downloads
> [!WARNING]
> These builds are maintained by the community. While they should be safe, download at your own risk. There may be issues with using these versus the official builds. Any issues specific with these builds should be sent to their respective maintainer. Make sure you always try an [official download](https://github.com/freetubeapp/freetube/#official-downloads) before reporting your issue to us!
* Arch User Repository (AUR): [Download](https://aur.archlinux.org/packages/freetube-bin/)
* Chocolatey: [Download](https://chocolatey.org/packages/freetube/)
* FreeTubeAndroid (FreeTube port for Android and PWA): [Download](https://github.com/MarmadileManteater/FreeTubeAndroid/releases) and [Source Code](https://github.com/MarmadileManteater/FreeTubeAndroid)
* Homebrew Formulae (Mac only): [Download for Apple Silicon](https://github.com/PikachuEXE/homebrew-FreeTube)
* Nix Packages: [Download](https://search.nixos.org/packages?query=freetube)
* PortableApps (Windows Only): [Download](https://github.com/rddim/FreeTubePortable/releases) and [Source Code](https://github.com/rddim/FreeTubePortable)
* Scoop (Windows Only): [Usage](https://github.com/ScoopInstaller/Scoop)
* Snap: [Download](https://snapcraft.io/freetube) and [Source Code](https://git.launchpad.net/freetube)
* WAPT: [Download](https://wapt.tranquil.it/store/en/tis-freetube)
* Windows Package Manager (winget): [Usage](https://docs.microsoft.com/en-us/windows/package-manager/winget/)
## Contributing
Thank you very much to the [People and Projects](https://docs.freetubeapp.io/credits/) that make FreeTube possible!
If you like to get your hands dirty and want to contribute, we would love to
have your help. Send a pull request and someone will review your code.
> [!IMPORTANT]
> Please follow the [Contribution Guidelines](https://github.com/FreeTubeApp/FreeTube/blob/development/CONTRIBUTING.md) before sending your pull request.
## Localization
<a href="https://hosted.weblate.org/engage/free-tube/">
<img src="https://hosted.weblate.org/widgets/free-tube/-/287x66-grey.png" alt="Translation status" />
</a>
We are actively looking for translations! We use [Weblate](https://hosted.weblate.org/engage/free-tube/) to make it easy for translators to get involved. Click on the badge above to learn how to get involved.
For the Linux Flatpak, the desktop entry comment string can be translated at our [Flatpak repository](https://github.com/flathub/io.freetubeapp.FreeTube/blob/master/io.freetubeapp.FreeTube.desktop).
## Contact
If you ever have any questions, feel free to ask it on our [Discussions](https://github.com/FreeTubeApp/FreeTube/discussions) page. Alternatively, you can email us at FreeTubeApp@protonmail.com or you can join our [Matrix Room](https://matrix.to/#/#freetube:matrix.org).
> [!IMPORTANT]
> Don't forget to check out the [rules](https://docs.freetubeapp.io/community/matrix/) before joining.
## Donate
If you enjoy using FreeTube, you're welcome to leave a donation using the following method.
* Bitcoin Address: `1Lih7Ho5gnxb1CwPD4o59ss78pwo2T91eS`
While your donations are much appreciated, only donate if you really want to. Donations are used for keeping the website up and running and eventual code signing costs.
> [!TIP]
> If you are using the Invidious API then we recommend that you donate to the instance that you use. You can also donate to the [Invidious team](https://invidious.io/donate/) or the [Local API developer](https://github.com/sponsors/LuanRT).
## License
[](https://www.gnu.org/licenses/agpl-3.0.html)
FreeTube is Free Software: You can use, study share and improve it at your
will. Specifically you can redistribute and/or modify it under the terms of the
[GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.html) as
published by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
================================================
FILE: _scripts/ProcessLocalesPlugin.js
================================================
const { existsSync, readFileSync } = require('fs')
const { readFile } = require('fs/promises')
const { join } = require('path')
const { brotliCompress, constants } = require('zlib')
const { promisify } = require('util')
const { load: loadYaml } = require('js-yaml')
const brotliCompressAsync = promisify(brotliCompress)
const PLUGIN_NAME = 'ProcessLocalesPlugin'
class ProcessLocalesPlugin {
constructor(options = {}) {
this.compress = !!options.compress
this.hotReload = !!options.hotReload
if (typeof options.inputDir !== 'string') {
throw new Error('ProcessLocalesPlugin: no input directory `inputDir` specified.')
} else if (!existsSync(options.inputDir)) {
throw new Error('ProcessLocalesPlugin: the specified input directory does not exist.')
}
this.inputDir = options.inputDir
if (typeof options.outputDir !== 'string') {
throw new Error('ProcessLocalesPlugin: no output directory `outputDir` specified.')
}
this.outputDir = options.outputDir
/** @type {Map<string, any>} */
this.locales = new Map()
this.localeNames = []
/** @type {Map<string, any>} */
this.cache = new Map()
this.filePaths = []
this.previousTimestamps = new Map()
this.startTime = Date.now()
/** @type {(updatedLocales: [string, string][]) => void|null} */
this.notifyLocaleChange = null
this.loadLocales()
}
/** @param {import('webpack').Compiler} compiler */
apply(compiler) {
const { Compilation, DefinePlugin } = compiler.webpack
new DefinePlugin({
'process.env.HOT_RELOAD_LOCALES': this.hotReload
}).apply(compiler)
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
compilation.hooks.processAssets.tapPromise({
name: PLUGIN_NAME,
stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL
}, async (_assets) => {
// While running in the webpack dev server, this hook gets called for every incremental build.
// For incremental builds we can return the already processed versions, which saves time
// and makes webpack treat them as cached
/** @type {[string, string][]} */
const updatedLocales = []
if (this.hotReload && !this.notifyLocaleChange) {
console.warn('ProcessLocalesPlugin: Unable to live reload locales as `notifyLocaleChange` is not set.')
}
const promises = []
for (const [locale, data] of this.locales) {
promises.push(this.processLocale(locale, data, updatedLocales, compiler, compilation))
}
await Promise.all(promises)
if (this.hotReload && this.notifyLocaleChange && updatedLocales.length > 0) {
this.notifyLocaleChange(updatedLocales)
}
})
})
compiler.hooks.afterCompile.tap(PLUGIN_NAME, (compilation) => {
if (compiler.watching) {
// watch locale files for changes
compilation.fileDependencies.addAll(this.filePaths)
}
})
}
/**
* @param {string} locale
* @param {any} data
* @param {[string, string][]} updatedLocales
* @param {import('webpack').Compiler} compiler
* @param {import('webpack').Compilation} compilation
*/
async processLocale(locale, data, updatedLocales, compiler, compilation) {
if (compiler.watching && compiler.fileTimestamps) {
const filePath = join(this.inputDir, `${locale}.yaml`)
const timestamp = compiler.fileTimestamps.get(filePath)?.safeTime
if (timestamp && timestamp > (this.previousTimestamps.get(locale) ?? this.startTime)) {
this.previousTimestamps.set(locale, timestamp)
const contents = await readFile(filePath, 'utf-8')
data = loadYaml(contents)
} else {
const { filename, source } = this.cache.get(locale)
compilation.emitAsset(filename, source, { minimized: true })
return
}
}
this.removeEmptyValues(data)
let filename = `${this.outputDir}/${locale}.json`
let output = JSON.stringify(data)
if (this.hotReload && compiler.fileTimestamps) {
updatedLocales.push([locale, output])
}
if (this.compress) {
filename += '.br'
output = await this.compressLocale(output)
}
let source = new compiler.webpack.sources.RawSource(output)
if (compiler.watching) {
source = new compiler.webpack.sources.CachedSource(source)
this.cache.set(locale, { filename, source })
// we don't need the unmodified sources anymore, as we use the cache `this.cache`
// so we can clear this to free some memory
this.locales.set(locale, null)
}
compilation.emitAsset(filename, source, { minimized: true })
}
loadLocales() {
const activeLocales = JSON.parse(readFileSync(`${this.inputDir}/activeLocales.json`))
for (const locale of activeLocales) {
const filePath = join(this.inputDir, `${locale}.yaml`)
this.filePaths.push(filePath)
const contents = readFileSync(filePath, 'utf-8')
const data = loadYaml(contents)
this.locales.set(locale, data)
this.localeNames.push(data['Locale Name'] ?? locale)
}
}
async compressLocale(data) {
const buffer = Buffer.from(data, 'utf-8')
return await brotliCompressAsync(buffer, {
params: {
[constants.BROTLI_PARAM_MODE]: constants.BROTLI_MODE_TEXT,
[constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY,
[constants.BROTLI_PARAM_SIZE_HINT]: buffer.byteLength
}
})
}
/**
* vue-i18n doesn't fallback if the translation is an empty string
* so we want to get rid of them and also remove the empty objects that can get left behind
* if we've removed all the keys and values in them
* @param {object|string} data
*/
removeEmptyValues(data) {
for (const key of Object.keys(data)) {
const value = data[key]
if (typeof value === 'object') {
this.removeEmptyValues(value)
}
if (!value || (typeof value === 'object' && Object.keys(value).length === 0)) {
delete data[key]
}
}
}
}
module.exports = ProcessLocalesPlugin
================================================
FILE: _scripts/_undefinedDefaultExport.mjs
================================================
export default undefined
================================================
FILE: _scripts/build.mjs
================================================
import { Arch, build, Platform } from 'electron-builder'
import config from './ebuilder.config.mjs'
const args = process.argv
/** @type {Map<import('electron-builder').Platform, Map<import('electron-builder').Arch, Array<string>>>} */
let targets
const platform = process.platform
if (platform === 'darwin') {
let arch = Arch.x64
if (args[2] === 'arm64') {
arch = Arch.arm64
}
targets = Platform.MAC.createTarget(['DMG', 'zip', '7z'], arch)
} else if (platform === 'win32') {
let arch = Arch.x64
if (args[2] === 'arm64') {
arch = Arch.arm64
}
targets = Platform.WINDOWS.createTarget(['nsis', 'zip', '7z', 'portable'], arch)
} else if (platform === 'linux') {
let arch = Arch.x64
if (args[2] === 'arm64') {
arch = Arch.arm64
}
if (args[2] === 'arm32') {
arch = Arch.armv7l
}
targets = Platform.LINUX.createTarget(['deb', 'zip', '7z', 'rpm', 'AppImage', 'pacman'], arch)
}
const output = await build({ targets, config, publish: 'never' })
console.log(output)
================================================
FILE: _scripts/clean.mjs
================================================
import { rm } from 'fs/promises'
import { join } from 'path'
const BUILD_PATH = join(import.meta.dirname, '..', 'build')
const DIST_PATH = join(import.meta.dirname, '..', 'dist')
await Promise.all([
rm(BUILD_PATH, { recursive: true, force: true }),
rm(DIST_PATH, { recursive: true, force: true })
])
================================================
FILE: _scripts/dev-runner.js
================================================
process.env.NODE_ENV = 'development'
const electron = require('electron')
const webpack = require('webpack')
const WebpackDevServer = require('webpack-dev-server')
const kill = require('tree-kill')
const path = require('path')
const { spawn } = require('child_process')
const ProcessLocalesPlugin = require('./ProcessLocalesPlugin')
let electronProcess = null
let manualRestart = null
const remoteDebugging = process.argv.indexOf('--remote-debug') !== -1
const web = process.argv.indexOf('--web') !== -1
let mainConfig
let rendererConfig
let preloadConfig
let botGuardScriptConfig
let webConfig
let SHAKA_LOCALES_TO_BE_BUNDLED
if (!web) {
mainConfig = require('./webpack.main.config')
rendererConfig = require('./webpack.renderer.config')
preloadConfig = require('./webpack.preload.config.js')
botGuardScriptConfig = require('./webpack.botGuardScript.config')
SHAKA_LOCALES_TO_BE_BUNDLED = rendererConfig.SHAKA_LOCALES_TO_BE_BUNDLED
delete rendererConfig.SHAKA_LOCALES_TO_BE_BUNDLED
} else {
webConfig = require('./webpack.web.config')
}
if (remoteDebugging) {
// disable dvtools open in electron
process.env.RENDERER_REMOTE_DEBUGGING = true
}
// Define exit code for relaunch and set it in the environment
const relaunchExitCode = 69
process.env.FREETUBE_RELAUNCH_EXIT_CODE = relaunchExitCode
const port = 9080
async function killElectron(pid) {
return new Promise((resolve, reject) => {
if (pid) {
kill(pid, err => {
if (err) reject(err)
resolve()
})
} else {
resolve()
}
})
}
async function restartElectron() {
console.log('\nStarting electron...')
const { pid } = electronProcess || {}
await killElectron(pid)
electronProcess = spawn(electron, [
path.join(__dirname, '../dist/main.js'),
// '--enable-logging', // Enable to show logs from all electron processes
remoteDebugging ? '--inspect=9222' : '',
remoteDebugging ? '--remote-debugging-port=9223' : ''
],
// { stdio: 'inherit' } // required for logs to actually appear in the stdout
)
electronProcess.on('exit', (code, _) => {
if (code === relaunchExitCode) {
electronProcess = null
restartElectron()
return
}
if (!manualRestart) process.exit(0)
})
}
/**
* @param {import('webpack').Compiler} compiler
* @param {WebpackDevServer} devServer
*/
function setupNotifyLocaleUpdate(compiler, devServer) {
const notifyLocaleChange = (updatedLocales) => {
devServer.sendMessage(devServer.webSocketServer.clients, 'freetube-locale-update', updatedLocales)
}
compiler.options.plugins
.filter(plugin => plugin instanceof ProcessLocalesPlugin)
.forEach((/** @type {ProcessLocalesPlugin} */plugin) => {
plugin.notifyLocaleChange = notifyLocaleChange
})
}
function startBotGuardScript() {
webpack(botGuardScriptConfig, (err) => {
if (err) console.error(err)
console.log(`\nCompiled ${botGuardScriptConfig.name} script!`)
})
}
function startMain() {
const compiler = webpack(mainConfig)
const { name } = compiler
compiler.hooks.afterEmit.tap('afterEmit', async () => {
console.log(`\nCompiled ${name} script!`)
manualRestart = true
await restartElectron()
setTimeout(() => {
manualRestart = false
}, 2500)
console.log(`\nWatching file changes for ${name} script...`)
})
compiler.watch({
aggregateTimeout: 500,
},
err => {
if (err) console.error(err)
})
}
function startPreload() {
const compiler = webpack(preloadConfig)
const { name } = compiler
let firstTime = true
compiler.hooks.afterEmit.tap('afterEmit', async () => {
console.log(`\nCompiled ${name} script!`)
if (firstTime) {
firstTime = false
} else {
manualRestart = true
await restartElectron()
setTimeout(() => {
manualRestart = false
}, 2500)
}
console.log(`\nWatching file changes for ${name} script...`)
})
compiler.watch({
aggregateTimeout: 500,
},
err => {
if (err) console.error(err)
})
}
function startRenderer(callback) {
const compiler = webpack(rendererConfig)
const { name } = compiler
compiler.hooks.afterEmit.tap('afterEmit', () => {
console.log(`\nCompiled ${name} script!`)
console.log(`\nWatching file changes for ${name} script...`)
})
const server = new WebpackDevServer({
client: {
overlay: {
runtimeErrors: false
}
},
static: [
{
directory: path.resolve(__dirname, '..', 'static'),
watch: {
ignored: [
/(dashFiles|storyboards)\/*/,
'/**/.DS_Store',
'**/static/locales/*'
]
},
publicPath: '/static'
},
{
directory: path.resolve(__dirname, '..', 'node_modules', 'shaka-player', 'ui', 'locales'),
publicPath: '/static/shaka-player-locales',
watch: {
// Ignore everything that isn't one of the locales that we would bundle in production mode
ignored: `**/!(${SHAKA_LOCALES_TO_BE_BUNDLED.join('|')}).json`
}
}
],
port
}, compiler)
server.startCallback(err => {
if (err) console.error(err)
setupNotifyLocaleUpdate(compiler, server)
callback()
})
}
function startWeb () {
const compiler = webpack(webConfig)
const { name } = compiler
compiler.hooks.afterEmit.tap('afterEmit', () => {
console.log(`\nCompiled ${name} script!`)
console.log(`\nWatching file changes for ${name} script...`)
})
const server = new WebpackDevServer({
open: true,
static: {
directory: path.resolve(__dirname, '..', 'static'),
watch: {
ignored: [
/(dashFiles|storyboards)\/*/,
'/**/.DS_Store',
'**/static/locales/*'
]
}
},
port
}, compiler)
server.startCallback(err => {
if (err) console.error(err)
setupNotifyLocaleUpdate(compiler, server)
})
}
if (!web) {
startRenderer(() => {
startBotGuardScript()
startPreload()
startMain()
})
} else {
startWeb()
}
================================================
FILE: _scripts/ebuilder.config.mjs
================================================
import packageDetails from '../package.json' with { type: 'json' }
/** @type {import('electron-builder').Configuration} */
export default {
appId: `io.freetubeapp.${packageDetails.name}`,
copyright: 'Copyleft © 2020-2026 freetubeapp@protonmail.com',
// asar: false,
// compression: 'store',
productName: packageDetails.productName,
directories: {
output: './build/',
},
protocols: [
{
name: 'FreeTube',
schemes: [
'freetube'
]
}
],
files: [
'_icons/iconColor.*',
'icon.svg',
'./dist/**/*',
'!dist/web/*',
'!node_modules/**/*',
],
// As we bundle all dependecies with webpack, the `node_modules` folder is excluded from packaging in the `files` array.
// electron-builder will however still spend time scanning the `node_modules` folder and building up a list of dependencies,
// returning `false` from the `beforeBuild` hook skips that.
beforeBuild: () => Promise.resolve(false),
dmg: {
contents: [
{
path: '/Applications',
type: 'link',
x: 410,
y: 230,
},
{
type: 'file',
x: 130,
y: 230,
},
],
window: {
height: 380,
width: 540,
}
},
linux: {
category: 'Network',
icon: '_icons/icon.svg',
target: ['deb', 'zip', '7z', 'rpm', 'AppImage', 'pacman'],
},
// See the following issues for more information
// https://github.com/jordansissel/fpm/issues/1503
// https://github.com/jgraph/drawio-desktop/issues/259
rpm: {
fpm: ['--rpm-rpmbuild-define=_build_id_links none']
},
deb: {
depends: [
'libgtk-3-0',
'libnotify4',
'libnss3',
'libxss1',
'libxtst6',
'xdg-utils',
'libatspi2.0-0',
'libuuid1',
'libsecret-1-0'
]
},
mac: {
category: 'public.app-category.utilities',
icon: '_icons/iconMac.icns',
target: ['dmg', 'zip', '7z'],
type: 'distribution',
extendInfo: {
CFBundleURLTypes: [
'freetube'
],
CFBundleURLSchemes: [
'freetube'
],
// Clear the default usage descriptions in the Info.plist file set by Electron that we don't need
// see: https://github.com/electron/electron/blob/main/shell/browser/resources/mac/Info.plist
NSAudioCaptureUsageDescription: undefined,
NSBluetoothAlwaysUsageDescription: undefined,
NSBluetoothPeripheralUsageDescription: undefined,
NSCameraUsageDescription: undefined,
NSMicrophoneUsageDescription: undefined,
}
},
win: {
icon: '_icons/icon.ico',
target: ['nsis', 'zip', '7z', 'portable'],
},
nsis: {
allowToChangeInstallationDirectory: true,
oneClick: false,
},
}
================================================
FILE: _scripts/eslint-rules/plugin.mjs
================================================
import preferUseI18nPolyfillRule from './prefer-use-i18n-polyfill-rule.mjs'
export default {
meta: {
name: 'eslint-plugin-freetube',
version: '1.0'
},
rules: {
'prefer-use-i18n-polyfill': preferUseI18nPolyfillRule
}
}
================================================
FILE: _scripts/eslint-rules/prefer-use-i18n-polyfill-rule.mjs
================================================
import { dirname, relative, resolve } from 'path'
const polyfillPath = resolve(import.meta.dirname, '../../src/renderer/composables/use-i18n-polyfill')
function getRelativePolyfillPath(filePath) {
const relativePath = relative(dirname(filePath), polyfillPath).replaceAll('\\', '/')
if (relativePath[0] !== '.') {
return `./${relativePath}`
}
return relativePath
}
/** @type {import('eslint').Rule.RuleModule} */
export default {
meta: {
type: 'problem',
fixable: 'code'
},
create(context) {
return {
'ImportDeclaration[source.value="vue-i18n"]'(node) {
const specifierIndex = node.specifiers.findIndex(specifier => specifier.type === 'ImportSpecifier' && specifier.imported.name === 'useI18n')
if (specifierIndex !== -1) {
context.report({
node: node.specifiers.length === 1 ? node : node.specifiers[specifierIndex],
message: "Please use FreeTube's useI18n polyfill, as vue-i18n's useI18n composable does not work when the vue-i18n is in legacy mode, which is needed for components using the Options API.",
fix: context.physicalFilename === '<text>'
? undefined
: (fixer) => {
const relativePath = getRelativePolyfillPath(context.physicalFilename)
// If the import only imports `useI18n`, we can just update the source/from text
// Else we need to create a new import for `useI18n` and remove useI18n from the original one
if (node.specifiers.length === 1) {
return fixer.replaceText(node.source, `'${relativePath}'`)
} else {
const specifier = node.specifiers[specifierIndex]
let specifierText = 'useI18n'
if (specifier.imported.name !== specifier.local.name) {
specifierText += ` as ${specifier.local.name}`
}
return [
fixer.removeRange([
specifierIndex === 0 ? specifier.start : node.specifiers[specifierIndex - 1].end,
specifierIndex === node.specifiers.length - 1 ? specifier.end : node.specifiers[specifierIndex + 1].start
]),
fixer.insertTextAfter(node, `\nimport { ${specifierText} } from '${relativePath}'`)
]
}
}
})
}
}
}
}
}
================================================
FILE: _scripts/findMissingTemplates.mjs
================================================
import { readdirSync, readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { load as loadYaml } from 'js-yaml'
const localesPath = join(import.meta.dirname, '..', 'static', 'locales')
const defaultLocale = 'en-US.yaml'
const errors = [
]
const defaultData = loadYaml(readFileSync(`${localesPath}/${defaultLocale}`, { encoding: 'utf-8' }))
const defaultKeys = Object.keys(defaultData)
const filesInLocaleDir = readdirSync(localesPath)
for (const file of filesInLocaleDir) {
if (file !== defaultLocale && file.endsWith('.yaml')) {
const fileData = loadYaml(readFileSync(`${localesPath}/${file}`, { encoding: 'utf-8' }))
const fileDataKeys = Object.keys(fileData)
addErrors(defaultData, fileData, defaultKeys, fileDataKeys, file)
}
}
writeFileSync('locale-errors.json', JSON.stringify(errors, null, 2))
if (errors.length > 0) {
console.error(errors)
} else {
console.log('no issues found')
}
/**
* @param {unknown} originalData - data from en-US converted to a JavaScript object
* @param {unknown} newData - data from the file we are analyzing converted to a JavaScript object
* @param {string[]} originalKeys - keys from en-US file
* @param {string[]} newKeys - keys from the file we are currently analyzing
* @param {string} file - the file we are currently analyzing
*/
function addErrors(originalData, newData, originalKeys, newKeys, file) {
newKeys.forEach(newKey => {
if (originalKeys.includes(newKey)) {
if (typeof originalData[newKey] === 'object') {
addErrors(originalData[newKey], newData[newKey], Object.keys(originalData[newKey]), Object.keys(newData[newKey]), file)
} else if (isMissingInterpolation(originalData[newKey], newData[newKey], file)) {
errors.push({ fileName: file, error: 'value is missing a template or has an extra template', key: newKey, defaultValue: originalData[newKey], value: newData[newKey] })
}
} else {
// The key doesn't exist in the en-US file but exists in current yaml file.
// We should go through this eventually but it's not as important as invalid templates
// errors.push({ fileName: file, error: 'extra key found', key: fdk })
}
})
}
/**
*
* @param {String} defaultValue
* @param {String} otherValue
*/
function isMissingInterpolation(defaultValue, otherValue, filename) {
if (otherValue === '') {
// not translated yet, we don't care
return false
}
const defaultMatches = Array.from(new Set(defaultValue.match(/{[^}]*}/g)))
const otherMatches = Array.from(new Set(otherValue.match(/{[^}]*}/g)))
if (defaultMatches) {
if (!otherMatches) {
// no templates found.
return true
}
defaultMatches.sort()
otherMatches.sort()
const defaultMatchesStringified = JSON.stringify(defaultMatches)
const otherMatchesStringified = JSON.stringify(otherMatches)
// check if templates match.
return defaultMatchesStringified !== otherMatchesStringified
} else if (otherMatches) {
// extra template found
return true
}
}
================================================
FILE: _scripts/getInstances.js
================================================
const fs = require('fs/promises')
const invidiousApiUrl = 'https://api.invidious.io/instances.json'
fetch(invidiousApiUrl).then(e => e.json()).then(res => {
const data = res.filter((instance) => {
return !(instance[0].includes('.onion') ||
instance[0].includes('.i2p') ||
!instance[1].api)
}).map((instance) => {
return {
url: instance[1].uri.replace(/\/$/, ''),
cors: instance[1].cors
}
})
fs.writeFile('././static/invidious-instances.json', JSON.stringify(data, null, 2))
})
================================================
FILE: _scripts/getRegions.mjs
================================================
/**
* This script updates the files in static/geolocations with the available locations on YouTube.
*
* It tries to map every active FreeTube language (static/locales/activelocales.json)
* to it's equivalent on YouTube.
*
* It then uses those language mappings,
* to scrape the location selection menu on the YouTube website, in every mapped language.
*
* All languages it couldn't find on YouTube, that don't have manually added mapping,
* get logged to the console, as well as all unmapped YouTube languages.
*/
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs'
import { Innertube, Misc } from 'youtubei.js'
const STATIC_DIRECTORY = `${import.meta.dirname}/../static`
const activeLanguagesPath = `${STATIC_DIRECTORY}/locales/activeLocales.json`
/** @type {string[]} */
const activeLanguages = JSON.parse(readFileSync(activeLanguagesPath, { encoding: 'utf8' }))
// en-US is en on YouTube
const initialResponse = await scrapeLanguage('en')
// Scrape language menu in en-US
/** @type {string[]} */
const youTubeLanguages = initialResponse.data.actions[0].openPopupAction.popup.multiPageMenuRenderer.sections[0].multiPageMenuSectionRenderer.items[2].compactLinkRenderer.serviceEndpoint.signalServiceEndpoint.actions[0].getMultiPageMenuAction.menu.multiPageMenuRenderer.sections[0].multiPageMenuSectionRenderer.items
.map(({ compactLinkRenderer }) => {
return compactLinkRenderer.serviceEndpoint.signalServiceEndpoint.actions[0].selectLanguageCommand.hl
})
// map FreeTube languages to their YouTube equivalents
const foundLanguageNames = ['en-US']
const unusedYouTubeLanguageNames = []
const languagesToScrape = []
for (const language of youTubeLanguages) {
if (activeLanguages.includes(language)) {
foundLanguageNames.push(language)
languagesToScrape.push({
youTube: language,
freeTube: language
})
// eslint-disable-next-line @stylistic/brace-style
}
// special cases
else if (language === 'de') {
foundLanguageNames.push('de-DE')
languagesToScrape.push({
youTube: 'de',
freeTube: 'de-DE'
})
} else if (language === 'fr') {
foundLanguageNames.push('fr-FR')
languagesToScrape.push({
youTube: 'fr',
freeTube: 'fr-FR'
})
} else if (language === 'no') {
// according to https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
// "no" is the macro language for "nb" and "nn"
foundLanguageNames.push('nb-NO', 'nn')
languagesToScrape.push({
youTube: 'no',
freeTube: 'nb-NO'
})
languagesToScrape.push({
youTube: 'no',
freeTube: 'nn'
})
} else if (language === 'iw') {
// according to https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
// "iw" is the old/original code for Hebrew, these days it's "he"
foundLanguageNames.push('he')
languagesToScrape.push({
youTube: 'iw',
freeTube: 'he'
})
} else if (language === 'es-419') {
foundLanguageNames.push('es-AR', 'es-MX')
languagesToScrape.push({
youTube: 'es-419',
freeTube: 'es-AR'
})
languagesToScrape.push({
youTube: 'es-419',
freeTube: 'es-MX'
})
} else if (language !== 'en') {
unusedYouTubeLanguageNames.push(language)
}
}
foundLanguageNames.push('pt-BR')
languagesToScrape.push({
youTube: 'pt',
freeTube: 'pt-BR'
})
console.log("Active FreeTube languages that aren't available on YouTube:")
console.log(activeLanguages.filter(lang => !foundLanguageNames.includes(lang)).sort())
console.log("YouTube languages that don't have an equivalent active FreeTube language:")
console.log(unusedYouTubeLanguageNames.sort())
// Scrape the location menu in various languages and write files to the file system
rmSync(`${STATIC_DIRECTORY}/geolocations`, { recursive: true })
mkdirSync(`${STATIC_DIRECTORY}/geolocations`)
processGeolocations('en-US', 'en', initialResponse)
for (const { youTube, freeTube } of languagesToScrape) {
const response = await scrapeLanguage(youTube)
processGeolocations(freeTube, youTube, response)
}
/**
* @param {string} youTubeLanguageCode
*/
async function scrapeLanguage(youTubeLanguageCode) {
const session = await Innertube.create({
retrieve_player: false,
generate_session_locally: true,
lang: youTubeLanguageCode
})
return await session.actions.execute('/account/account_menu')
}
/**
* @param {string} freeTubeLanguage
* @param {string} youTubeLanguage
* @param {import('youtubei.js').ApiResponse} response
*/
function processGeolocations(freeTubeLanguage, youTubeLanguage, response) {
/** @type {{ name: string, code: string }[]} */
const geolocations = response.data.actions[0].openPopupAction.popup.multiPageMenuRenderer.sections[0].multiPageMenuSectionRenderer.items[4].compactLinkRenderer.serviceEndpoint.signalServiceEndpoint.actions[0].getMultiPageMenuAction.menu.multiPageMenuRenderer.sections[0].multiPageMenuSectionRenderer.items
.map(({ compactLinkRenderer }) => {
return {
name: new Misc.Text(compactLinkRenderer.title).toString().trim(),
code: compactLinkRenderer.serviceEndpoint.signalServiceEndpoint.actions[0].selectCountryCommand.gl
}
})
const normalisedFreeTubeLanguage = freeTubeLanguage.replace('_', '-')
// give Intl.Collator 4 locales, in the hopes that it supports one of them
// deduplicate the list so it doesn't have to do duplicate work
const localeSet = new Set()
localeSet.add(normalisedFreeTubeLanguage)
localeSet.add(youTubeLanguage)
localeSet.add(normalisedFreeTubeLanguage.split('-')[0])
localeSet.add(youTubeLanguage.split('-')[0])
const locales = Array.from(localeSet)
// only sort if node supports sorting the language, otherwise hope that YouTube's sorting was correct
// node 20.3.1 doesn't support sorting `eu`
if (Intl.Collator.supportedLocalesOf(locales).length > 0) {
const collator = new Intl.Collator(locales)
geolocations.sort((a, b) => collator.compare(a.name, b.name))
}
const output = {
names: geolocations.map(entry => entry.name),
codes: geolocations.map(entry => entry.code)
}
writeFileSync(`${STATIC_DIRECTORY}/geolocations/${freeTubeLanguage}.json`, JSON.stringify(output))
}
================================================
FILE: _scripts/getShakaLocales.js
================================================
const { readFileSync, readdirSync } = require('fs')
const { join } = require('path')
function getPreloadedLocales() {
const localesFile = readFileSync(join(__dirname, '../node_modules/shaka-player/dist/locales.js'), 'utf-8')
const localesLine = localesFile.match(/^\/\/ LOCALES: ([\w ,-]+)$/m)
if (!localesLine) {
throw new Error("Failed to parse shaka-player's preloaded locales")
}
return localesLine[1].split(',').map(locale => locale.trim())
}
function getAllLocales() {
const filenames = readdirSync(join(__dirname, '../node_modules/shaka-player/ui/locales'))
return new Set(filenames
.filter(filename => filename !== 'source.json' && filename.endsWith('.json'))
.map(filename => filename.replace('.json', '')))
}
/**
* Maps the shaka locales to FreeTube's active ones
* This allows us to know which locale files are actually needed
* and which shaka locale needs to be activated for a given FreeTube one.
* @param {Set<string>} shakaLocales
* @param {string[]} freeTubeLocales
*/
function getMappings(shakaLocales, freeTubeLocales) {
/**
* @type {[string, string][]}
* Using this structure as it gets passed to `new Map()` in the player component
* The first element is the FreeTube locale, the second one is the shaka-player one
*/
const mappings = []
for (const locale of freeTubeLocales) {
if (shakaLocales.has(locale)) {
mappings.push([
locale,
locale
])
} else if (shakaLocales.has(locale.split('-')[0])) {
mappings.push([
locale,
locale.split('-')[0]
])
}
}
// special cases
mappings.push(
// according to https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
// "no" is the macro language for "nb" and "nn"
[
'nb-NO',
'no'
],
[
'nn',
'no'
],
// according to https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
// "iw" is the old/original code for Hebrew, these days it's "he"
[
'he',
'iw'
],
// not sure why we have pt, pt-PT and pt-BR in the FreeTube locales
// as pt and pt-PT are the same thing, but we should handle it here anyway
[
'pt',
'pt-PT'
]
)
return mappings
}
function getShakaLocales() {
const shakaLocales = getAllLocales()
/** @type {string[]} */
const freeTubeLocales = JSON.parse(readFileSync(join(__dirname, '../static/locales/activeLocales.json'), 'utf-8'))
const mappings = getMappings(shakaLocales, freeTubeLocales)
const preloaded = getPreloadedLocales()
const shakaMappings = mappings.map(mapping => mapping[1])
// use a set to deduplicate the list
// we don't need to bundle any locale files that are already embedded in shaka-player/preloaded
/** @type {string[]} */
const toBeBundled = [...new Set(shakaMappings.filter(locale => !preloaded.includes(locale)))]
return {
SHAKA_LOCALE_MAPPINGS: mappings,
SHAKA_LOCALES_PREBUNDLED: preloaded,
SHAKA_LOCALES_TO_BE_BUNDLED: toBeBundled
}
}
module.exports = getShakaLocales()
================================================
FILE: _scripts/injectAllowedPaths.mjs
================================================
/**
* Injects the paths that the renderer process is allowed to read into the main.js file,
* by replacing __FREETUBE_ALLOWED_PATHS__ with an array of strings with the paths.
*
* This allows the main process to validate the paths which the renderer process accesses,
* to ensure that it cannot access other files on the disk, without the users permission (e.g. file picker).
*/
import { closeSync, ftruncateSync, openSync, readFileSync, readdirSync, writeSync } from 'fs'
import { join, relative, resolve } from 'path'
const distDirectory = resolve(import.meta.dirname, '..', 'dist')
const webDirectory = join(distDirectory, 'web')
const paths = readdirSync(distDirectory, {
recursive: true,
withFileTypes: true
})
.filter(dirent => {
// only include files not directories
return dirent.isFile() &&
// disallow the renderer process/browser windows to read the main.js file
dirent.name !== 'main.js' &&
dirent.name !== 'main.js.LICENSE.txt' &&
// disallow the renderer process/browser windows to read the preload.js file
dirent.name !== 'preload.js' &&
// disallow the renderer process/browser windows to read the botGuardScript.js file
dirent.name !== 'botGuardScript.js' &&
// filter out any web build files, in case the dist directory contains a web build
!dirent.parentPath.startsWith(webDirectory)
})
.map(dirent => {
const joined = join(dirent.parentPath, dirent.name)
return '/' + relative(distDirectory, joined).replaceAll('\\', '/')
})
let fileHandle
try {
fileHandle = openSync(join(distDirectory, 'main.js'), 'r+')
let contents = readFileSync(fileHandle, 'utf-8')
contents = contents.replace('__FREETUBE_ALLOWED_PATHS__', JSON.stringify(paths))
ftruncateSync(fileHandle)
writeSync(fileHandle, contents, 0, 'utf-8')
} finally {
if (typeof fileHandle !== 'undefined') {
closeSync(fileHandle)
}
}
================================================
FILE: _scripts/mime-db-shrinking-loader.js
================================================
/**
* electron-context-menu only needs mime-db for its save as feature.
* As we only activate save image and save as image features, we can remove all other mimetypes,
* as they will never get used.
* Which results in quite a significant reduction in file size.
* @param {string} source
*/
module.exports = function (source) {
const original = JSON.parse(source)
// Only the extensions field is needed, see: https://github.com/kevva/ext-list/blob/v2.2.2/index.js
return JSON.stringify({
'image/apng': { extensions: original['image/apng'].extensions },
'image/avif': { extensions: original['image/avif'].extensions },
'image/gif': { extensions: original['image/gif'].extensions },
'image/jpeg': { extensions: original['image/jpeg'].extensions },
'image/png': { extensions: original['image/png'].extensions },
'image/svg+xml': { extensions: original['image/svg+xml'].extensions },
'image/webp': { extensions: original['image/webp'].extensions }
})
}
================================================
FILE: _scripts/patch-shaka-player-loader.js
================================================
/**
* fixes shaka-player referencing the Roboto font on google fonts in its CSS
* by updating the CSS to point to the local Roboto font
* @param {string} source
*/
module.exports = function (source) {
return source.replace(/@font-face{font-family:Roboto;[^}]+}/, '')
}
================================================
FILE: _scripts/sigFrameConfig.js
================================================
const { hash } = require('crypto')
const { join } = require('path')
const { readFileSync } = require('fs')
const path = join(__dirname, '../src/renderer/sigFrameScript.js')
const rawScript = readFileSync(path, 'utf8')
const script = require('terser').minify_sync({ [path]: rawScript }).code
module.exports.sigFrameTemplateParameters = {
sigFrameSrc: `data:text/html,${encodeURIComponent(`<!doctype html><script>${script}</script>`)}`,
sigFrameCspHash: `sha512-${hash('sha512', script, 'base64')}`
}
================================================
FILE: _scripts/webpack.botGuardScript.config.js
================================================
const path = require('path')
/** @type {import('webpack').Configuration} */
module.exports = {
name: 'botGuardScript',
// Always use production mode, as we use the output as a function body and the debug output doesn't work for that
mode: 'production',
devtool: false,
target: 'web',
entry: {
botGuardScript: path.join(__dirname, '../src/botGuardScript.js'),
},
output: {
filename: '[name].js',
path: path.join(__dirname, '../dist'),
library: {
type: 'modern-module'
}
},
experiments: {
outputModule: true
}
}
================================================
FILE: _scripts/webpack.main.config.js
================================================
const path = require('path')
const webpack = require('webpack')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const JsonMinimizerPlugin = require('json-minimizer-webpack-plugin')
const isDevMode = process.env.NODE_ENV === 'development'
/** @type {import('webpack').Configuration} */
const config = {
name: 'main',
mode: process.env.NODE_ENV,
devtool: isDevMode ? 'eval-cheap-module-source-map' : false,
entry: {
main: path.join(__dirname, '../src/main/index.js'),
},
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/,
},
{
resource: path.resolve(__dirname, '../node_modules/mime-db/db.json'),
use: path.join(__dirname, 'mime-db-shrinking-loader.js')
}
],
generator: {
json: {
JSONParse: false
}
}
},
// webpack defaults to only optimising the production builds, so having this here is fine
optimization: {
minimizer: [
'...', // extend webpack's list instead of overwriting it
new JsonMinimizerPlugin({
exclude: /\/locales\/.*\.json/
})
]
},
node: {
__dirname: isDevMode,
__filename: isDevMode
},
plugins: [
new webpack.DefinePlugin({
'process.platform': `'${process.platform}'`,
'process.env.IS_ELECTRON_MAIN': true
})
],
output: {
filename: '[name].js',
libraryTarget: 'commonjs2',
path: path.join(__dirname, '../dist'),
},
target: 'electron-main',
}
if (!isDevMode) {
config.plugins.push(
new CopyWebpackPlugin({
patterns: [
{
from: path.join(__dirname, '../static'),
to: path.join(__dirname, '../dist/static'),
globOptions: {
dot: true,
ignore: ['**/.*', '**/locales/**', '**/pwabuilder-sw.js', '**/manifest.json', '**/dashFiles/**', '**/storyboards/**'],
},
},
]
})
)
}
module.exports = config
================================================
FILE: _scripts/webpack.preload.config.js
================================================
const path = require('path')
const isDevMode = process.env.NODE_ENV === 'development'
/** @type {import('webpack').Configuration} */
const config = {
name: 'preload',
mode: process.env.NODE_ENV,
devtool: isDevMode ? 'eval-cheap-module-source-map' : false,
entry: {
preload: path.join(__dirname, '../src/preload/main.js'),
},
infrastructureLogging: {
// Only warnings and errors
// level: 'none' disable logging
// Please read https://webpack.js.org/configuration/other-options/#infrastructurelogginglevel
level: isDevMode ? 'info' : 'none'
},
output: {
path: path.join(__dirname, '../dist'),
filename: '[name].js'
},
externals: [
'electron/renderer'
],
externalsType: 'commonjs',
node: {
__dirname: false,
__filename: false
},
target: 'electron-preload',
}
module.exports = config
================================================
FILE: _scripts/webpack.renderer.config.js
================================================
const path = require('path')
const { readFileSync, readdirSync } = require('fs')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const ProcessLocalesPlugin = require('./ProcessLocalesPlugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const {
SHAKA_LOCALE_MAPPINGS,
SHAKA_LOCALES_PREBUNDLED,
SHAKA_LOCALES_TO_BE_BUNDLED
} = require('./getShakaLocales')
const { sigFrameTemplateParameters } = require('./sigFrameConfig')
const isDevMode = process.env.NODE_ENV === 'development'
const { version: swiperVersion } = JSON.parse(readFileSync(path.join(__dirname, '../node_modules/swiper/package.json')))
const processLocalesPlugin = new ProcessLocalesPlugin({
compress: !isDevMode,
hotReload: isDevMode,
inputDir: path.join(__dirname, '../static/locales'),
outputDir: 'static/locales',
})
/** @type {import('webpack').Configuration} */
const config = {
name: 'renderer',
mode: process.env.NODE_ENV,
devtool: isDevMode ? 'eval-cheap-module-source-map' : false,
entry: {
renderer: path.join(__dirname, '../src/renderer/main.js'),
},
infrastructureLogging: {
// Only warnings and errors
// level: 'none' disable logging
// Please read https://webpack.js.org/configuration/other-options/#infrastructurelogginglevel
level: isDevMode ? 'info' : 'none'
},
output: {
scriptType: 'text/javascript',
path: path.join(__dirname, '../dist'),
filename: '[name].js',
},
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/,
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
compilerOptions: {
isCustomElement: (tag) => tag === 'swiper-container' || tag === 'swiper-slide'
}
}
},
{
test: /\.scss$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
},
{
loader: 'css-loader',
options: {
esModule: false
}
},
{
loader: 'sass-loader',
options: {
implementation: require('sass')
}
},
],
},
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader
},
{
loader: 'css-loader',
options: {
esModule: false
}
}
],
rules: [
{
resource: path.resolve(__dirname, '../node_modules/shaka-player/dist/controls.css'),
use: path.join(__dirname, 'patch-shaka-player-loader.js')
}
],
},
{
test: /\.(png|jpe?g|gif|tif?f|bmp|webp|svg)(\?.*)?$/,
type: 'asset/resource',
generator: {
filename: 'imgs/[name][ext]'
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
type: 'asset/resource',
generator: {
filename: 'fonts/[name][ext]'
}
},
],
generator: {
json: {
JSONParse: false
}
}
},
// webpack defaults to only optimising the production builds, so having this here is fine
optimization: {
minimizer: [
'...', // extend webpack's list instead of overwriting it
new CssMinimizerPlugin()
]
},
node: {
__dirname: false,
__filename: false
},
plugins: [
processLocalesPlugin,
new webpack.DefinePlugin({
'process.platform': `'${process.platform}'`,
'process.env.IS_ELECTRON': true,
'process.env.IS_ELECTRON_MAIN': false,
'process.env.SUPPORTS_LOCAL_API': true,
__VUE_OPTIONS_API__: 'true',
__VUE_PROD_DEVTOOLS__: 'false',
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false',
__VUE_I18N_LEGACY_API__: 'true',
__VUE_I18N_FULL_INSTALL__: 'false',
__INTLIFY_PROD_DEVTOOLS__: 'false',
'process.env.LOCALE_NAMES': JSON.stringify(processLocalesPlugin.localeNames),
'process.env.GEOLOCATION_NAMES': JSON.stringify(readdirSync(path.join(__dirname, '..', 'static', 'geolocations')).map(filename => filename.replace('.json', ''))),
'process.env.SWIPER_VERSION': `'${swiperVersion}'`,
'process.env.SHAKA_LOCALE_MAPPINGS': JSON.stringify(SHAKA_LOCALE_MAPPINGS),
'process.env.SHAKA_LOCALES_PREBUNDLED': JSON.stringify(SHAKA_LOCALES_PREBUNDLED)
}),
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.resolve(__dirname, '../src/index.ejs'),
templateParameters: sigFrameTemplateParameters
}),
new VueLoaderPlugin(),
new MiniCssExtractPlugin({
filename: isDevMode ? '[name].css' : '[name].[contenthash].css',
chunkFilename: isDevMode ? '[id].css' : '[id].[contenthash].css',
}),
new CopyWebpackPlugin({
patterns: [
{
from: path.join(__dirname, '../node_modules/swiper/modules/{a11y,navigation,pagination}-element.css').replaceAll('\\', '/'),
to: `swiper-${swiperVersion}.css`,
context: path.join(__dirname, '../node_modules/swiper/modules'),
transformAll: (assets) => {
return Buffer.concat(assets.map(asset => asset.data))
}
},
// Don't need to copy them in dev mode,
// as we configure WebpackDevServer to serve them
...(isDevMode
? []
: [
{
from: path.join(__dirname, '../node_modules/shaka-player/ui/locales', `{${SHAKA_LOCALES_TO_BE_BUNDLED.join(',')}}.json`).replaceAll('\\', '/'),
to: path.join(__dirname, '../dist/static/shaka-player-locales'),
context: path.join(__dirname, '../node_modules/shaka-player/ui/locales'),
transform: {
transformer: (input) => {
return JSON.stringify(JSON.parse(input.toString('utf-8')))
}
}
}
])
]
})
],
resolve: {
alias: {
DB_HANDLERS_ELECTRON_RENDERER_OR_WEB$: path.resolve(__dirname, '../src/datastores/handlers/electron.js'),
'youtubei.js$': 'youtubei.js/web',
// change to "shaka-player.ui-es2021.debug.js" to get debug logs (update jsconfig to get updated types)
'shaka-player$': 'shaka-player/dist/shaka-player.ui-es2021.js',
// Make @fortawesome/vue-fontawesome use the trimmed down API instead of the original @fortawesome/fontawesome-svg-core
'@fortawesome/fontawesome-svg-core$': path.resolve(__dirname, '../src/renderer/fontawesome-minimal.js'),
// Fix dompurify not being tree-shaking friendly
dompurify$: path.resolve(__dirname, '_undefinedDefaultExport.mjs')
},
extensions: ['.js', '.vue']
},
target: 'web',
}
if (isDevMode) {
// hack to pass it through to the dev-runner.js script
// gets removed there before the config object is passed to webpack
config.SHAKA_LOCALES_TO_BE_BUNDLED = SHAKA_LOCALES_TO_BE_BUNDLED
}
module.exports = config
================================================
FILE: _scripts/webpack.web.config.js
================================================
const path = require('path')
const fs = require('fs')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const JsonMinimizerPlugin = require('json-minimizer-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const ProcessLocalesPlugin = require('./ProcessLocalesPlugin')
const {
SHAKA_LOCALE_MAPPINGS,
SHAKA_LOCALES_PREBUNDLED,
SHAKA_LOCALES_TO_BE_BUNDLED
} = require('./getShakaLocales')
const isDevMode = process.env.NODE_ENV === 'development'
const { version: swiperVersion } = JSON.parse(fs.readFileSync(path.join(__dirname, '../node_modules/swiper/package.json')))
/** @type {import('webpack').Configuration} */
const config = {
name: 'web',
mode: process.env.NODE_ENV,
devtool: isDevMode ? 'eval-cheap-module-source-map' : false,
entry: {
web: path.join(__dirname, '../src/renderer/main.js'),
},
output: {
path: path.join(__dirname, '../dist/web'),
filename: '[name].js',
},
externals: {
'youtubei.js': '{}',
googlevideo: '{}'
},
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/,
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
compilerOptions: {
isCustomElement: (tag) => tag === 'swiper-container' || tag === 'swiper-slide',
}
}
},
{
test: /\.scss$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
},
{
loader: 'css-loader',
options: {
esModule: false
}
},
{
loader: 'sass-loader',
options: {
implementation: require('sass')
}
},
],
},
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader
},
{
loader: 'css-loader',
options: {
esModule: false
}
}
],
rules: [
{
resource: path.resolve(__dirname, '../node_modules/shaka-player/dist/controls.css'),
use: path.join(__dirname, 'patch-shaka-player-loader.js')
}
],
},
{
test: /\.html$/,
use: 'vue-html-loader',
},
{
test: /\.(png|jpe?g|gif|tif?f|bmp|webp|svg)(\?.*)?$/,
type: 'asset/resource',
generator: {
filename: 'imgs/[name][ext]'
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
type: 'asset/resource',
generator: {
filename: 'fonts/[name][ext]'
}
},
],
generator: {
json: {
JSONParse: false
}
}
},
// webpack defaults to only optimising the production builds, so having this here is fine
optimization: {
minimizer: [
'...', // extend webpack's list instead of overwriting it
new JsonMinimizerPlugin({
exclude: /\/locales\/.*\.json/
}),
new CssMinimizerPlugin()
]
},
node: {
__dirname: false,
__filename: false
},
plugins: [
new webpack.DefinePlugin({
'process.platform': 'undefined',
'process.env.IS_ELECTRON': false,
'process.env.IS_ELECTRON_MAIN': false,
'process.env.SUPPORTS_LOCAL_API': false,
__VUE_OPTIONS_API__: 'true',
__VUE_PROD_DEVTOOLS__: 'false',
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false',
__VUE_I18N_LEGACY_API__: 'true',
__VUE_I18N_FULL_INSTALL__: 'false',
__INTLIFY_PROD_DEVTOOLS__: 'false',
'process.env.SWIPER_VERSION': `'${swiperVersion}'`
}),
new webpack.ProvidePlugin({
process: 'process/browser.js'
}),
new HtmlWebpackPlugin({
excludeChunks: ['processTaskWorker'],
filename: 'index.html',
template: path.resolve(__dirname, '../src/index.ejs')
}),
new VueLoaderPlugin(),
new MiniCssExtractPlugin({
filename: isDevMode ? '[name].css' : '[name].[contenthash].css',
chunkFilename: isDevMode ? '[id].css' : '[id].[contenthash].css',
}),
new CopyWebpackPlugin({
patterns: [
{
from: path.join(__dirname, '../node_modules/swiper/modules/{a11y,navigation,pagination}-element.css').replaceAll('\\', '/'),
to: `swiper-${swiperVersion}.css`,
context: path.join(__dirname, '../node_modules/swiper/modules'),
transformAll: (assets) => {
return Buffer.concat(assets.map(asset => asset.data))
}
}
]
})
],
resolve: {
alias: {
DB_HANDLERS_ELECTRON_RENDERER_OR_WEB$: path.resolve(__dirname, '../src/datastores/handlers/web.js'),
// change to "shaka-player.ui-es2021.debug.js" to get debug logs (update jsconfig to get updated types)
'shaka-player$': 'shaka-player/dist/shaka-player.ui-es2021.js',
// Make @fortawesome/vue-fontawesome use the trimmed down API instead of the original @fortawesome/fontawesome-svg-core
'@fortawesome/fontawesome-svg-core$': path.resolve(__dirname, '../src/renderer/fontawesome-minimal.js')
},
extensions: ['.js', '.vue']
},
target: 'web',
}
const processLocalesPlugin = new ProcessLocalesPlugin({
compress: false,
hotReload: isDevMode,
inputDir: path.join(__dirname, '../static/locales'),
outputDir: 'static/locales',
})
config.plugins.push(
processLocalesPlugin,
new webpack.DefinePlugin({
'process.env.LOCALE_NAMES': JSON.stringify(processLocalesPlugin.localeNames),
'process.env.GEOLOCATION_NAMES': JSON.stringify(fs.readdirSync(path.join(__dirname, '..', 'static', 'geolocations')).map(filename => filename.replace('.json', ''))),
'process.env.SHAKA_LOCALE_MAPPINGS': JSON.stringify(SHAKA_LOCALE_MAPPINGS),
'process.env.SHAKA_LOCALES_PREBUNDLED': JSON.stringify(SHAKA_LOCALES_PREBUNDLED)
}),
new CopyWebpackPlugin({
patterns: [
{
from: path.join(__dirname, '../static/pwabuilder-sw.js'),
to: path.join(__dirname, '../dist/web/pwabuilder-sw.js'),
},
{
from: path.join(__dirname, '../static'),
to: path.join(__dirname, '../dist/web/static'),
globOptions: {
dot: true,
ignore: ['**/.*', '**/locales/**', '**/pwabuilder-sw.js', '**/dashFiles/**', '**/storyboards/**'],
},
},
{
from: path.join(__dirname, '../node_modules/shaka-player/ui/locales', `{${SHAKA_LOCALES_TO_BE_BUNDLED.join(',')}}.json`).replaceAll('\\', '/'),
to: path.join(__dirname, '../dist/web/static/shaka-player-locales'),
context: path.join(__dirname, '../node_modules/shaka-player/ui/locales')
}
]
})
)
module.exports = config
================================================
FILE: eslint.config.mjs
================================================
import eslintPluginVue from 'eslint-plugin-vue'
import vuejsAccessibility from 'eslint-plugin-vuejs-accessibility'
import eslintPluginUnicorn from 'eslint-plugin-unicorn'
import intlifyVueI18N from '@intlify/eslint-plugin-vue-i18n'
import globals from 'globals'
import vueEslintParser from 'vue-eslint-parser'
import js from '@eslint/js'
import eslintPluginJsonc from 'eslint-plugin-jsonc'
import eslintPluginYml from 'eslint-plugin-yml'
import jsdoc from 'eslint-plugin-jsdoc'
import stylistic from '@stylistic/eslint-plugin'
import eslintPluginImportX from 'eslint-plugin-import-x'
import eslintPluginN from 'eslint-plugin-n'
import eslintPluginPromise from 'eslint-plugin-promise'
import freetube from './_scripts/eslint-rules/plugin.mjs'
import activeLocales from './static/locales/activeLocales.json' with { type: 'json' }
export default [
{
ignores: [
'build/',
'dist/',
'eslint.config.mjs',
// The JSON files inside this directory are auto-generated, so they don't follow the code style rules
'static/geolocations/'
]
},
{
name: 'base',
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
globals: {
...globals.es2022,
...globals.node,
document: 'readonly',
navigator: 'readonly',
window: 'readonly',
},
},
plugins: {
'import-x': eslintPluginImportX,
n: eslintPluginN,
promise: eslintPluginPromise,
},
rules: {
'no-var': 'warn',
'object-shorthand': ['warn', 'properties'],
'accessor-pairs': ['error', { setWithoutGet: true, enforceForClassMembers: true }],
'array-callback-return': ['error', {
allowImplicit: false,
checkForEach: false,
}],
camelcase: ['error', {
allow: ['^UNSAFE_'],
properties: 'never',
ignoreGlobals: true,
}],
curly: ['error', 'multi-line'],
'default-case-last': 'error',
eqeqeq: ['error', 'always', { null: 'ignore' }],
'new-cap': ['error', { newIsCap: true, capIsNew: false, properties: true }],
'no-array-constructor': 'error',
'no-caller': 'error',
'no-constant-condition': ['error', { checkLoops: false }],
'no-empty': ['error', { allowEmptyCatch: true }],
'no-eval': 'error',
'no-extend-native': 'error',
'no-extra-bind': 'error',
'no-implied-eval': 'error',
'no-iterator': 'error',
'no-labels': ['error', { allowLoop: false, allowSwitch: false }],
'no-lone-blocks': 'error',
'no-multi-str': 'error',
'no-new': 'error',
'no-new-func': 'error',
'no-object-constructor': 'error',
'no-new-wrappers': 'error',
'no-octal-escape': 'error',
'no-proto': 'error',
'no-redeclare': ['error', { builtinGlobals: false }],
'no-return-assign': ['error', 'except-parens'],
'no-self-compare': 'error',
'no-sequences': 'error',
'no-template-curly-in-string': 'error',
'no-throw-literal': 'error',
'no-undef-init': 'error',
'no-unmodified-loop-condition': 'error',
'no-unneeded-ternary': ['error', { defaultAssignment: false }],
'no-unreachable-loop': 'error',
'no-unused-expressions': ['error', {
allowShortCircuit: true,
allowTernary: true,
allowTaggedTemplates: true,
}],
'no-unused-vars': ['error', {
args: 'none',
caughtErrors: 'none',
ignoreRestSiblings: true,
vars: 'all',
}],
'no-use-before-define': ['error', { functions: false, classes: false, variables: false }],
'no-useless-call': 'error',
'no-useless-computed-key': 'error',
'no-useless-constructor': 'error',
'no-useless-rename': 'error',
'no-useless-return': 'error',
'no-void': 'error',
'one-var': ['error', { initialized: 'never' }],
'prefer-const': ['error', { destructuring: 'all' }],
'prefer-promise-reject-errors': 'error',
'prefer-regex-literals': ['error', { disallowRedundantWrapping: true }],
'symbol-description': 'error',
'unicode-bom': ['error', 'never'],
'use-isnan': ['error', {
enforceForSwitchCase: true,
enforceForIndexOf: true,
}],
'valid-typeof': ['error', { requireStringLiterals: true }],
yoda: ['error', 'never'],
'import-x/export': 'error',
'import-x/first': 'error',
'import-x/no-absolute-path': ['error', { esmodule: true, commonjs: true, amd: false }],
'import-x/no-duplicates': 'error',
'import-x/no-named-default': 'error',
'import-x/no-webpack-loader-syntax': 'error',
'n/handle-callback-err': ['error', '^(err|error)$'],
'n/no-callback-literal': 'error',
'n/no-deprecated-api': 'warn',
'n/no-exports-assign': 'error',
'n/no-new-require': 'error',
'n/no-path-concat': 'error',
'n/process-exit-as-throw': 'error',
'promise/param-names': 'error',
},
},
{
name: 'style',
plugins: {
'@stylistic': stylistic,
},
rules: {
'@stylistic/array-bracket-spacing': ['error', 'never'],
'@stylistic/arrow-spacing': ['error', { before: true, after: true }],
'@stylistic/block-spacing': ['error', 'always'],
'@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: true }],
'@stylistic/comma-dangle': ['warn', {
arrays: 'ignore',
enums: 'ignore',
exports: 'ignore',
imports: 'ignore',
objects: 'ignore',
}],
'@stylistic/comma-spacing': ['error', { before: false, after: true }],
'@stylistic/comma-style': ['error', 'last'],
'@stylistic/computed-property-spacing': ['error', 'never', { enforceForClassMembers: true }],
'@stylistic/dot-location': ['error', 'property'],
'@stylistic/eol-last': 'error',
'@stylistic/function-call-spacing': ['error', 'never'],
'@stylistic/generator-star-spacing': ['error', { before: true, after: true }],
'@stylistic/indent': ['error', 2, {
SwitchCase: 1,
VariableDeclarator: 1,
outerIIFEBody: 1,
MemberExpression: 1,
FunctionDeclaration: { parameters: 1, body: 1 },
FunctionExpression: { parameters: 1, body: 1 },
CallExpression: { arguments: 1 },
ArrayExpression: 1,
ObjectExpression: 1,
ImportDeclaration: 1,
flatTernaryExpressions: false,
ignoreComments: false,
ignoredNodes: ['TemplateLiteral *'],
offsetTernaryExpressions: true,
}],
'@stylistic/key-spacing': ['error', { beforeColon: false, afterColon: true }],
'@stylistic/keyword-spacing': ['error', { before: true, after: true }],
'@stylistic/lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }],
'@stylistic/multiline-ternary': ['error', 'always-multiline'],
'@stylistic/new-parens': 'error',
'@stylistic/no-extra-parens': ['error', 'fu
gitextract_w3ol9anw/ ├── .babelrc ├── .editorconfig ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yaml │ │ ├── config.yml │ │ └── feature_request.yaml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── auto-merge.yml │ ├── dependabot.yml │ ├── issue-labeler.yml │ ├── pr-labeler.yml │ └── workflows/ │ ├── autoMerge.yml │ ├── build.yml │ ├── calibreapp-image-actions.yml │ ├── codeql.yml │ ├── conflicts.yml │ ├── flatpak.yml │ ├── label-issue.yml │ ├── label-pr.yml │ ├── linter.yml │ ├── no-response.yml │ ├── release.yml │ ├── remove-outdated-labels.yml │ ├── stale.yml │ └── updateSite.yml ├── .gitignore ├── .stylelintignore ├── .vscode/ │ ├── extensions.json │ ├── launch.json │ └── settings.json ├── .whitesource ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── _icons/ │ └── iconMac.icns ├── _scripts/ │ ├── ProcessLocalesPlugin.js │ ├── _undefinedDefaultExport.mjs │ ├── build.mjs │ ├── clean.mjs │ ├── dev-runner.js │ ├── ebuilder.config.mjs │ ├── eslint-rules/ │ │ ├── plugin.mjs │ │ └── prefer-use-i18n-polyfill-rule.mjs │ ├── findMissingTemplates.mjs │ ├── getInstances.js │ ├── getRegions.mjs │ ├── getShakaLocales.js │ ├── injectAllowedPaths.mjs │ ├── mime-db-shrinking-loader.js │ ├── patch-shaka-player-loader.js │ ├── sigFrameConfig.js │ ├── webpack.botGuardScript.config.js │ ├── webpack.main.config.js │ ├── webpack.preload.config.js │ ├── webpack.renderer.config.js │ └── webpack.web.config.js ├── eslint.config.mjs ├── jsconfig.json ├── lefthook-local.yml.example ├── lefthook.yml ├── package.json ├── src/ │ ├── botGuardScript.js │ ├── constants.js │ ├── data/ │ │ └── .gitkeep │ ├── datastores/ │ │ ├── handlers/ │ │ │ ├── base.js │ │ │ ├── electron.js │ │ │ ├── index.js │ │ │ └── web.js │ │ └── index.js │ ├── index.ejs │ ├── main/ │ │ ├── ImageCache.js │ │ ├── externalPlayer.js │ │ ├── index.js │ │ ├── poTokenGenerator.js │ │ └── utils.js │ ├── preload/ │ │ ├── interface.js │ │ ├── main.js │ │ └── preload-interface.d.ts │ └── renderer/ │ ├── App.css │ ├── App.vue │ ├── components/ │ │ ├── ChannelAbout/ │ │ │ ├── ChannelAbout.css │ │ │ └── ChannelAbout.vue │ │ ├── ChannelDetails/ │ │ │ ├── ChannelDetails.css │ │ │ └── ChannelDetails.vue │ │ ├── ChannelHome/ │ │ │ ├── ChannelHome.css │ │ │ └── ChannelHome.vue │ │ ├── CommentSection/ │ │ │ ├── CommentSection.css │ │ │ └── CommentSection.vue │ │ ├── DataSettings/ │ │ │ ├── DataSettings.css │ │ │ └── DataSettings.vue │ │ ├── DistractionSettings/ │ │ │ ├── DistractionSettings.css │ │ │ └── DistractionSettings.vue │ │ ├── ExperimentalSettings/ │ │ │ ├── ExperimentalSettings.css │ │ │ └── ExperimentalSettings.vue │ │ ├── ExternalPlayerSettings.vue │ │ ├── FtAgeRestricted/ │ │ │ ├── FtAgeRestricted.css │ │ │ └── FtAgeRestricted.vue │ │ ├── FtAutoGrid/ │ │ │ ├── FtAutoGrid.css │ │ │ └── FtAutoGrid.vue │ │ ├── FtAutoLoadNextPageWrapper.vue │ │ ├── FtButton/ │ │ │ ├── FtButton.css │ │ │ └── FtButton.vue │ │ ├── FtChannelBubble/ │ │ │ ├── FtChannelBubble.css │ │ │ └── FtChannelBubble.vue │ │ ├── FtCheckboxList/ │ │ │ ├── FtCheckboxList.css │ │ │ └── FtCheckboxList.vue │ │ ├── FtCommunityPoll/ │ │ │ ├── FtCommunityPoll.css │ │ │ └── FtCommunityPoll.vue │ │ ├── FtCommunityPost/ │ │ │ ├── FtCommunityPost.scss │ │ │ └── FtCommunityPost.vue │ │ ├── FtCreatePlaylistPrompt/ │ │ │ ├── FtCreatePlaylistPrompt.css │ │ │ └── FtCreatePlaylistPrompt.vue │ │ ├── FtElementList/ │ │ │ ├── FtElementList.css │ │ │ └── FtElementList.vue │ │ ├── FtIconButton/ │ │ │ ├── FtIconButton.scss │ │ │ └── FtIconButton.vue │ │ ├── FtInput/ │ │ │ ├── FtInput.css │ │ │ └── FtInput.vue │ │ ├── FtInputTags/ │ │ │ ├── FtInputTags.css │ │ │ └── FtInputTags.vue │ │ ├── FtKeyboardShortcutPrompt/ │ │ │ ├── FtKeyboardShortcutPrompt.css │ │ │ └── FtKeyboardShortcutPrompt.vue │ │ ├── FtListChannel/ │ │ │ ├── FtListChannel.scss │ │ │ └── FtListChannel.vue │ │ ├── FtListHashtag/ │ │ │ ├── FtListHashtag.scss │ │ │ └── FtListHashtag.vue │ │ ├── FtListLazyWrapper/ │ │ │ ├── FtListLazyWrapper.css │ │ │ └── FtListLazyWrapper.vue │ │ ├── FtListPlaylist/ │ │ │ ├── FtListPlaylist.scss │ │ │ └── FtListPlaylist.vue │ │ ├── FtListVideoLazy.vue │ │ ├── FtListVideoNumbered/ │ │ │ ├── FtListVideoNumbered.css │ │ │ └── FtListVideoNumbered.vue │ │ ├── FtLoader/ │ │ │ ├── FtLoader.css │ │ │ └── FtLoader.vue │ │ ├── FtLogoFull/ │ │ │ ├── FtLogoFull.css │ │ │ └── FtLogoFull.vue │ │ ├── FtNotificationBanner/ │ │ │ ├── FtNotificationBanner.css │ │ │ └── FtNotificationBanner.vue │ │ ├── FtPlaylistAddVideoPrompt/ │ │ │ ├── FtPlaylistAddVideoPrompt.css │ │ │ └── FtPlaylistAddVideoPrompt.vue │ │ ├── FtPlaylistSelector/ │ │ │ ├── FtPlaylistSelector.scss │ │ │ └── FtPlaylistSelector.vue │ │ ├── FtProfileBubble/ │ │ │ ├── FtProfileBubble.css │ │ │ └── FtProfileBubble.vue │ │ ├── FtProfileChannelList/ │ │ │ ├── FtProfileChannelList.css │ │ │ └── FtProfileChannelList.vue │ │ ├── FtProfileEdit/ │ │ │ ├── FtProfileEdit.css │ │ │ └── FtProfileEdit.vue │ │ ├── FtProfileFilterChannelsList/ │ │ │ ├── FtProfileFilterChannelsList.css │ │ │ └── FtProfileFilterChannelsList.vue │ │ ├── FtProfileSelector/ │ │ │ ├── FtProfileSelector.css │ │ │ └── FtProfileSelector.vue │ │ ├── FtProgressBar/ │ │ │ ├── FtProgressBar.css │ │ │ └── FtProgressBar.vue │ │ ├── FtPrompt/ │ │ │ ├── FtPrompt.css │ │ │ └── FtPrompt.vue │ │ ├── FtRadioButton/ │ │ │ ├── FtRadioButton.css │ │ │ └── FtRadioButton.vue │ │ ├── FtRefreshWidget/ │ │ │ ├── FtRefreshWidget.scss │ │ │ └── FtRefreshWidget.vue │ │ ├── FtSearchFilters/ │ │ │ ├── FtSearchFilters.css │ │ │ └── FtSearchFilters.vue │ │ ├── FtSelect/ │ │ │ ├── FtSelect.css │ │ │ └── FtSelect.vue │ │ ├── FtSettingsMenu/ │ │ │ ├── FtSettingsMenu.css │ │ │ └── FtSettingsMenu.vue │ │ ├── FtSettingsSection/ │ │ │ ├── FtSettingsSection.scss │ │ │ └── FtSettingsSection.vue │ │ ├── FtShareButton/ │ │ │ ├── FtShareButton.css │ │ │ └── FtShareButton.vue │ │ ├── FtSlider/ │ │ │ ├── FtSlider.css │ │ │ └── FtSlider.vue │ │ ├── FtSponsorBlockCategory/ │ │ │ ├── FtSponsorBlockCategory.css │ │ │ └── FtSponsorBlockCategory.vue │ │ ├── FtSubscribeButton/ │ │ │ ├── FtSubscribeButton.css │ │ │ └── FtSubscribeButton.vue │ │ ├── FtTimestampCatcher.vue │ │ ├── FtToast/ │ │ │ ├── FtToast.css │ │ │ └── FtToast.vue │ │ ├── FtToggleSwitch/ │ │ │ ├── FtToggleSwitch.scss │ │ │ └── FtToggleSwitch.vue │ │ ├── FtTooltip/ │ │ │ ├── FtTooltip.css │ │ │ └── FtTooltip.vue │ │ ├── GeneralSettings/ │ │ │ ├── GeneralSettings.css │ │ │ └── GeneralSettings.vue │ │ ├── ParentalControlSettings.vue │ │ ├── PasswordDialog/ │ │ │ ├── PasswordDialog.css │ │ │ └── PasswordDialog.vue │ │ ├── PasswordSettings/ │ │ │ ├── PasswordSettings.css │ │ │ └── PasswordSettings.vue │ │ ├── PlayerSettings/ │ │ │ ├── PlayerSettings.css │ │ │ └── PlayerSettings.vue │ │ ├── PlaylistInfo/ │ │ │ ├── PlaylistInfo.scss │ │ │ └── PlaylistInfo.vue │ │ ├── PrivacySettings.vue │ │ ├── ProxySettings/ │ │ │ ├── ProxySettings.css │ │ │ └── ProxySettings.vue │ │ ├── SideNav/ │ │ │ ├── SideNav.css │ │ │ └── SideNav.vue │ │ ├── SideNavMoreOptions/ │ │ │ ├── SideNavMoreOptions.css │ │ │ └── SideNavMoreOptions.vue │ │ ├── SponsorBlockSettings.vue │ │ ├── SubscriptionSettings/ │ │ │ ├── SubscriptionSettings.css │ │ │ └── SubscriptionSettings.vue │ │ ├── SubscriptionsLive.vue │ │ ├── SubscriptionsPosts.vue │ │ ├── SubscriptionsShorts.vue │ │ ├── SubscriptionsTabUi/ │ │ │ ├── SubscriptionsTabUi.css │ │ │ └── SubscriptionsTabUi.vue │ │ ├── SubscriptionsVideos.vue │ │ ├── ThemeSettings.vue │ │ ├── TopNav/ │ │ │ ├── TopNav.scss │ │ │ └── TopNav.vue │ │ ├── WatchVideoChapters/ │ │ │ ├── WatchVideoChapters.css │ │ │ └── WatchVideoChapters.vue │ │ ├── WatchVideoDescription/ │ │ │ ├── WatchVideoDescription.css │ │ │ └── WatchVideoDescription.vue │ │ ├── WatchVideoInfo/ │ │ │ ├── WatchVideoInfo.css │ │ │ └── WatchVideoInfo.vue │ │ ├── WatchVideoLiveChat/ │ │ │ ├── WatchVideoLiveChat.css │ │ │ └── WatchVideoLiveChat.vue │ │ ├── WatchVideoRecommendations/ │ │ │ ├── WatchVideoRecommendations.css │ │ │ └── WatchVideoRecommendations.vue │ │ ├── ft-card/ │ │ │ ├── ft-card.css │ │ │ ├── ft-card.js │ │ │ └── ft-card.vue │ │ ├── ft-flex-box/ │ │ │ ├── ft-flex-box.css │ │ │ ├── ft-flex-box.js │ │ │ └── ft-flex-box.vue │ │ ├── ft-list-video/ │ │ │ ├── ft-list-video.js │ │ │ ├── ft-list-video.scss │ │ │ └── ft-list-video.vue │ │ ├── ft-shaka-video-player/ │ │ │ ├── ft-shaka-video-player.css │ │ │ ├── ft-shaka-video-player.js │ │ │ ├── ft-shaka-video-player.vue │ │ │ └── player-components/ │ │ │ ├── AudioTrackSelection.js │ │ │ ├── AutoplayToggle.js │ │ │ ├── FullWindowButton.js │ │ │ ├── LegacyQualitySelection.js │ │ │ ├── ScreenshotButton.js │ │ │ ├── SkipButton.js │ │ │ ├── StatsButton.js │ │ │ └── TheatreModeButton.js │ │ └── watch-video-playlist/ │ │ ├── watch-video-playlist.css │ │ ├── watch-video-playlist.js │ │ └── watch-video-playlist.vue │ ├── composables/ │ │ ├── colors.js │ │ └── use-i18n-polyfill.js │ ├── directives/ │ │ └── vSaferHtml.js │ ├── fontawesome-minimal.js │ ├── helpers/ │ │ ├── api/ │ │ │ ├── PlayerCache.js │ │ │ ├── invidious.js │ │ │ └── local.js │ │ ├── channels.js │ │ ├── colors.js │ │ ├── player/ │ │ │ ├── EbmlParser.js │ │ │ ├── Mp4SegmentIndexParser.js │ │ │ ├── SabrManifestParser.js │ │ │ ├── SabrSchemePlugin.js │ │ │ ├── WebmSegmentIndexParser.js │ │ │ └── utils.js │ │ ├── playlists.js │ │ ├── sponsorblock.js │ │ ├── strings.js │ │ ├── subscriptions.js │ │ └── utils.js │ ├── i18n/ │ │ └── index.js │ ├── main.js │ ├── router/ │ │ └── index.js │ ├── scss-partials/ │ │ ├── _ft-list-item.scss │ │ └── _utils.scss │ ├── sigFrameScript.js │ ├── store/ │ │ ├── index.js │ │ └── modules/ │ │ ├── history.js │ │ ├── invidious.js │ │ ├── player.js │ │ ├── playlists.js │ │ ├── profiles.js │ │ ├── search-history.js │ │ ├── settings.js │ │ ├── subscription-cache.js │ │ └── utils.js │ ├── themes.css │ └── views/ │ ├── About/ │ │ ├── About.css │ │ └── About.vue │ ├── Channel/ │ │ ├── Channel.css │ │ └── Channel.vue │ ├── Hashtag/ │ │ ├── Hashtag.css │ │ └── Hashtag.vue │ ├── History/ │ │ ├── History.css │ │ └── History.vue │ ├── Playlist/ │ │ ├── Playlist.scss │ │ └── Playlist.vue │ ├── Popular/ │ │ ├── Popular.css │ │ └── Popular.vue │ ├── Post.vue │ ├── ProfileSettings/ │ │ ├── ProfileSettings.css │ │ └── ProfileSettings.vue │ ├── SearchPage/ │ │ ├── SearchPage.css │ │ └── SearchPage.vue │ ├── Settings/ │ │ ├── Settings.css │ │ └── Settings.vue │ ├── SubscribedChannels/ │ │ ├── SubscribedChannels.css │ │ └── SubscribedChannels.vue │ ├── Subscriptions/ │ │ ├── Subscriptions.css │ │ └── Subscriptions.vue │ ├── Trending/ │ │ ├── Trending.css │ │ └── Trending.vue │ ├── UserPlaylists/ │ │ ├── UserPlaylists.css │ │ └── UserPlaylists.vue │ └── Watch/ │ ├── Watch.js │ ├── Watch.scss │ └── Watch.vue ├── static/ │ ├── .gitkeep │ ├── external-player-map.json │ ├── geolocations/ │ │ ├── ar.json │ │ ├── be.json │ │ ├── bg.json │ │ ├── ca.json │ │ ├── cs.json │ │ ├── da.json │ │ ├── de-DE.json │ │ ├── el.json │ │ ├── en-GB.json │ │ ├── en-US.json │ │ ├── es-AR.json │ │ ├── es-MX.json │ │ ├── es.json │ │ ├── et.json │ │ ├── eu.json │ │ ├── fa.json │ │ ├── fi.json │ │ ├── fr-FR.json │ │ ├── gl.json │ │ ├── he.json │ │ ├── hr.json │ │ ├── hu.json │ │ ├── id.json │ │ ├── is.json │ │ ├── it.json │ │ ├── ja.json │ │ ├── ko.json │ │ ├── lt.json │ │ ├── nb-NO.json │ │ ├── nl.json │ │ ├── nn.json │ │ ├── pl.json │ │ ├── pt-BR.json │ │ ├── pt-PT.json │ │ ├── pt.json │ │ ├── ro.json │ │ ├── ru.json │ │ ├── sk.json │ │ ├── sl.json │ │ ├── sr.json │ │ ├── sv.json │ │ ├── ta.json │ │ ├── tr.json │ │ ├── uk.json │ │ ├── vi.json │ │ ├── zh-CN.json │ │ └── zh-TW.json │ ├── invidious-instances.json │ ├── locales/ │ │ ├── activeLocales.json │ │ ├── af.yaml │ │ ├── ar.yaml │ │ ├── as.yaml │ │ ├── awa.yaml │ │ ├── az.yaml │ │ ├── be.yaml │ │ ├── bg.yaml │ │ ├── bn.yaml │ │ ├── br.yaml │ │ ├── bs.yaml │ │ ├── ca.yaml │ │ ├── ckb.yaml │ │ ├── cs.yaml │ │ ├── cy.yaml │ │ ├── da.yaml │ │ ├── de-DE.yaml │ │ ├── el.yaml │ │ ├── en-GB.yaml │ │ ├── en-US.yaml │ │ ├── eo.yaml │ │ ├── es-AR.yaml │ │ ├── es-MX.yaml │ │ ├── es.yaml │ │ ├── et.yaml │ │ ├── eu.yaml │ │ ├── fa.yaml │ │ ├── fi.yaml │ │ ├── fil.yaml │ │ ├── fr-FR.yaml │ │ ├── gl.yaml │ │ ├── gsw.yaml │ │ ├── he.yaml │ │ ├── hi.yaml │ │ ├── hr.yaml │ │ ├── hu.yaml │ │ ├── id.yaml │ │ ├── is.yaml │ │ ├── it.yaml │ │ ├── ja.yaml │ │ ├── ka.yaml │ │ ├── km.yaml │ │ ├── ko.yaml │ │ ├── ku.yaml │ │ ├── la.yaml │ │ ├── lt.yaml │ │ ├── lv.yaml │ │ ├── ms.yaml │ │ ├── my.yaml │ │ ├── nb-NO.yaml │ │ ├── ne.yaml │ │ ├── nl.yaml │ │ ├── nn.yaml │ │ ├── or.yaml │ │ ├── pl.yaml │ │ ├── pt-BR.yaml │ │ ├── pt-PT.yaml │ │ ├── pt.yaml │ │ ├── ro.yaml │ │ ├── ru.yaml │ │ ├── sat.yaml │ │ ├── si.yaml │ │ ├── sk.yaml │ │ ├── sl.yaml │ │ ├── sm.yaml │ │ ├── sq.yaml │ │ ├── sr.yaml │ │ ├── sv.yaml │ │ ├── ta.yaml │ │ ├── ti.yaml │ │ ├── tok.yaml │ │ ├── tr.yaml │ │ ├── uk.yaml │ │ ├── ur.yaml │ │ ├── uz.yaml │ │ ├── vi.yaml │ │ ├── vls.yaml │ │ ├── xh.yaml │ │ ├── zh-CN.yaml │ │ └── zh-TW.yaml │ ├── manifest.json │ └── pwabuilder-sw.js └── stylelint.config.mjs
SYMBOL INDEX (719 symbols across 60 files)
FILE: _scripts/ProcessLocalesPlugin.js
constant PLUGIN_NAME (line 10) | const PLUGIN_NAME = 'ProcessLocalesPlugin'
class ProcessLocalesPlugin (line 12) | class ProcessLocalesPlugin {
method constructor (line 13) | constructor(options = {}) {
method apply (line 47) | apply(compiler) {
method processLocale (line 97) | async processLocale(locale, data, updatedLocales, compiler, compilatio...
method loadLocales (line 143) | loadLocales() {
method compressLocale (line 159) | async compressLocale(data) {
method removeEmptyValues (line 177) | removeEmptyValues(data) {
FILE: _scripts/clean.mjs
constant BUILD_PATH (line 4) | const BUILD_PATH = join(import.meta.dirname, '..', 'build')
constant DIST_PATH (line 5) | const DIST_PATH = join(import.meta.dirname, '..', 'dist')
FILE: _scripts/dev-runner.js
constant SHAKA_LOCALES_TO_BE_BUNDLED (line 24) | let SHAKA_LOCALES_TO_BE_BUNDLED
function killElectron (line 49) | async function killElectron(pid) {
function restartElectron (line 63) | async function restartElectron() {
function setupNotifyLocaleUpdate (line 93) | function setupNotifyLocaleUpdate(compiler, devServer) {
function startBotGuardScript (line 105) | function startBotGuardScript() {
function startMain (line 113) | function startMain() {
function startPreload (line 137) | function startPreload() {
function startRenderer (line 167) | function startRenderer(callback) {
function startWeb (line 215) | function startWeb () {
FILE: _scripts/eslint-rules/prefer-use-i18n-polyfill-rule.mjs
function getRelativePolyfillPath (line 5) | function getRelativePolyfillPath(filePath) {
method create (line 21) | create(context) {
FILE: _scripts/findMissingTemplates.mjs
function addErrors (line 40) | function addErrors(originalData, newData, originalKeys, newKeys, file) {
function isMissingInterpolation (line 62) | function isMissingInterpolation(defaultValue, otherValue, filename) {
FILE: _scripts/getRegions.mjs
constant STATIC_DIRECTORY (line 17) | const STATIC_DIRECTORY = `${import.meta.dirname}/../static`
function scrapeLanguage (line 125) | async function scrapeLanguage(youTubeLanguageCode) {
function processGeolocations (line 140) | function processGeolocations(freeTubeLanguage, youTubeLanguage, response) {
FILE: _scripts/getShakaLocales.js
function getPreloadedLocales (line 4) | function getPreloadedLocales() {
function getAllLocales (line 16) | function getAllLocales() {
function getMappings (line 31) | function getMappings(shakaLocales, freeTubeLocales) {
function getShakaLocales (line 85) | function getShakaLocales() {
FILE: src/constants.js
constant MAIN_PROFILE_ID (line 242) | const MAIN_PROFILE_ID = 'allChannels'
constant MOBILE_WIDTH_THRESHOLD (line 245) | const MOBILE_WIDTH_THRESHOLD = 680
constant PLAYLIST_HEIGHT_FORCE_LIST_THRESHOLD (line 248) | const PLAYLIST_HEIGHT_FORCE_LIST_THRESHOLD = 500
constant SEARCH_CHAR_LIMIT (line 251) | const SEARCH_CHAR_LIMIT = 100
constant SEARCH_RESULTS_DISPLAY_LIMIT (line 254) | const SEARCH_RESULTS_DISPLAY_LIMIT = 14
constant MIXED_SEARCH_HISTORY_ENTRIES_DISPLAY_LIMIT (line 257) | const MIXED_SEARCH_HISTORY_ENTRIES_DISPLAY_LIMIT = 4
constant ABOUT_BITCOIN_ADDRESS (line 260) | const ABOUT_BITCOIN_ADDRESS = '1Lih7Ho5gnxb1CwPD4o59ss78pwo2T91eS'
FILE: src/datastores/handlers/base.js
class Settings (line 3) | class Settings {
method find (line 4) | static async find() {
method upsert (line 54) | static upsert(_id, value) {
method _findAppReadyRelatedSettings (line 60) | static _findAppReadyRelatedSettings() {
method _findOne (line 77) | static _findOne(_id) {
method _findSidenavSettings (line 81) | static _findSidenavSettings() {
method _updateBounds (line 89) | static _updateBounds(value) {
class History (line 95) | class History {
method find (line 96) | static find() {
method upsert (line 100) | static upsert(record) {
method overwrite (line 104) | static async overwrite(records) {
method updateWatchProgress (line 110) | static updateWatchProgress(videoId, watchProgress) {
method updateLastViewedPlaylist (line 114) | static updateLastViewedPlaylist(videoId, lastViewedPlaylistId, lastVie...
method delete (line 118) | static delete(videoId) {
method deleteAll (line 122) | static deleteAll() {
class Profiles (line 127) | class Profiles {
method create (line 128) | static create(profile) {
method find (line 132) | static find() {
method upsert (line 136) | static upsert(profile) {
method addChannelToProfiles (line 140) | static addChannelToProfiles(channel, profileIds) {
method removeChannelFromProfiles (line 155) | static removeChannelFromProfiles(channelId, profileIds) {
method delete (line 170) | static delete(id) {
class Playlists (line 175) | class Playlists {
method create (line 176) | static create(playlists) {
method find (line 180) | static find() {
method upsert (line 184) | static upsert(playlist) {
method upsertVideoByPlaylistId (line 188) | static upsertVideoByPlaylistId(_id, lastUpdatedAt, videoData) {
method upsertVideosByPlaylistId (line 199) | static upsertVideosByPlaylistId(_id, lastUpdatedAt, videos) {
method delete (line 210) | static delete(_id) {
method deleteVideoIdByPlaylistId (line 214) | static deleteVideoIdByPlaylistId(_id, lastUpdatedAt, videoId, playlist...
method deleteVideoIdsByPlaylistId (line 238) | static deleteVideoIdsByPlaylistId(_id, lastUpdatedAt, playlistItemIds) {
method deleteAllVideosByPlaylistId (line 249) | static deleteAllVideosByPlaylistId(_id) {
method deleteMultiple (line 257) | static deleteMultiple(ids) {
method deleteAll (line 261) | static deleteAll() {
class SearchHistory (line 266) | class SearchHistory {
method find (line 267) | static find() {
method upsert (line 271) | static upsert(searchHistoryEntry) {
method overwrite (line 275) | static async overwrite(records) {
method delete (line 281) | static delete(_id) {
method deleteAll (line 285) | static deleteAll() {
class SubscriptionCache (line 290) | class SubscriptionCache {
method find (line 291) | static find() {
method updateVideosByChannelId (line 295) | static updateVideosByChannelId(channelId, entries, timestamp) {
method updateLiveStreamsByChannelId (line 303) | static updateLiveStreamsByChannelId(channelId, entries, timestamp) {
method updateShortsByChannelId (line 311) | static updateShortsByChannelId(channelId, entries, timestamp) {
method updateShortsWithChannelPageShortsByChannelId (line 319) | static async updateShortsWithChannelPageShortsByChannelId(channelId, e...
method updateCommunityPostsByChannelId (line 355) | static updateCommunityPostsByChannelId(channelId, entries, timestamp) {
method deleteMultipleChannels (line 363) | static deleteMultipleChannels(channelIds) {
method deleteAll (line 367) | static deleteAll() {
function loadDatastores (line 372) | function loadDatastores() {
function compactAllDatastores (line 383) | function compactAllDatastores() {
FILE: src/datastores/handlers/electron.js
class Settings (line 3) | class Settings {
method find (line 4) | static find() {
method upsert (line 8) | static upsert(_id, value) {
class History (line 13) | class History {
method find (line 14) | static find() {
method upsert (line 18) | static upsert(record) {
method overwrite (line 22) | static overwrite(records) {
method updateWatchProgress (line 26) | static updateWatchProgress(videoId, watchProgress) {
method updateLastViewedPlaylist (line 33) | static updateLastViewedPlaylist(videoId, lastViewedPlaylistId, lastVie...
method delete (line 40) | static delete(videoId) {
method deleteAll (line 44) | static deleteAll() {
class Profiles (line 49) | class Profiles {
method create (line 50) | static create(profile) {
method find (line 54) | static find() {
method upsert (line 58) | static upsert(profile) {
method addChannelToProfiles (line 62) | static addChannelToProfiles(channel, profileIds) {
method removeChannelFromProfiles (line 66) | static removeChannelFromProfiles(channelId, profileIds) {
method delete (line 70) | static delete(id) {
class Playlists (line 75) | class Playlists {
method create (line 76) | static create(playlists) {
method find (line 80) | static find() {
method upsert (line 84) | static upsert(playlist) {
method upsertVideoByPlaylistId (line 88) | static upsertVideoByPlaylistId(_id, lastUpdatedAt, videoData) {
method upsertVideosByPlaylistId (line 95) | static upsertVideosByPlaylistId(_id, lastUpdatedAt, videos) {
method delete (line 102) | static delete(_id) {
method deleteVideoIdByPlaylistId (line 106) | static deleteVideoIdByPlaylistId(_id, lastUpdatedAt, videoId, playlist...
method deleteVideoIdsByPlaylistId (line 113) | static deleteVideoIdsByPlaylistId(_id, lastUpdatedAt, playlistItemIds) {
method deleteAllVideosByPlaylistId (line 120) | static deleteAllVideosByPlaylistId(_id) {
method deleteMultiple (line 124) | static deleteMultiple(ids) {
method deleteAll (line 128) | static deleteAll() {
class SearchHistory (line 133) | class SearchHistory {
method find (line 134) | static find() {
method upsert (line 138) | static upsert(searchHistoryEntry) {
method overwrite (line 142) | static overwrite(records) {
method delete (line 146) | static delete(_id) {
method deleteAll (line 150) | static deleteAll() {
class SubscriptionCache (line 155) | class SubscriptionCache {
method find (line 156) | static find() {
method updateVideosByChannelId (line 160) | static updateVideosByChannelId(channelId, entries, timestamp) {
method updateLiveStreamsByChannelId (line 167) | static updateLiveStreamsByChannelId(channelId, entries, timestamp) {
method updateShortsByChannelId (line 174) | static updateShortsByChannelId(channelId, entries, timestamp) {
method updateShortsWithChannelPageShortsByChannelId (line 181) | static updateShortsWithChannelPageShortsByChannelId(channelId, entries) {
method updateCommunityPostsByChannelId (line 188) | static updateCommunityPostsByChannelId(channelId, entries, timestamp) {
method deleteMultipleChannels (line 195) | static deleteMultipleChannels(channelIds) {
method deleteAll (line 199) | static deleteAll() {
FILE: src/datastores/handlers/web.js
class Settings (line 13) | class Settings {
method find (line 14) | static find() {
method upsert (line 18) | static upsert(_id, value) {
FILE: src/datastores/index.js
function createDatastore (line 28) | function createDatastore(name) {
FILE: src/main/ImageCache.js
constant CLEANUP_INTERVAL (line 2) | const CLEANUP_INTERVAL = 300_000
constant FALLBACK_MAX_AGE (line 5) | const FALLBACK_MAX_AGE = 7200
class ImageCache (line 7) | class ImageCache {
method constructor (line 8) | constructor() {
method add (line 14) | add(url, mimeType, data, expiry) {
method has (line 18) | has(url) {
method get (line 22) | get(url) {
method _cleanup (line 36) | _cleanup() {
function extractExpiryTimestamp (line 53) | function extractExpiryTimestamp(headers) {
FILE: src/main/externalPlayer.js
constant ID_REGEX (line 34) | const ID_REGEX = /^[\w-]+$/
function handleOpenInExternalPlayer (line 43) | async function handleOpenInExternalPlayer(event, payload) {
function loadExternalPlayerData (line 191) | async function loadExternalPlayerData() {
FILE: src/main/index.js
function printHelp (line 51) | function printHelp() {
function runApp (line 61) | function runApp() {
FILE: src/main/poTokenGenerator.js
function enqueueAsyncFunction (line 22) | function enqueueAsyncFunction(func, ...args) {
function generatePoToken (line 50) | function generatePoToken(videoId, context, proxyUrl) {
function sharedInit (line 82) | async function sharedInit() {
function internalGeneratePotoken (line 149) | async function internalGeneratePotoken(videoId, context, proxyUrl) {
function cleanupSession (line 213) | async function cleanupSession() {
FILE: src/main/utils.js
function isFreeTubeUrl (line 4) | function isFreeTubeUrl(url) {
FILE: src/preload/preload-interface.d.ts
type Window (line 4) | interface Window {
FILE: src/renderer/components/ft-list-video/ft-list-video.js
method playlistSharable (line 195) | playlistSharable() {
method playlistIdTypePairFinal (line 448) | playlistIdTypePairFinal() {
method quickBookmarkPlaylist (line 478) | quickBookmarkPlaylist() {
method isQuickBookmarkEnabled (line 481) | isQuickBookmarkEnabled() {
method watchVideoRouterLink (line 512) | watchVideoRouterLink() {
method watchPageLinkQuery (line 524) | watchPageLinkQuery() {
method showAddToPlaylistPrompt (line 555) | showAddToPlaylistPrompt(value) {
method toggleDeArrow (line 624) | toggleDeArrow() {
method toggleQuickBookmarked (line 848) | toggleQuickBookmarked() {
method addToQuickBookmarkPlaylist (line 860) | addToQuickBookmarkPlaylist() {
method removeFromQuickBookmarkPlaylist (line 880) | removeFromQuickBookmarkPlaylist() {
FILE: src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js
constant HTTP_IN_HEX (line 37) | const HTTP_IN_HEX = 0x68747470
constant USE_OVERFLOW_MENU_WIDTH_THRESHOLD (line 39) | const USE_OVERFLOW_MENU_WIDTH_THRESHOLD = 634
constant LOCALE_MAPPINGS (line 64) | const LOCALE_MAPPINGS = new Map(process.env.SHAKA_LOCALE_MAPPINGS)
function setupSponsorBlock (line 483) | async function setupSponsorBlock() {
function skipSponsorBlockSegments (line 510) | function skipSponsorBlockSegments(currentTime) {
function getPlayerConfig (line 595) | function getPlayerConfig(format, useAutoQuality = false) {
function manifestPreprocessorTXml (line 636) | function manifestPreprocessorTXml(mpdNode) {
function sortAdapationSetsByCodec (line 693) | function sortAdapationSetsByCodec(periods) {
function sortAudioAdaptationSetsByBitrate (line 753) | function sortAudioAdaptationSetsByBitrate(periods) {
function configureUI (line 894) | function configureUI(firstTime = false) {
function handleControlsContainerWheel (line 956) | function handleControlsContainerWheel(event) {
function handleControlsContainerClick (line 985) | function handleControlsContainerClick(event) {
function addUICustomizations (line 996) | function addUICustomizations() {
function resized (line 1078) | function resized(entries) {
function setLocale (line 1092) | async function setLocale(locale) {
function startPowerSaveBlocker (line 1144) | function startPowerSaveBlocker() {
function stopPowerSaveBlocker (line 1150) | function stopPowerSaveBlocker() {
function handlePlay (line 1160) | function handlePlay() {
function handlePause (line 1168) | function handlePause() {
function handleEnded (line 1176) | function handleEnded() {
function handleCanPlay (line 1186) | function handleCanPlay() {
function updateVolume (line 1194) | function updateVolume() {
function handleTimeupdate (line 1217) | function handleTimeupdate() {
function handleEnterPictureInPicture (line 1255) | function handleEnterPictureInPicture(event) {
function handleLeavePictureInPicture (line 1261) | function handleLeavePictureInPicture() {
function handlePictureInPictureResize (line 1269) | function handlePictureInPictureResize() {
function requestFilter (line 1318) | function requestFilter(type, request, _context) {
function responseFilter (line 1339) | async function responseFilter(type, response, context) {
function hlsProxiedUrlReplacer (line 1409) | function hlsProxiedUrlReplacer(match) {
function setDashQuality (line 1442) | function setDashQuality(quality, audioBandwidth, label) {
function setLegacyQuality (line 1490) | async function setLegacyQuality(playbackPosition = null, previousQuality...
function gatherInitialStatsValues (line 1534) | function gatherInitialStatsValues() {
function updateQualityStats (line 1578) | function updateQualityStats({ newTrack }) {
function updateLegacyQualityStats (line 1614) | function updateLegacyQualityStats(newFormat) {
function updateStats (line 1637) | function updateStats() {
function takeScreenshot (line 1689) | async function takeScreenshot() {
function registerAudioTrackSelection (line 1775) | function registerAudioTrackSelection() {
function registerAutoplayToggle (line 1787) | function registerAutoplayToggle() {
function registerTheatreModeButton (line 1805) | function registerTheatreModeButton() {
function registerFullWindowButton (line 1823) | function registerFullWindowButton() {
function registerLegacyQualitySelection (line 1857) | function registerLegacyQualitySelection() {
function registerStatsButton (line 1892) | function registerStatsButton() {
function registerScreenshotButton (line 1913) | function registerScreenshotButton() {
function registerSkipButtons (line 1931) | function registerSkipButtons() {
function cleanUpCustomPlayerControls (line 1966) | function cleanUpCustomPlayerControls() {
function changeVolume (line 2001) | function changeVolume(step) {
function changePlayBackRate (line 2031) | function changePlayBackRate(step) {
function canSeek (line 2048) | function canSeek() {
function seekBySeconds (line 2068) | function seekBySeconds(seconds, canSeekResult = false, showPopUp = false) {
function mouseScrollPlaybackRate (line 2112) | function mouseScrollPlaybackRate(event) {
function mouseScrollPlaybackRateHandler (line 2123) | function mouseScrollPlaybackRateHandler(event) {
function mouseScrollSkip (line 2137) | function mouseScrollSkip(event) {
function mouseScrollSkipHandler (line 2148) | function mouseScrollSkipHandler(event) {
function mouseScrollVolume (line 2164) | function mouseScrollVolume(event) {
function mouseScrollVolumeHandler (line 2184) | function mouseScrollVolumeHandler(event) {
function canChapterJump (line 2210) | function canChapterJump(event, direction) {
function frameByFrame (line 2221) | function frameByFrame(step) {
function keyboardShortcutHandler (line 2244) | function keyboardShortcutHandler(event) {
function handleError (line 2519) | function handleError(error, context, details) {
function createSponsorBlockMarkers (line 2570) | function createSponsorBlockMarkers(duration) {
function createChapterMarkers (line 2585) | function createChapterMarkers() {
function addMarkers (line 2616) | function addMarkers(markers) {
function onlineHandler (line 2641) | function onlineHandler() {
function offlineHandler (line 2645) | function offlineHandler() {
function fullscreenChangeHandler (line 2649) | function fullscreenChangeHandler() {
function performFirstLoad (line 2800) | async function performFirstLoad() {
function handleLoaded (line 2869) | async function handleLoaded() {
function isPaused (line 3184) | function isPaused() {
function pause (line 3188) | function pause() {
function getCurrentTime (line 3192) | function getCurrentTime() {
function setCurrentTime (line 3199) | function setCurrentTime(time) {
function destroyPlayer (line 3210) | async function destroyPlayer() {
function showOverlayControls (line 3271) | function showOverlayControls() {
function showValueChange (line 3281) | function showValueChange(message, icon = null, invertContentOrder = fals...
FILE: src/renderer/components/ft-shaka-video-player/player-components/AudioTrackSelection.js
class AudioTrackSelection (line 7) | class AudioTrackSelection extends shaka.ui.SettingsMenu {
method constructor (line 13) | constructor(events, parent, controls) {
method updateAudioTracks_ (line 65) | updateAudioTracks_() {
method onAudioTrackSelected_ (line 118) | onAudioTrackSelected_(track) {
method updateLocalisedStrings_ (line 143) | updateLocalisedStrings_() {
FILE: src/renderer/components/ft-shaka-video-player/player-components/AutoplayToggle.js
class AutoplayToggle (line 6) | class AutoplayToggle extends shaka.ui.Element {
method constructor (line 13) | constructor(autoplayEnabled, events, parent, controls) {
method updateLocalisedStrings_ (line 69) | updateLocalisedStrings_() {
FILE: src/renderer/components/ft-shaka-video-player/player-components/FullWindowButton.js
class FullWindowButton (line 7) | class FullWindowButton extends shaka.ui.Element {
method constructor (line 14) | constructor(fullWindowEnabled, events, parent, controls) {
method updateLocalisedStrings_ (line 69) | updateLocalisedStrings_() {
FILE: src/renderer/components/ft-shaka-video-player/player-components/LegacyQualitySelection.js
class LegacyQualitySelection (line 5) | class LegacyQualitySelection extends shaka.ui.SettingsMenu {
method constructor (line 13) | constructor(activeLegacyFormat, legacyFormats, events, parent, control...
method updateResolutionSelection_ (line 80) | updateResolutionSelection_() {
method onFormatSelected_ (line 119) | async onFormatSelected_(format) {
method updateLocalisedStrings_ (line 147) | updateLocalisedStrings_() {
FILE: src/renderer/components/ft-shaka-video-player/player-components/ScreenshotButton.js
class ScreenshotButton (line 7) | class ScreenshotButton extends shaka.ui.Element {
method constructor (line 13) | constructor(events, parent, controls) {
method updateLocalisedStrings_ (line 52) | updateLocalisedStrings_() {
FILE: src/renderer/components/ft-shaka-video-player/player-components/SkipButton.js
class SkipButton (line 7) | class SkipButton extends shaka.ui.Element {
method constructor (line 14) | constructor(events, parent, controls, type = 'next') {
method updateLocalisedStrings_ (line 55) | updateLocalisedStrings_() {
FILE: src/renderer/components/ft-shaka-video-player/player-components/StatsButton.js
class StatsButton (line 7) | class StatsButton extends shaka.ui.Element {
method constructor (line 14) | constructor(showStats, events, parent, controls) {
method updateLocalisedStrings_ (line 65) | updateLocalisedStrings_() {
FILE: src/renderer/components/ft-shaka-video-player/player-components/TheatreModeButton.js
class TheatreModeButton (line 7) | class TheatreModeButton extends shaka.ui.Element {
method constructor (line 14) | constructor(theatreModeEnabled, events, parent, controls) {
method updateLocalisedStrings_ (line 69) | updateLocalisedStrings_() {
FILE: src/renderer/components/watch-video-playlist/watch-video-playlist.js
method selectedUserPlaylistVideoCount (line 91) | selectedUserPlaylistVideoCount () {
method selectedUserPlaylistLastUpdatedAt (line 94) | selectedUserPlaylistLastUpdatedAt () {
method playlistPageLinkTo (line 138) | playlistPageLinkTo() {
method previewTransformXPercentage (line 154) | previewTransformXPercentage() {
method selectedUserPlaylistVideoCount (line 194) | selectedUserPlaylistVideoCount () {
method selectedUserPlaylistLastUpdatedAt (line 199) | selectedUserPlaylistLastUpdatedAt () {
method calculateWindowWidth (line 618) | calculateWindowWidth() {
FILE: src/renderer/composables/colors.js
function useColorTranslations (line 4) | function useColorTranslations() {
FILE: src/renderer/composables/use-i18n-polyfill.js
function useI18n (line 17) | function useI18n() {
function t (line 80) | function t(...args) {
FILE: src/renderer/directives/vSaferHtml.js
constant USE_NATIVE_SANITIZER (line 3) | const USE_NATIVE_SANITIZER = process.env.IS_ELECTRON || ('Sanitizer' in ...
FILE: src/renderer/helpers/api/PlayerCache.js
class PlayerCache (line 1) | class PlayerCache {
method get (line 2) | async get(key) {
method set (line 6) | async set(key, value) {
method remove (line 10) | async remove(_key) {
FILE: src/renderer/helpers/api/invidious.js
function getCurrentInstanceUrl (line 17) | function getCurrentInstanceUrl() {
function getProxyUrl (line 24) | function getProxyUrl(uri) {
function invidiousFetch (line 39) | function invidiousFetch(url) {
function invidiousAPICall (line 53) | function invidiousAPICall({ resource, id = '', params = {}, doLogError =...
function resolveUrl (line 79) | async function resolveUrl(url) {
function invidiousGetChannelId (line 95) | async function invidiousGetChannelId(url) {
function invidiousGetChannelInfo (line 131) | async function invidiousGetChannelInfo(channelId) {
function getInvidiousChannelTab (line 152) | async function getInvidiousChannelTab(tab, channelId, continuation, sort...
function getInvidiousChannelVideos (line 176) | async function getInvidiousChannelVideos(channelId, sortBy, continuation) {
function getInvidiousChannelShorts (line 191) | async function getInvidiousChannelShorts(channelId, sortBy, continuation) {
function getInvidiousChannelLive (line 211) | async function getInvidiousChannelLive(channelId, sortBy, continuation) {
function getInvidiousChannelPlaylists (line 226) | async function getInvidiousChannelPlaylists(channelId, sortBy, continuat...
function getInvidiousChannelReleases (line 235) | async function getInvidiousChannelReleases(channelId, continuation) {
function getInvidiousChannelPodcasts (line 244) | async function getInvidiousChannelPodcasts(channelId, continuation) {
function getInvidiousChannelCourses (line 253) | async function getInvidiousChannelCourses(channelId, continuation) {
function searchInvidiousChannel (line 263) | async function searchInvidiousChannel(channelId, query, page) {
function invidiousGetPlaylistInfo (line 310) | async function invidiousGetPlaylistInfo(playlistId) {
function invidiousGetVideoInformation (line 421) | async function invidiousGetVideoInformation(videoId) {
function invidiousGetComments (line 452) | async function invidiousGetComments({ id, nextPageToken = '', sortNewest...
function invidiousGetCommentReplies (line 470) | async function invidiousGetCommentReplies({ id, replyToken }) {
function getInvidiousSearchSuggestions (line 487) | async function getInvidiousSearchSuggestions(query) {
function getInvidiousPopularFeed (line 512) | async function getInvidiousPopularFeed() {
function getInvidiousSearchResults (line 538) | async function getInvidiousSearchResults(query, page, searchSettings) {
function youtubeImageUrlToInvidious (line 577) | function youtubeImageUrlToInvidious(url, currentInstance = null) {
function invidiousImageUrlToInvidious (line 600) | function invidiousImageUrlToInvidious(url, currentInstance = null) {
function parseInvidiousCommentData (line 607) | function parseInvidiousCommentData(response) {
function invidiousGetCommunityPosts (line 632) | async function invidiousGetCommunityPosts(channelId, continuation = null) {
function getInvidiousCommunityPost (line 649) | async function getInvidiousCommunityPost(postId, authorId = null) {
function getInvidiousCommunityPostComments (line 672) | async function getInvidiousCommunityPostComments({ postId, authorId }) {
function getInvidiousCommunityPostCommentReplies (line 688) | async function getInvidiousCommunityPostCommentReplies({ postId, replyTo...
function parseInvidiousCommunityData (line 703) | function parseInvidiousCommunityData(data) {
function parseInvidiousCommunityAttachments (line 723) | function parseInvidiousCommunityAttachments(data) {
function getHashtagInvidious (line 820) | async function getHashtagInvidious(hashtag, page = 1) {
function generateInvidiousDashManifestLocally (line 841) | async function generateInvidiousDashManifestLocally(formats) {
function convertInvidiousToLocalFormat (line 863) | function convertInvidiousToLocalFormat(format) {
function mapInvidiousLegacyFormat (line 933) | function mapInvidiousLegacyFormat(format) {
function normalizeManyInvidiousVideosAttributes (line 954) | function normalizeManyInvidiousVideosAttributes(videos, fallbackAuthorId...
function normalizeOneInvidiousVideoAttributes (line 966) | function normalizeOneInvidiousVideoAttributes(video, fallbackAuthorId = ...
function setMultiplePublishedTimestamps (line 978) | function setMultiplePublishedTimestamps(videos) {
function setPublishedTimestamp (line 990) | function setPublishedTimestamp(video) {
FILE: src/renderer/helpers/api/local.js
constant TRACKING_PARAM_NAMES (line 15) | const TRACKING_PARAM_NAMES = [
function createInnertube (line 79) | async function createInnertube({ withPlayer = false, location = undefine...
function getLocalSearchSuggestions (line 111) | async function getLocalSearchSuggestions(query) {
function clearLocalSearchSuggestionsSession (line 126) | function clearLocalSearchSuggestionsSession() {
function getLocalPlaylist (line 130) | async function getLocalPlaylist(id) {
function serializeContinuationItem (line 146) | function serializeContinuationItem(continuationItem, actions) {
function extractFeedContinuationItem (line 174) | function extractFeedContinuationItem(feed) {
function extractLocalCacheablePlaylistContinuation (line 197) | function extractLocalCacheablePlaylistContinuation(playlist) {
function extractLocalCacheableSearchContinuation (line 223) | function extractLocalCacheableSearchContinuation(search) {
function getLocalCachedFeedContinuation (line 247) | async function getLocalCachedFeedContinuation(type, continuation) {
function getLocalPlaylistContinuation (line 271) | async function getLocalPlaylistContinuation(playlist) {
function untilEndOfLocalPlayList (line 298) | async function untilEndOfLocalPlayList(playlist, callback, options = { r...
function getLocalTrending (line 312) | async function getLocalTrending(location, tab) {
function getLocalSearchResults (line 354) | async function getLocalSearchResults(query, filters, safetyMode) {
function getLocalSearchContinuation (line 364) | async function getLocalSearchContinuation(continuationData) {
function getLocalVideoInfo (line 390) | async function getLocalVideoInfo(id) {
function getLocalComments (line 584) | async function getLocalComments(id) {
function decipherFormats (line 602) | async function decipherFormats(formats, player) {
function decipherManifestUrl (line 616) | async function decipherManifestUrl(url, player, poToken, isDash) {
function getLocalChannelId (line 667) | async function getLocalChannelId(url, doLogError = false) {
function getLocalChannel (line 700) | async function getLocalChannel(id) {
function getLocalChannelVideos (line 720) | async function getLocalChannelVideos(id) {
function getLocalChannelLiveStreams (line 777) | async function getLocalChannelLiveStreams(id) {
function getLocalChannelCommunity (line 825) | async function getLocalChannelCommunity(id) {
function getLocalArtistTopicChannelReleases (line 858) | async function getLocalArtistTopicChannelReleases(channel) {
function getLocalArtistTopicChannelReleasesContinuation (line 888) | async function getLocalArtistTopicChannelReleasesContinuation(channel, c...
function parseLocalChannelHeader (line 908) | function parseLocalChannelHeader(channel, onlyIdNameThumbnail = false) {
function parseLocalChannelVideos (line 1075) | function parseLocalChannelVideos(videos, channelId, channelName) {
function parseShort (line 1094) | function parseShort(short, channelId, channelName) {
function parseLocalChannelShorts (line 1129) | function parseLocalChannelShorts(shorts, channelId, channelName) {
function parseLocalListPlaylist (line 1138) | function parseLocalListPlaylist(playlist, channelId = undefined, channel...
function handleSearchResponse (line 1210) | function handleSearchResponse(response) {
function parseChannelHomeTab (line 1237) | function parseChannelHomeTab(homeTab, channelId, channelName) {
function parseLocalPlaylistVideo (line 1302) | function parseLocalPlaylistVideo(video) {
function parseLocalListVideo (line 1410) | function parseLocalListVideo(item, channelId, channelName) {
constant VIEWS_OR_WATCHING_REGEX (line 1520) | const VIEWS_OR_WATCHING_REGEX = /views?|watching/i
function parseLockupView (line 1527) | function parseLockupView(lockupView, channelId = undefined, channelName ...
function parseListItem (line 1630) | function parseListItem(item, channelId, channelName) {
function parseLocalWatchNextVideo (line 1743) | function parseLocalWatchNextVideo(video) {
function convertSearchFilters (line 1779) | function convertSearchFilters(filters) {
function parseLocalTextRuns (line 1815) | function parseLocalTextRuns(runs, emojiSize = 16, options = { looseChann...
function mapLocalLegacyFormat (line 1938) | function mapLocalLegacyFormat(format) {
function parseLocalComment (line 1980) | function parseLocalComment(comment, commentThread = undefined) {
function parseLocalSubscriberCount (line 2020) | function parseLocalSubscriberCount(text) {
function parseLocalCommunityPosts (line 2059) | function parseLocalCommunityPosts(posts) {
function parseLocalCommunityPost (line 2079) | function parseLocalCommunityPost(post) {
function parseLocalAttachment (line 2108) | function parseLocalAttachment(attachment) {
function getHashtagLocal (line 2162) | async function getHashtagLocal(hashtag) {
function getLocalCommunityPost (line 2167) | async function getLocalCommunityPost(postId, channelId) {
function getLocalCommunityPostComments (line 2181) | async function getLocalCommunityPostComments(postId, channelId) {
FILE: src/renderer/helpers/channels.js
function findChannelById (line 12) | async function findChannelById(id, backendOptions) {
function findChannelTagInfo (line 45) | async function findChannelTagInfo(id, backendOptions) {
function checkYoutubeChannelId (line 77) | function checkYoutubeChannelId(id) {
FILE: src/renderer/helpers/colors.js
function getRandomColorClass (line 93) | function getRandomColorClass() {
function getRandomColor (line 97) | function getRandomColor() {
function calculateColorLuminance (line 101) | function calculateColorLuminance(colorValue) {
FILE: src/renderer/helpers/player/EbmlParser.js
class EbmlParser (line 10) | class EbmlParser {
method constructor (line 14) | constructor(data) {
method hasMoreData (line 25) | hasMoreData() {
method parseElement (line 35) | parseElement() {
method parseId_ (line 71) | parseId_() {
method parseVint_ (line 101) | parseVint_() {
class EbmlElement (line 128) | class EbmlElement {
method constructor (line 133) | constructor(id, dataView) {
method getOffset (line 145) | getOffset() {
method createParser (line 153) | createParser() {
method getUint (line 161) | getUint() {
method getFloat (line 195) | getFloat() {
function getVintValue (line 219) | function getVintValue(vint) {
constant DYNAMIC_SIZES (line 251) | const DYNAMIC_SIZES = [
function isDynamicSizeValue (line 268) | function isDynamicSizeValue(vint) {
FILE: src/renderer/helpers/player/Mp4SegmentIndexParser.js
function parseMp4SegmentIndex (line 25) | function parseMp4SegmentIndex(
function parseSIDX (line 77) | function parseSIDX(
FILE: src/renderer/helpers/player/SabrManifestParser.js
constant MANIFEST_TYPE_SABR (line 64) | const MANIFEST_TYPE_SABR = 'application/sabr+json'
constant CODECS_REGEX (line 66) | const CODECS_REGEX = /codecs="?([^"]+)"?/
constant VIDEO_CODEC_PRIORITIES (line 68) | const VIDEO_CODEC_PRIORITIES = [
class SabrManifestParser (line 78) | class SabrManifestParser {
method constructor (line 79) | constructor() {
method banLocation (line 90) | banLocation(_uri) {
method configure (line 97) | configure(config, _isPreloadFn) {
method onInitialVariantChosen (line 104) | onInitialVariantChosen(_variant) {
method setMediaElement (line 110) | setMediaElement(_mediaElement) {
method start (line 118) | async start(uri, { filter, networkingEngine }) {
method stop (line 269) | stop() {
function buildFormatId (line 278) | function buildFormatId(format) {
function createAudioStream (line 291) | function createAudioStream(
function createVideoStream (line 398) | function createVideoStream(format, id, presentationTimeline, networkingE...
function createTextStreams (line 467) | function createTextStreams(captions, presentationTimeline, currentId) {
function createImageStreams (line 528) | function createImageStreams(storyboards, presentationTimeline, currentId) {
function createMediaSegmentIndex (line 622) | async function createMediaSegmentIndex(
function createVodMediaSegmentIndex (line 684) | function createVodMediaSegmentIndex(url, response, format, stream, durat...
FILE: src/renderer/helpers/player/SabrSchemePlugin.js
function formatIdFromString (line 86) | function formatIdFromString(str) {
function createBufferedRange (line 101) | function createBufferedRange(formatId, buffered, segmentIndex) {
function createFullBufferRange (line 123) | function createFullBufferRange(formatId) {
function fillBufferedRanges (line 147) | function fillBufferedRanges(player, manifest, audioFormatsActive, stream...
function createCacheResponse (line 203) | function createCacheResponse(uri, request, data) {
function createRecoverableNetworkError (line 218) | function createRecoverableNetworkError(code, ...args) {
function prepareSabrContexts (line 225) | function prepareSabrContexts(sabrStreamState) {
function decodePart (line 248) | function decodePart(part, decoder) {
function createTimeoutController (line 264) | function createTimeoutController(callback, timeoutMs) {
function doRequest (line 285) | async function doRequest(
function setupSabrScheme (line 625) | function setupSabrScheme(sabrData, getPlayer, getManifest, playerWidth, ...
FILE: src/renderer/helpers/player/WebmSegmentIndexParser.js
constant EBML_ID (line 10) | const EBML_ID = 0x1a45dfa3
constant SEGMENT_ID (line 11) | const SEGMENT_ID = 0x18538067
constant INFO_ID (line 12) | const INFO_ID = 0x1549a966
constant TIMECODE_SCALE_ID (line 13) | const TIMECODE_SCALE_ID = 0x2ad7b1
constant DURATION_ID (line 14) | const DURATION_ID = 0x4489
constant CUES_ID (line 15) | const CUES_ID = 0x1c53bb6b
constant CUE_POINT_ID (line 16) | const CUE_POINT_ID = 0xbb
constant CUE_TIME_ID (line 17) | const CUE_TIME_ID = 0xb3
constant CUE_TRACK_POSITIONS_ID (line 18) | const CUE_TRACK_POSITIONS_ID = 0xb7
constant CUE_CLUSTER_POSITION (line 19) | const CUE_CLUSTER_POSITION = 0xf1
function parseWebmSegmentIndex (line 38) | function parseWebmSegmentIndex(
function parseWebmContainer (line 81) | function parseWebmContainer(initData) {
function parseSegment (line 125) | function parseSegment(segmentElement) {
function parseInfo (line 160) | function parseInfo(infoElement) {
function parseCues (line 207) | function parseCues(
function parseCuePoint (line 298) | function parseCuePoint(cuePointElement) {
FILE: src/renderer/helpers/player/utils.js
function logShakaError (line 14) | function logShakaError(error, context, videoId, details) {
function getSponsorBlockSegments (line 64) | async function getSponsorBlockSegments(videoId, categories) {
function translateSponsorBlockCategory (line 98) | function translateSponsorBlockCategory(category) {
function sortCaptions (line 133) | function sortCaptions(captions) {
function repairInvidiousManifest (line 202) | function repairInvidiousManifest(periods) {
function findMostSimilarAudioBandwidth (line 318) | function findMostSimilarAudioBandwidth(variants, bandwidthToMatch) {
function deduplicateAudioTracks (line 337) | function deduplicateAudioTracks(tracks) {
FILE: src/renderer/helpers/playlists.js
constant SORT_BY_VALUES (line 1) | const SORT_BY_VALUES = {
function getSortedPlaylistItems (line 15) | function getSortedPlaylistItems(playlistItems, sortOrder, locale, revers...
function videoDurationPresent (line 40) | function videoDurationPresent(video) {
function videoDurationWithFallback (line 46) | function videoDurationWithFallback(video) {
function publishedWithFallback (line 53) | function publishedWithFallback(video) {
function compareTwoPlaylistItems (line 58) | function compareTwoPlaylistItems(a, b, sortOrder, collator) {
function processToBeAddedPlaylistVideo (line 95) | function processToBeAddedPlaylistVideo(videoData) {
FILE: src/renderer/helpers/sponsorblock.js
function getVideoHash (line 3) | async function getVideoHash(videoId) {
function sponsorBlockSkipSegments (line 34) | async function sponsorBlockSkipSegments(videoId, categories) {
function deArrowData (line 61) | async function deArrowData(videoId) {
function deArrowThumbnail (line 81) | async function deArrowThumbnail(videoId, timestamp) {
FILE: src/renderer/helpers/strings.js
function isNullOrEmpty (line 8) | function isNullOrEmpty(_string) {
function isKeyboardEventKeyPrintableChar (line 17) | function isKeyboardEventKeyPrintableChar(eventKey) {
function translateWindowTitle (line 32) | function translateWindowTitle(title) {
function getFirstCharacter (line 68) | function getFirstCharacter(text, locale) {
FILE: src/renderer/helpers/subscriptions.js
function updateVideoListAfterProcessing (line 7) | function updateVideoListAfterProcessing(videos) {
function parseYouTubeRSSFeed (line 70) | async function parseYouTubeRSSFeed(rssString, channelId) {
function parseRSSEntry (line 99) | async function parseRSSEntry(entry, channelId, channelName) {
FILE: src/renderer/helpers/utils.js
constant CHANNEL_HANDLE_REGEX (line 8) | const CHANNEL_HANDLE_REGEX = /^@[\w.-]{3,30}$/
constant PUBLISHED_TEXT_REGEX (line 10) | const PUBLISHED_TEXT_REGEX = /(\d+)\s?([a-z]+)/i
function getIconForSortPreference (line 16) | function getIconForSortPreference(sortPreference) {
function calculatePublishedDate (line 57) | function calculatePublishedDate(publishedText, isLive = false, isUpcomin...
function buildVTTFileLocally (line 107) | function buildVTTFileLocally(storyboard, videoLengthSeconds) {
function showToast (line 172) | function showToast(message, time = null, action = null, abortSignal = nu...
function copyToClipboard (line 198) | async function copyToClipboard(content, { messageOnSuccess = null, messa...
function openExternalLink (line 222) | async function openExternalLink(url) {
function openInternalPath (line 235) | function openInternalPath({ path, query = undefined, doCreateNewWindow, ...
function readFileWithPicker (line 253) | async function readFileWithPicker(
function writeFileWithPicker (line 340) | async function writeFileWithPicker(
function createWebURL (line 415) | function createWebURL(path) {
function formatDurationAsTimestamp (line 440) | function formatDurationAsTimestamp(lengthSeconds) {
function searchFiltersMatch (line 481) | function searchFiltersMatch(filtersA, filtersB) {
function replaceFilenameForbiddenChars (line 493) | function replaceFilenameForbiddenChars(filenameOriginal) {
function getSystemLocale (line 529) | async function getSystemLocale() {
function extractNumberFromString (line 542) | function extractNumberFromString(str) {
function showExternalPlayerUnsupportedActionToast (line 554) | function showExternalPlayerUnsupportedActionToast(externalPlayer, action) {
function getVideoParamsFromUrl (line 587) | function getVideoParamsFromUrl(url) {
function toDistractionFreeTitle (line 683) | function toDistractionFreeTitle(title, minUpperCase = 3) {
function formatNumber (line 708) | function formatNumber(number, options = undefined) {
function getTodayDateStrLocalTimezone (line 712) | function getTodayDateStrLocalTimezone() {
function getRelativeTimeFromDate (line 730) | function getRelativeTimeFromDate(date, hideSeconds = false, useThirtyDay...
function escapeHTML (line 790) | function escapeHTML(untrusted) {
function deepCopy (line 804) | function deepCopy(obj) {
function fetchWithTimeout (line 815) | async function fetchWithTimeout(timeoutMs, input, init) {
function ctrlFHandler (line 845) | function ctrlFHandler(event, inputElement) {
function randomArrayItem (line 860) | function randomArrayItem(array) {
function removeFromArrayIfExists (line 869) | function removeFromArrayIfExists(array, entry) {
function base64EncodeUtf8 (line 880) | function base64EncodeUtf8(text) {
function getChannelPlaylistId (line 905) | function getChannelPlaylistId(channelId, type, sortBy) {
function getIndividualLocalizedShortcut (line 930) | function getIndividualLocalizedShortcut(shortcut) {
function getMacIconForShortcut (line 955) | function getMacIconForShortcut(shortcut) {
function getLocalizedShortcut (line 986) | function getLocalizedShortcut(shortcut) {
function addKeyboardShortcutToActionTitle (line 1004) | function addKeyboardShortcutToActionTitle(actionTitle, shortcut) {
function localizeAndAddKeyboardShortcutToActionTitle (line 1016) | function localizeAndAddKeyboardShortcutToActionTitle(localizedActionTitl...
function debounce (line 1032) | function debounce(func, wait) {
function throttle (line 1056) | function throttle(func, wait) {
FILE: src/renderer/i18n/index.js
function loadLocale (line 25) | async function loadLocale(locale) {
FILE: src/renderer/router/index.js
method scrollBehavior (line 146) | scrollBehavior(to, from, savedPosition) {
FILE: src/renderer/store/modules/history.js
method getHistoryCacheSorted (line 12) | getHistoryCacheSorted(state) {
method getHistoryCacheById (line 16) | getHistoryCacheById(state) {
method grabHistory (line 22) | async grabHistory({ commit }) {
method updateHistory (line 38) | async updateHistory({ commit }, record) {
method overwriteHistory (line 51) | async overwriteHistory({ commit }, historyItems) {
method removeFromHistory (line 68) | async removeFromHistory({ commit }, videoId) {
method removeAllHistory (line 77) | async removeAllHistory({ commit }) {
method updateWatchProgress (line 87) | async updateWatchProgress({ commit }, { videoId, watchProgress }) {
method updateLastViewedPlaylist (line 96) | async updateLastViewedPlaylist({ commit }, { videoId, lastViewedPlaylist...
method setHistoryCacheSorted (line 107) | setHistoryCacheSorted(state, historyCacheSorted) {
method setHistoryCacheById (line 111) | setHistoryCacheById(state, historyCacheById) {
method upsertToHistoryCache (line 115) | upsertToHistoryCache(state, record) {
method updateRecordWatchProgressInHistoryCache (line 130) | updateRecordWatchProgressInHistoryCache(state, { videoId, watchProgress ...
method updateRecordLastViewedPlaylistIdInHistoryCache (line 142) | updateRecordLastViewedPlaylistIdInHistoryCache(state, { videoId, lastVie...
method removeFromHistoryCacheById (line 156) | removeFromHistoryCacheById(state, videoId) {
FILE: src/renderer/store/modules/invidious.js
method getCurrentInvidiousInstance (line 11) | getCurrentInvidiousInstance(state) {
method getCurrentInvidiousInstanceUrl (line 15) | getCurrentInvidiousInstanceUrl(state) {
method getCurrentInvidiousInstanceAuthorization (line 19) | getCurrentInvidiousInstanceAuthorization(state) {
method getInvidiousInstancesList (line 23) | getInvidiousInstancesList(state) {
method fetchInvidiousInstancesFromFile (line 29) | async fetchInvidiousInstancesFromFile({ commit }) {
method fetchInvidiousInstances (line 43) | async fetchInvidiousInstances({ commit }) {
method setRandomCurrentInvidiousInstance (line 71) | setRandomCurrentInvidiousInstance({ commit, state }) {
method setCurrentInvidiousInstance (line 78) | setCurrentInvidiousInstance(state, value) {
method setInvidiousInstancesList (line 116) | setInvidiousInstancesList(state, value) {
FILE: src/renderer/store/modules/player.js
method cachePlayerLocale (line 11) | async cachePlayerLocale({ commit }, locale) {
method addPlayerLocaleToCache (line 24) | addPlayerLocaleToCache(state, { locale, data }) {
FILE: src/renderer/store/modules/playlists.js
function generateRandomPlaylistId (line 4) | function generateRandomPlaylistId() {
function generateRandomPlaylistName (line 8) | function generateRandomPlaylistName() {
function findEmptyOrLatestPlayedPlaylist (line 16) | function findEmptyOrLatestPlayedPlaylist(playlists) {
function processNewPlayist (line 35) | function processNewPlayist(playlist) {
method getQuickBookmarkPlaylist (line 92) | getQuickBookmarkPlaylist(state, getters) {
method addPlaylist (line 104) | async addPlaylist({ state, commit, rootState, dispatch }, payload) {
method addPlaylists (line 121) | async addPlaylists({ state, commit, rootState, dispatch }, payload) {
method updatePlaylist (line 139) | async updatePlaylist({ commit }, playlist) {
method updatePlaylistLastPlayedAt (line 159) | async updatePlaylistLastPlayedAt({ commit }, playlist) {
method addVideo (line 172) | async addVideo({ commit }, payload) {
method addVideos (line 189) | async addVideos({ commit }, payload) {
method grabAllPlaylists (line 234) | async grabAllPlaylists({ rootState, commit, dispatch, state }) {
method removeAllPlaylists (line 372) | async removeAllPlaylists({ commit }) {
method removeAllVideos (line 381) | async removeAllVideos({ commit }, _id) {
method removePlaylist (line 390) | async removePlaylist({ commit }, playlistId) {
method removePlaylists (line 399) | async removePlaylists({ commit }, playlistIds) {
method removeVideo (line 408) | async removeVideo({ commit }, payload) {
method removeVideos (line 424) | async removeVideos({ commit }, payload) {
method addPlaylist (line 442) | addPlaylist(state, payload) {
method addPlaylists (line 446) | addPlaylists(state, payload) {
method upsertPlaylistToList (line 450) | upsertPlaylistToList(state, updatedPlaylist) {
method addVideo (line 463) | addVideo(state, payload) {
method addVideos (line 471) | addVideos(state, payload) {
method removeAllPlaylists (line 479) | removeAllPlaylists(state) {
method removeAllVideos (line 483) | removeAllVideos(state, playlistId) {
method removeVideo (line 490) | removeVideo(state, { _id, lastUpdatedAt, videoId, playlistItemId }) {
method removeVideos (line 503) | removeVideos(state, { _id, lastUpdatedAt, playlistItemIds }) {
method removePlaylist (line 514) | removePlaylist(state, playlistId) {
method setAllPlaylists (line 518) | setAllPlaylists(state, payload) {
method setPlaylistsReady (line 522) | setPlaylistsReady(state, payload) {
FILE: src/renderer/store/modules/profiles.js
function profileSort (line 41) | function profileSort(a, b) {
method grabAllProfiles (line 50) | async grabAllProfiles({ rootState, commit, state }, defaultName = null) {
method batchUpdateSubscriptionDetails (line 100) | async batchUpdateSubscriptionDetails({ dispatch, state }, channels) {
method updateSubscriptionDetails (line 141) | async updateSubscriptionDetails({ dispatch, state }, { channelThumbnailU...
method createProfile (line 172) | async createProfile({ commit }, profile) {
method updateProfile (line 181) | async updateProfile({ commit }, profile) {
method addChannelToProfiles (line 190) | async addChannelToProfiles({ commit }, { channel, profileIds }) {
method removeChannelFromProfiles (line 204) | async removeChannelFromProfiles({ commit }, { channelId, profileIds }) {
method removeProfile (line 213) | async removeProfile({ commit }, profileId) {
method updateActiveProfile (line 222) | updateActiveProfile({ commit }, id) {
method setProfileList (line 228) | setProfileList(state, profileList) {
method setActiveProfile (line 232) | setActiveProfile(state, activeProfile) {
method addProfileToList (line 236) | addProfileToList(state, profile) {
method upsertProfileToList (line 241) | upsertProfileToList(state, updatedProfile) {
method addChannelToProfiles (line 255) | addChannelToProfiles(state, { channel, profileIds }) {
method removeChannelFromProfiles (line 261) | removeChannelFromProfiles(state, { channelId, profileIds }) {
method removeProfileFromList (line 271) | removeProfileFromList(state, profileId) {
FILE: src/renderer/store/modules/search-history.js
method grabSearchHistoryEntries (line 42) | async grabSearchHistoryEntries({ commit }) {
method updateSearchHistoryEntry (line 51) | async updateSearchHistoryEntry({ commit }, searchHistoryEntry) {
method overwriteSearchHistory (line 64) | async overwriteSearchHistory({ commit }, historyItems) {
method removeSearchHistoryEntry (line 77) | async removeSearchHistoryEntry({ commit }, _id) {
method removeAllSearchHistoryEntries (line 86) | async removeAllSearchHistoryEntries({ commit }) {
method setSearchHistoryEntries (line 97) | setSearchHistoryEntries(state, searchHistoryEntries) {
method upsertSearchHistoryEntryToList (line 101) | upsertSearchHistoryEntryToList(state, updatedSearchHistoryEntry) {
method removeSearchHistoryEntryFromList (line 109) | removeSearchHistoryEntryFromList(state, _id) {
FILE: src/renderer/store/modules/subscription-cache.js
method grabAllSubscriptions (line 27) | async grabAllSubscriptions({ commit, dispatch, rootGetters }) {
method updateSubscriptionVideosCacheByChannel (line 81) | async updateSubscriptionVideosCacheByChannel({ commit }, { channelId, vi...
method updateSubscriptionShortsCacheByChannel (line 90) | async updateSubscriptionShortsCacheByChannel({ commit }, { channelId, vi...
method updateSubscriptionShortsCacheWithChannelPageShorts (line 99) | async updateSubscriptionShortsCacheWithChannelPageShorts({ commit }, { c...
method updateSubscriptionLiveCacheByChannel (line 108) | async updateSubscriptionLiveCacheByChannel({ commit }, { channelId, vide...
method updateSubscriptionPostsCacheByChannel (line 117) | async updateSubscriptionPostsCacheByChannel({ commit }, { channelId, pos...
method clearSubscriptionsCacheForManyChannels (line 126) | async clearSubscriptionsCacheForManyChannels({ commit }, channelIds) {
method clearSubscriptionsCache (line 135) | async clearSubscriptionsCache({ commit }) {
method updateVideoCacheByChannel (line 146) | updateVideoCacheByChannel(state, { channelId, entries, timestamp = new D...
method updateShortsCacheByChannel (line 153) | updateShortsCacheByChannel(state, { channelId, entries, timestamp = new ...
method updateShortsCacheWithChannelPageShorts (line 160) | updateShortsCacheWithChannelPageShorts(state, { channelId, entries }) {
method updateLiveCacheByChannel (line 185) | updateLiveCacheByChannel(state, { channelId, entries, timestamp = new Da...
method updatePostsCacheByChannel (line 192) | updatePostsCacheByChannel(state, { channelId, entries, timestamp = new D...
method clearCaches (line 200) | clearCaches(state) {
method clearCachesForManyChannels (line 207) | clearCachesForManyChannels(state, channelIds) {
method setCaches (line 216) | setCaches(state, { videos, liveStreams, shorts, communityPosts }) {
method setSubscriptionCacheReady (line 223) | setSubscriptionCacheReady(state, payload) {
FILE: src/renderer/store/modules/utils.js
method getIsSideNavOpen (line 63) | getIsSideNavOpen(state) {
method getOutlinesHidden (line 67) | getOutlinesHidden(state) {
method getSessionSearchHistory (line 71) | getSessionSearchHistory(state) {
method getPopularCache (line 79) | getPopularCache(state) {
method getTrendingCache (line 83) | getTrendingCache(state) {
method getCachedPlaylist (line 87) | getCachedPlaylist(state) {
method getSearchSettings (line 91) | getSearchSettings(state) {
method getSearchFilterValueChanged (line 95) | getSearchFilterValueChanged(state) {
method getIsKeyboardShortcutPromptShown (line 99) | getIsKeyboardShortcutPromptShown(state) {
method getShowAddToPlaylistPrompt (line 103) | getShowAddToPlaylistPrompt(state) {
method getShowCreatePlaylistPrompt (line 107) | getShowCreatePlaylistPrompt(state) {
method getShowSearchFilters (line 111) | getShowSearchFilters(state) {
method getToBeAddedToPlaylistVideoList (line 115) | getToBeAddedToPlaylistVideoList(state) {
method getNewPlaylistDefaultProperties (line 119) | getNewPlaylistDefaultProperties(state) {
method getNewPlaylistVideoObject (line 123) | getNewPlaylistVideoObject(state) {
method getShowProgressBar (line 127) | getShowProgressBar(state) {
method getProgressBarPercentage (line 131) | getProgressBarPercentage(state) {
method getRegionNames (line 135) | getRegionNames(state) {
method getRegionValues (line 139) | getRegionValues(state) {
method getRecentBlogPosts (line 143) | getRecentBlogPosts(state) {
method getExternalPlayerNames (line 147) | getExternalPlayerNames(state) {
method getExternalPlayerValues (line 151) | getExternalPlayerValues(state) {
method getExternalPlayerCmdArguments (line 155) | getExternalPlayerCmdArguments (state) {
method getLastTrendingRefreshTimestamp (line 159) | getLastTrendingRefreshTimestamp(state) {
method getLastPopularRefreshTimestamp (line 163) | getLastPopularRefreshTimestamp(state) {
method getSubscriptionForVideosFirstAutoFetchRun (line 167) | getSubscriptionForVideosFirstAutoFetchRun(state) {
method getSubscriptionForLiveStreamsFirstAutoFetchRun (line 170) | getSubscriptionForLiveStreamsFirstAutoFetchRun (state) {
method getSubscriptionForShortsFirstAutoFetchRun (line 173) | getSubscriptionForShortsFirstAutoFetchRun (state) {
method getSubscriptionForPostsFirstAutoFetchRun (line 176) | getSubscriptionForPostsFirstAutoFetchRun (state) {
method getAppTitle (line 179) | getAppTitle (state) {
method isAnyPromptOpen (line 182) | isAnyPromptOpen(state) {
method showOutlines (line 188) | showOutlines({ commit }) {
method hideOutlines (line 192) | hideOutlines({ commit }) {
method showAddToPlaylistPromptForManyVideos (line 227) | showAddToPlaylistPromptForManyVideos ({ commit }, { videos: videoObjectA...
method hideAddToPlaylistPrompt (line 286) | hideAddToPlaylistPrompt ({ commit }) {
method showCreatePlaylistPrompt (line 292) | showCreatePlaylistPrompt ({ commit }, data) {
method showKeyboardShortcutPrompt (line 297) | showKeyboardShortcutPrompt ({ commit }) {
method hideKeyboardShortcutPrompt (line 301) | hideKeyboardShortcutPrompt ({ commit }) {
method showSearchFilters (line 305) | showSearchFilters ({ commit }) {
method hideSearchFilters (line 309) | hideSearchFilters ({ commit }) {
method updateShowProgressBar (line 313) | updateShowProgressBar ({ commit }, value) {
method getRegionData (line 317) | async getRegionData ({ commit }, locale) {
method getYoutubeUrlInfo (line 328) | async getYoutubeUrlInfo({ rootState, state }, urlStr) {
method clearSessionSearchHistory (line 590) | clearSessionSearchHistory ({ commit }) {
method getExternalPlayerCmdArgumentsData (line 594) | async getExternalPlayerCmdArgumentsData ({ commit }) {
method toggleSideNav (line 616) | toggleSideNav (state) {
method setOutlinesHidden (line 620) | setOutlinesHidden(state, value) {
method setShowProgressBar (line 624) | setShowProgressBar (state, value) {
method setProgressBarPercentage (line 628) | setProgressBarPercentage (state, value) {
method setSessionSearchHistory (line 632) | setSessionSearchHistory (state, history) {
method setDeArrowCache (line 636) | setDeArrowCache (state, cache) {
method addVideoToDeArrowCache (line 640) | addVideoToDeArrowCache (state, payload) {
method addThumbnailToDeArrowCache (line 648) | addThumbnailToDeArrowCache (state, payload) {
method removeFromSessionSearchHistory (line 652) | removeFromSessionSearchHistory (state, query) {
method addToSessionSearchHistory (line 656) | addToSessionSearchHistory (state, payload) {
method setShowAddToPlaylistPrompt (line 675) | setShowAddToPlaylistPrompt (state, payload) {
method setShowCreatePlaylistPrompt (line 679) | setShowCreatePlaylistPrompt (state, payload) {
method setIsKeyboardShortcutPromptShown (line 683) | setIsKeyboardShortcutPromptShown (state, payload) {
method setShowSearchFilters (line 687) | setShowSearchFilters (state, payload) {
method setToBeAddedToPlaylistVideoList (line 691) | setToBeAddedToPlaylistVideoList (state, payload) {
method setNewPlaylistDefaultProperties (line 695) | setNewPlaylistDefaultProperties (state, payload) {
method resetNewPlaylistDefaultProperties (line 698) | resetNewPlaylistDefaultProperties (state) {
method setNewPlaylistVideoObject (line 702) | setNewPlaylistVideoObject (state, payload) {
method setPopularCache (line 706) | setPopularCache (state, value) {
method setTrendingCache (line 710) | setTrendingCache (state, { value, page }) {
method setLastTrendingRefreshTimestamp (line 718) | setLastTrendingRefreshTimestamp (state, { page, timestamp }) {
method setLastPopularRefreshTimestamp (line 722) | setLastPopularRefreshTimestamp (state, timestamp) {
method clearTrendingCache (line 730) | clearTrendingCache(state, page) {
method setCachedPlaylist (line 734) | setCachedPlaylist(state, value) {
method setSearchFilterValueChanged (line 738) | setSearchFilterValueChanged (state, value) {
method setSearchSortBy (line 742) | setSearchSortBy (state, value) {
method setSearchTime (line 746) | setSearchTime (state, value) {
method setSearchType (line 750) | setSearchType (state, value) {
method setSearchDuration (line 754) | setSearchDuration (state, value) {
method setSearchFeatures (line 758) | setSearchFeatures (state, value) {
method setRegionNames (line 762) | setRegionNames (state, value) {
method setRegionValues (line 766) | setRegionValues (state, value) {
method setRecentBlogPosts (line 770) | setRecentBlogPosts (state, value) {
method setExternalPlayerNames (line 774) | setExternalPlayerNames (state, value) {
method setExternalPlayerValues (line 778) | setExternalPlayerValues (state, value) {
method setExternalPlayerCmdArguments (line 782) | setExternalPlayerCmdArguments (state, value) {
method setAppTitle (line 787) | setAppTitle (state, value) {
method addOpenPrompt (line 791) | addOpenPrompt(state, id) {
method removeOpenPrompt (line 795) | removeOpenPrompt(state, id) {
method setSubscriptionForVideosFirstAutoFetchRun (line 799) | setSubscriptionForVideosFirstAutoFetchRun (state) {
method setSubscriptionForLiveStreamsFirstAutoFetchRun (line 802) | setSubscriptionForLiveStreamsFirstAutoFetchRun (state) {
method setSubscriptionForShortsFirstAutoFetchRun (line 805) | setSubscriptionForShortsFirstAutoFetchRun (state) {
method setSubscriptionForPostsFirstAutoFetchRun (line 808) | setSubscriptionForPostsFirstAutoFetchRun (state) {
FILE: src/renderer/views/Watch/Watch.js
constant MANIFEST_TYPE_DASH (line 57) | const MANIFEST_TYPE_DASH = 'application/dash+xml'
constant MANIFEST_TYPE_HLS (line 58) | const MANIFEST_TYPE_HLS = 'application/x-mpegurl'
method channelsHidden (line 259) | channelsHidden() {
method forbiddenTitles (line 268) | forbiddenTitles() {
method canSaveWatchProgress (line 311) | canSaveWatchProgress() {
method $route (line 319) | async $route() {
method userPlaylistsReady (line 322) | userPlaylistsReady() {
method reloadView (line 340) | async reloadView() {
method onMountedDependOnLocalStateLoading (line 374) | onMountedDependOnLocalStateLoading() {
method handleWatchProgressManualSave (line 1166) | handleWatchProgressManualSave() {
method handleWatchProgressAutoSave (line 1171) | handleWatchProgressAutoSave() {
method handleWatchProgressAutoSaveWhenProgressEnabled (line 1175) | handleWatchProgressAutoSaveWhenProgressEnabled() {
method _saveWatchProgress (line 1179) | _saveWatchProgress() {
method updateLocalPlaylistLastPlayedAtSometimes (line 1834) | updateLocalPlaylistLastPlayedAtSometimes() {
method resetAutoplayInterruptionTimeout (line 1841) | resetAutoplayInterruptionTimeout() {
method updatePlaybackRate (line 1847) | updatePlaybackRate(newRate) {
method onPlayerReloadRequested (line 1858) | async onPlayerReloadRequested() {
FILE: static/pwabuilder-sw.js
constant CACHE (line 4) | const CACHE = 'pwabuilder-adv-cache'
function pathComparer (line 28) | function pathComparer(requestUrl, pathRegEx) {
function comparePaths (line 32) | function comparePaths(requestUrl, pathsArray) {
function cacheFirstFetch (line 79) | function cacheFirstFetch(event) {
function networkFirstFetch (line 121) | function networkFirstFetch(event) {
function fromCache (line 136) | function fromCache(request) {
function updateCache (line 151) | function updateCache(request, response) {
Condensed preview — 446 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (5,368K chars).
[
{
"path": ".babelrc",
"chars": 156,
"preview": "{\n \"presets\": [\n [\n \"@babel/env\",\n {\n \"targets\": {\n \"chrome\": \"130\",\n \"node\": \"20"
},
{
"path": ".editorconfig",
"chars": 188,
"preview": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ni"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.yaml",
"chars": 5743,
"preview": "name: Bug Report\ndescription: Report an issue or unexpected behavior that occurs within the application\ntitle: \"[Bug]: \""
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 812,
"preview": "blank_issues_enabled: false\ncontact_links:\n - name: Discussions\n url: https://github.com/FreeTubeApp/FreeTube/discus"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.yaml",
"chars": 4597,
"preview": "name: Feature request\ndescription: Suggest an idea for FreeTube which you would like to see in a future release\ntitle: \""
},
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"chars": 1560,
"preview": "<!-- Thanks for sending a pull request! Make sure to follow the contributing guidelines. -->\n<!-- Important note, we may"
},
{
"path": ".github/auto-merge.yml",
"chars": 155,
"preview": "minApprovals:\n COLLABORATOR: 2\nmaxRequestedChanges:\n COLLABORATOR: 0\nmergeMethod: squash\nrequiredBaseBranches:\n- dev"
},
{
"path": ".github/dependabot.yml",
"chars": 1171,
"preview": "version: 2\nupdates:\n - package-ecosystem: \"npm\"\n directory: \"/\"\n schedule:\n interval: \"weekly\"\n labels:\n "
},
{
"path": ".github/issue-labeler.yml",
"chars": 1422,
"preview": "'B: visual':\n - '(visual bug)'\n\n'B: Unofficial Download':\n - '(AUR \\(Unofficial\\)|Chocolatey \\(Unofficial\\)|\\.apk "
},
{
"path": ".github/pr-labeler.yml",
"chars": 179,
"preview": "'PR: waiting for review':\n- changed-files:\n - any-glob-to-any-file: '**'\n\n'PR: dependencies':\n- any:\n - changed-files:"
},
{
"path": ".github/workflows/autoMerge.yml",
"chars": 657,
"preview": "name: Auto Merge PR\non:\n pull_request_target:\n types: [opened, reopened, auto_merge_disabled, ready_for_review]\n\nper"
},
{
"path": ".github/workflows/build.yml",
"chars": 16883,
"preview": "# This workflow will do a clean install of node dependencies, build the source code and run tests across different versi"
},
{
"path": ".github/workflows/calibreapp-image-actions.yml",
"chars": 1585,
"preview": "# Compress images on demand (workflow_dispatch), and at 12am every Sunday (schedule).\n# Open a Pull Request if any image"
},
{
"path": ".github/workflows/codeql.yml",
"chars": 1844,
"preview": "name: \"CodeQL\"\n\non:\n push:\n branches: [ \"development\" ]\n pull_request:\n # The branches below must be a subset of"
},
{
"path": ".github/workflows/conflicts.yml",
"chars": 1018,
"preview": "name: \"Conflicts\"\non:\n # So that PRs touching the same files as the push are updated\n push:\n # So that the `dirtyLabe"
},
{
"path": ".github/workflows/flatpak.yml",
"chars": 6415,
"preview": "# This is a basic workflow that is manually triggered\n\nname: Create Flatpak PR\n\n# Controls when the action will run. Wor"
},
{
"path": ".github/workflows/label-issue.yml",
"chars": 414,
"preview": "name: \"Issue Labeler\"\non:\n issues:\n types: [opened]\n\npermissions: {}\n\njobs:\n triage:\n runs-on: ubuntu-slim\n\n "
},
{
"path": ".github/workflows/label-pr.yml",
"chars": 478,
"preview": "name: \"Pull Request Labeler\"\non:\n pull_request_target:\n types: [opened, reopened, ready_for_review]\n\npermissions: {}"
},
{
"path": ".github/workflows/linter.yml",
"chars": 1999,
"preview": "# This is a basic workflow to help you get started with Actions\n\nname: Linter\n\n# Controls when the action will run. Trig"
},
{
"path": ".github/workflows/no-response.yml",
"chars": 1293,
"preview": "name: No Response\n\n# Both `issue_comment` and `scheduled` event types are required for this Action\n# to work properly.\no"
},
{
"path": ".github/workflows/release.yml",
"chars": 27046,
"preview": "# This is a basic workflow that is manually triggered\n\nname: Upload Release\n\n# Controls when the action will run. Workfl"
},
{
"path": ".github/workflows/remove-outdated-labels.yml",
"chars": 1932,
"preview": "name: Remove outdated labels\non:\n pull_request_target:\n types:\n - closed\n - converted_to_draft\n - rea"
},
{
"path": ".github/workflows/stale.yml",
"chars": 1268,
"preview": "name: 'Close stale PRs'\non:\n schedule:\n - cron: '30 1 * * *'\n\npermissions: {}\n\njobs:\n stale:\n # As this action r"
},
{
"path": ".github/workflows/updateSite.yml",
"chars": 2753,
"preview": "# This is a basic workflow that is manually triggered\n\nname: Update Site Version Number\n\n# Controls when the action will"
},
{
"path": ".gitignore",
"chars": 371,
"preview": ".DS_Store\ndist/electron/*\nstoryboards/*\ndashFiles/*\nstatic/dashFiles\nstatic/storyboards\nstatic/dashFiles/*\nstatic/storyb"
},
{
"path": ".stylelintignore",
"chars": 89,
"preview": "src/data/\nsrc/datastores/\nsrc/main/\nsrc/renderer/videoJS.css\ndist/\nstatic/\nnode_modules/\n"
},
{
"path": ".vscode/extensions.json",
"chars": 220,
"preview": "{\n \"recommendations\": [\n \"editorconfig.editorconfig\",\n \"dbaeumer.vscode-eslint\",\n \"stylelint.vscode-stylelint\""
},
{
"path": ".vscode/launch.json",
"chars": 600,
"preview": "{\n \"version\": \"0.2.0\",\n \"configurations\": [\n {\n \"name\": \"dev-runner (Electron)\",\n \"type\": \"node\",\n \""
},
{
"path": ".vscode/settings.json",
"chars": 544,
"preview": "{\n \"stylelint.packageManager\": \"yarn\",\n \"stylelint.snippet\": [\n \"css\",\n \"less\",\n \"postcss\",\n \"sass"
},
{
"path": ".whitesource",
"chars": 278,
"preview": "##########################################################\n#### WhiteSource \"Bolt for Github\" configuration file ####\n##"
},
{
"path": "CONTRIBUTING.md",
"chars": 3492,
"preview": "# Code Contributions\n\n## Before starting to code\n Please follow these guidelines before starting to code you feature or "
},
{
"path": "LICENSE",
"chars": 34523,
"preview": " GNU AFFERO GENERAL PUBLIC LICENSE\n Version 3, 19 November 2007\n\n Copyright (C)"
},
{
"path": "README.md",
"chars": 10846,
"preview": "<p align=\"center\">\n <img alt=\"\" src=\"/_icons/logoColor.svg\" width=500 align=\"center\">\n</p>\n\nFreeTube is an open source d"
},
{
"path": "_scripts/ProcessLocalesPlugin.js",
"chars": 6126,
"preview": "const { existsSync, readFileSync } = require('fs')\nconst { readFile } = require('fs/promises')\nconst { join } = require("
},
{
"path": "_scripts/_undefinedDefaultExport.mjs",
"chars": 25,
"preview": "export default undefined\n"
},
{
"path": "_scripts/build.mjs",
"chars": 1012,
"preview": "import { Arch, build, Platform } from 'electron-builder'\nimport config from './ebuilder.config.mjs'\n\nconst args = proces"
},
{
"path": "_scripts/clean.mjs",
"chars": 306,
"preview": "import { rm } from 'fs/promises'\nimport { join } from 'path'\n\nconst BUILD_PATH = join(import.meta.dirname, '..', 'build'"
},
{
"path": "_scripts/dev-runner.js",
"chars": 6075,
"preview": "process.env.NODE_ENV = 'development'\n\nconst electron = require('electron')\nconst webpack = require('webpack')\nconst Webp"
},
{
"path": "_scripts/ebuilder.config.mjs",
"chars": 2724,
"preview": "import packageDetails from '../package.json' with { type: 'json' }\n\n/** @type {import('electron-builder').Configuration}"
},
{
"path": "_scripts/eslint-rules/plugin.mjs",
"chars": 239,
"preview": "import preferUseI18nPolyfillRule from './prefer-use-i18n-polyfill-rule.mjs'\n\nexport default {\n meta: {\n name: 'eslin"
},
{
"path": "_scripts/eslint-rules/prefer-use-i18n-polyfill-rule.mjs",
"chars": 2502,
"preview": "import { dirname, relative, resolve } from 'path'\n\nconst polyfillPath = resolve(import.meta.dirname, '../../src/renderer"
},
{
"path": "_scripts/findMissingTemplates.mjs",
"chars": 3062,
"preview": "import { readdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { load as loa"
},
{
"path": "_scripts/getInstances.js",
"chars": 522,
"preview": "const fs = require('fs/promises')\nconst invidiousApiUrl = 'https://api.invidious.io/instances.json'\n\nfetch(invidiousApiU"
},
{
"path": "_scripts/getRegions.mjs",
"chars": 6208,
"preview": "/**\n * This script updates the files in static/geolocations with the available locations on YouTube.\n *\n * It tries to m"
},
{
"path": "_scripts/getShakaLocales.js",
"chars": 3051,
"preview": "const { readFileSync, readdirSync } = require('fs')\nconst { join } = require('path')\n\nfunction getPreloadedLocales() {\n "
},
{
"path": "_scripts/injectAllowedPaths.mjs",
"chars": 1916,
"preview": "/**\n * Injects the paths that the renderer process is allowed to read into the main.js file,\n * by replacing __FREETUBE_"
},
{
"path": "_scripts/mime-db-shrinking-loader.js",
"chars": 992,
"preview": "/**\n * electron-context-menu only needs mime-db for its save as feature.\n * As we only activate save image and save as i"
},
{
"path": "_scripts/patch-shaka-player-loader.js",
"chars": 275,
"preview": "/**\n * fixes shaka-player referencing the Roboto font on google fonts in its CSS\n * by updating the CSS to point to the "
},
{
"path": "_scripts/sigFrameConfig.js",
"chars": 506,
"preview": "const { hash } = require('crypto')\nconst { join } = require('path')\nconst { readFileSync } = require('fs')\n\nconst path ="
},
{
"path": "_scripts/webpack.botGuardScript.config.js",
"chars": 563,
"preview": "const path = require('path')\n\n/** @type {import('webpack').Configuration} */\nmodule.exports = {\n name: 'botGuardScript'"
},
{
"path": "_scripts/webpack.main.config.js",
"chars": 1961,
"preview": "const path = require('path')\nconst webpack = require('webpack')\nconst CopyWebpackPlugin = require('copy-webpack-plugin')"
},
{
"path": "_scripts/webpack.preload.config.js",
"chars": 854,
"preview": "const path = require('path')\n\nconst isDevMode = process.env.NODE_ENV === 'development'\n\n/** @type {import('webpack').Con"
},
{
"path": "_scripts/webpack.renderer.config.js",
"chars": 7226,
"preview": "const path = require('path')\nconst { readFileSync, readdirSync } = require('fs')\nconst webpack = require('webpack')\ncons"
},
{
"path": "_scripts/webpack.web.config.js",
"chars": 6947,
"preview": "const path = require('path')\nconst fs = require('fs')\nconst webpack = require('webpack')\nconst HtmlWebpackPlugin = requi"
},
{
"path": "eslint.config.mjs",
"chars": 14992,
"preview": "import eslintPluginVue from 'eslint-plugin-vue'\nimport vuejsAccessibility from 'eslint-plugin-vuejs-accessibility'\nimpor"
},
{
"path": "jsconfig.json",
"chars": 448,
"preview": "{\n \"vueCompilerOptions\": {\n \"target\": 3.5\n },\n \"compilerOptions\": {\n \"module\": \"esnext\",\n \"moduleResolution\""
},
{
"path": "lefthook-local.yml.example",
"chars": 291,
"preview": "# See following doc for details\n# https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md#rc\n\n# You c"
},
{
"path": "lefthook.yml",
"chars": 1221,
"preview": "# Refer for explanation to following link:\n# https://github.com/evilmartians/lefthook/blob/master/docs/full_guide.md\npre"
},
{
"path": "package.json",
"chars": 5054,
"preview": "{\n \"name\": \"freetube\",\n \"productName\": \"FreeTube\",\n \"description\": \"A private YouTube client\",\n \"version\": \"0.23.15\""
},
{
"path": "src/botGuardScript.js",
"chars": 2824,
"preview": "import { BG, buildURL, GOOG_API_KEY } from 'bgutils-js'\n\n// This script has it's own webpack config, as it gets passed a"
},
{
"path": "src/constants.js",
"chars": 13017,
"preview": "// IPC Channels\nconst IpcChannels = {\n ENABLE_PROXY: 'enable-proxy',\n DISABLE_PROXY: 'disable-proxy',\n GET_SYSTEM_LOC"
},
{
"path": "src/data/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "src/datastores/handlers/base.js",
"chars": 11549,
"preview": "import * as db from '../index'\n\nclass Settings {\n static async find() {\n const currentLocale = await db.settings.fin"
},
{
"path": "src/datastores/handlers/electron.js",
"chars": 5967,
"preview": "import { DBActions } from '../../constants'\n\nclass Settings {\n static find() {\n return window.ftElectron.dbSettings("
},
{
"path": "src/datastores/handlers/index.js",
"chars": 284,
"preview": "export {\n settings as DBSettingHandlers,\n history as DBHistoryHandlers,\n profiles as DBProfileHandlers,\n playlists a"
},
{
"path": "src/datastores/handlers/web.js",
"chars": 898,
"preview": "import * as baseHandlers from './base'\n\n// TODO: Syncing\n// Syncing on the web would involve a different implementation\n"
},
{
"path": "src/datastores/index.js",
"chars": 1322,
"preview": "import Datastore from '@seald-io/nedb'\n\nlet dbPath = null\n\nif (process.env.IS_ELECTRON_MAIN) {\n const { app } = require"
},
{
"path": "src/index.ejs",
"chars": 1543,
"preview": "<!DOCTYPE html>\n<html>\n\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\"\n content=\"width=device-"
},
{
"path": "src/main/ImageCache.js",
"chars": 1898,
"preview": "// cleanup expired images once every 5 mins\nconst CLEANUP_INTERVAL = 300_000\n\n// images expire after 2 hours if no expir"
},
{
"path": "src/main/externalPlayer.js",
"chars": 7285,
"preview": "import { spawn } from 'node:child_process'\nimport { join } from 'node:path'\nimport { readFile } from 'node:fs/promises'\n"
},
{
"path": "src/main/index.js",
"chars": 77855,
"preview": "import {\n app, BrowserWindow, dialog, Menu, ipcMain,\n powerSaveBlocker, screen, session, shell,\n nativeTheme, net, pr"
},
{
"path": "src/main/poTokenGenerator.js",
"chars": 7142,
"preview": "import { session, WebContentsView } from 'electron'\nimport { readFile } from 'fs/promises'\nimport { join } from 'path'\n\n"
},
{
"path": "src/main/utils.js",
"chars": 527,
"preview": "/**\n * @param {string | URL} url\n */\nexport function isFreeTubeUrl(url) {\n let url_\n\n if (url instanceof URL) {\n ur"
},
{
"path": "src/preload/interface.js",
"chars": 8835,
"preview": "import { ipcRenderer, webFrame } from 'electron/renderer'\nimport { IpcChannels } from '../constants.js'\n\n/**\n * Linux fi"
},
{
"path": "src/preload/main.js",
"chars": 135,
"preview": "import { contextBridge } from 'electron/renderer'\nimport api from './interface.js'\n\ncontextBridge.exposeInMainWorld('ftE"
},
{
"path": "src/preload/preload-interface.d.ts",
"chars": 105,
"preview": "import api from './interface.js'\n\ndeclare global {\n interface Window {\n ftElectron: typeof api\n }\n}\n"
},
{
"path": "src/renderer/App.css",
"chars": 1239,
"preview": "@font-face {\n font-family: Roboto;\n src: url('assets/font/Roboto-Regular.ttf');\n}\n\n.app {\n display: flex;\n flex-wrap"
},
{
"path": "src/renderer/App.vue",
"chars": 22950,
"preview": "<template>\n <div\n v-if=\"dataReady\"\n class=\"app\"\n :class=\"{\n hideOutlines: outlinesHidden,\n isLocaleR"
},
{
"path": "src/renderer/components/ChannelAbout/ChannelAbout.css",
"chars": 630,
"preview": ".about {\n background-color: var(--card-bg-color);\n margin-block-start: 0;\n padding: 10px;\n position: relative;\n z-i"
},
{
"path": "src/renderer/components/ChannelAbout/ChannelAbout.vue",
"chars": 4288,
"preview": "<template>\n <div\n class=\"about\"\n >\n <template\n v-if=\"description\"\n >\n <h2>{{ $t(\"Channel.About.Chan"
},
{
"path": "src/renderer/components/ChannelDetails/ChannelDetails.css",
"chars": 3088,
"preview": ".bannerContainer {\n background: center / cover no-repeat var(--banner-url, transparent);\n block-size: 13vw;\n min-bloc"
},
{
"path": "src/renderer/components/ChannelDetails/ChannelDetails.vue",
"chars": 12164,
"preview": "<template>\n <FtCard>\n <div\n class=\"bannerContainer\"\n :class=\"{\n default: !bannerUrl\n }\"\n "
},
{
"path": "src/renderer/components/ChannelHome/ChannelHome.css",
"chars": 762,
"preview": ".shelfContainer {\n max-inline-size: 85vw;\n}\n\n.shelfTitle {\n font-size: 24px;\n cursor: pointer;\n\n /* Prevents overflo"
},
{
"path": "src/renderer/components/ChannelHome/ChannelHome.vue",
"chars": 1936,
"preview": "<template>\n <div>\n <div\n v-for=\"(shelf, index) in filteredShelves\"\n :key=\"index\"\n class=\"shelfContain"
},
{
"path": "src/renderer/components/CommentSection/CommentSection.css",
"chars": 3430,
"preview": ".card {\n padding-block: 0;\n padding-inline: 16px;\n}\n\n.getCommentsTitle,\n.commentsTitle,\n.getMoreComments {\n margin: 0"
},
{
"path": "src/renderer/components/CommentSection/CommentSection.vue",
"chars": 25211,
"preview": "<template>\n <FtCard\n class=\"card\"\n >\n <h3\n v-if=\"commentData.length > 0 && !isLoading && showComments\"\n "
},
{
"path": "src/renderer/components/DataSettings/DataSettings.css",
"chars": 36,
"preview": ".box {\n justify-content: center;\n}\n"
},
{
"path": "src/renderer/components/DataSettings/DataSettings.vue",
"chars": 43712,
"preview": "<template>\n <FtSettingsSection\n :title=\"$t('Settings.Data Settings.Data Settings')\"\n >\n <h4 class=\"groupTitle\">\n"
},
{
"path": "src/renderer/components/DistractionSettings/DistractionSettings.css",
"chars": 140,
"preview": "@media only screen and (width <= 800px) {\n .hide-on-mobile {\n display: none;\n }\n}\n\n.containingTextFlexBox {\n margi"
},
{
"path": "src/renderer/components/DistractionSettings/DistractionSettings.vue",
"chars": 23426,
"preview": "<template>\n <FtSettingsSection\n :title=\"t('Settings.Distraction Free Settings.Distraction Free Settings')\"\n >\n <"
},
{
"path": "src/renderer/components/ExperimentalSettings/ExperimentalSettings.css",
"chars": 91,
"preview": ".experimental-warning {\n text-align: center;\n font-weight: bold;\n padding-inline: 4%;\n}\n"
},
{
"path": "src/renderer/components/ExperimentalSettings/ExperimentalSettings.vue",
"chars": 2064,
"preview": "<template>\n <FtSettingsSection\n :title=\"$t('Settings.Experimental Settings.Experimental Settings')\"\n >\n <p class"
},
{
"path": "src/renderer/components/ExternalPlayerSettings.vue",
"chars": 5668,
"preview": "<template>\n <FtSettingsSection\n :title=\"$t('Settings.External Player Settings.External Player Settings')\"\n >\n <F"
},
{
"path": "src/renderer/components/FtAgeRestricted/FtAgeRestricted.css",
"chars": 274,
"preview": ".message {\n padding-block: 10px;\n}\n\n.frown {\n font-size: 10em;\n block-size: 100%;\n padding-block: 20px;\n}\n\n.message,"
},
{
"path": "src/renderer/components/FtAgeRestricted/FtAgeRestricted.vue",
"chars": 759,
"preview": "<template>\n <div>\n <h2 class=\"message\">\n {{ restrictedMessage }}\n </h2>\n <div class=\"frown\">\n {{ emo"
},
{
"path": "src/renderer/components/FtAutoGrid/FtAutoGrid.css",
"chars": 176,
"preview": ".grid {\n display: grid;\n gap: 8px;\n grid-template-columns: repeat(auto-fill, minmax(262px, 1fr));\n justify-content: "
},
{
"path": "src/renderer/components/FtAutoGrid/FtAutoGrid.vue",
"chars": 249,
"preview": "<template>\n <div\n :class=\"{\n grid: grid,\n list: !grid\n }\"\n >\n <slot />\n </div>\n</template>\n\n<scrip"
},
{
"path": "src/renderer/components/FtAutoLoadNextPageWrapper.vue",
"chars": 1161,
"preview": "<template>\n <div\n class=\"ft-auto-load-next-page-wrapper\"\n >\n <div\n v-observe-visibility=\"observeVisibilityO"
},
{
"path": "src/renderer/components/FtButton/FtButton.css",
"chars": 1169,
"preview": ".btn {\n font-family: Roboto, sans-serif;\n min-inline-size: 100px;\n font-size: 0.9rem;\n padding-block: 10px;\n paddin"
},
{
"path": "src/renderer/components/FtButton/FtButton.vue",
"chars": 807,
"preview": "<template>\n <button\n class=\"btn ripple\"\n :style=\"{\n color: textColor,\n backgroundColor: backgroundColor"
},
{
"path": "src/renderer/components/FtChannelBubble/FtChannelBubble.css",
"chars": 926,
"preview": ".bubblePadding {\n position: relative;\n inline-size: 100px;\n block-size: 100px;\n padding: 10px;\n cursor: pointer;\n "
},
{
"path": "src/renderer/components/FtChannelBubble/FtChannelBubble.vue",
"chars": 1673,
"preview": "<template>\n <router-link\n v-if=\"!selectable\"\n class=\"bubblePadding\"\n :aria-labelledby=\"id\"\n :to=\"`/channel/"
},
{
"path": "src/renderer/components/FtCheckboxList/FtCheckboxList.css",
"chars": 2402,
"preview": "/* stylelint-disable no-descending-specificity */\n.pure-checkbox input[type='checkbox'] {\n border: 0;\n clip-path: circ"
},
{
"path": "src/renderer/components/FtCheckboxList/FtCheckboxList.vue",
"chars": 949,
"preview": "<template>\n <div class=\"pure-checkbox filter\">\n <h3 class=\"checkboxTitle\">\n {{ title }}\n </h3>\n <template"
},
{
"path": "src/renderer/components/FtCommunityPoll/FtCommunityPoll.css",
"chars": 1168,
"preview": ".vote-count {\n padding-block-end: 6px;\n font-size: smaller;\n}\n\n.empty-circle {\n background-color: transparent;\n bord"
},
{
"path": "src/renderer/components/FtCommunityPoll/FtCommunityPoll.vue",
"chars": 2502,
"preview": "<template>\n <div class=\"poll\">\n <div\n class=\"vote-count\"\n >\n <!-- Format the votes to be split by comma"
},
{
"path": "src/renderer/components/FtCommunityPost/FtCommunityPost.scss",
"chars": 2568,
"preview": "/* stylelint-disable property-no-vendor-prefix */\n@use '../../scss-partials/ft-list-item';\n\n.outside {\n margin: auto;\n "
},
{
"path": "src/renderer/components/FtCommunityPost/FtCommunityPost.vue",
"chars": 9240,
"preview": "<template>\n <div\n class=\"ft-list-post ft-list-item outside\"\n :appearance=\"appearance\"\n :class=\"{ list: listTyp"
},
{
"path": "src/renderer/components/FtCreatePlaylistPrompt/FtCreatePlaylistPrompt.css",
"chars": 69,
"preview": ".playlistNameInput {\n inline-size: 80%;\n max-inline-size: 600px;\n}\n"
},
{
"path": "src/renderer/components/FtCreatePlaylistPrompt/FtCreatePlaylistPrompt.vue",
"chars": 3835,
"preview": "<template>\n <FtPrompt\n :label=\"title\"\n @click=\"hideCreatePlaylistPrompt\"\n >\n <FtFlexBox>\n <FtInput\n "
},
{
"path": "src/renderer/components/FtElementList/FtElementList.css",
"chars": 35,
"preview": ".maxWidth {\n inline-size: 100%;\n}\n"
},
{
"path": "src/renderer/components/FtElementList/FtElementList.vue",
"chars": 3575,
"preview": "<template>\n <FtAutoGrid\n :grid=\"displayValue !== 'list'\"\n >\n <FtListLazyWrapper\n v-for=\"(result, index) in "
},
{
"path": "src/renderer/components/FtIconButton/FtIconButton.scss",
"chars": 4085,
"preview": "/* stylelint-disable no-descending-specificity */\n.ftIconButton {\n display: flex;\n flex-flow: row wrap;\n justify-cont"
},
{
"path": "src/renderer/components/FtIconButton/FtIconButton.vue",
"chars": 7680,
"preview": "<template>\n <div\n ref=\"ftIconButton\"\n class=\"ftIconButton\"\n @focusout=\"handleDropdownFocusOut\"\n >\n <button"
},
{
"path": "src/renderer/components/FtInput/FtInput.css",
"chars": 5358,
"preview": "/* stylelint-disable no-descending-specificity */\n.ft-input-component {\n position: relative;\n}\n\nbody[dir='rtl'] .ft-inp"
},
{
"path": "src/renderer/components/FtInput/FtInput.vue",
"chars": 13280,
"preview": "<!-- eslint-disable vuejs-accessibility/mouse-events-have-key-events -->\n<template>\n <div\n class=\"ft-input-component"
},
{
"path": "src/renderer/components/FtInputTags/FtInputTags.css",
"chars": 1324,
"preview": ".ft-input-tags-component {\n position: relative;\n background-color: var(--bg-color);\n padding: 20px;\n border-radius: "
},
{
"path": "src/renderer/components/FtInputTags/FtInputTags.vue",
"chars": 5152,
"preview": "<template>\n <div\n class=\"ft-input-tags-component\"\n >\n <div\n v-if=\"disabled\"\n class=\"disabledMsg\"\n >"
},
{
"path": "src/renderer/components/FtKeyboardShortcutPrompt/FtKeyboardShortcutPrompt.css",
"chars": 1361,
"preview": "\n\n.keyboardShortcutPrompt {\n max-inline-size: 80%;\n}\n\n.titleAndCloseButton {\n display: flex;\n justify-content: space-"
},
{
"path": "src/renderer/components/FtKeyboardShortcutPrompt/FtKeyboardShortcutPrompt.vue",
"chars": 6907,
"preview": "<template>\n <FtPrompt\n :label=\"$t('KeyboardShortcutPrompt.Keyboard Shortcuts')\"\n @click=\"hideKeyboardShortcutProm"
},
{
"path": "src/renderer/components/FtListChannel/FtListChannel.scss",
"chars": 640,
"preview": "@use '../../scss-partials/ft-list-item';\n\n.infoAndSubscribe {\n display: flex;\n flex-flow: row wrap;\n justify-content:"
},
{
"path": "src/renderer/components/FtListChannel/FtListChannel.vue",
"chars": 4819,
"preview": "<template>\n <div\n class=\"ft-list-channel ft-list-item\"\n :class=\"{\n list: listType === 'list',\n grid: li"
},
{
"path": "src/renderer/components/FtListHashtag/FtListHashtag.scss",
"chars": 116,
"preview": "@use '../../scss-partials/ft-list-item';\n\n.hashtagImage {\n color: var(--primary-text-color);\n font-size: 150px;\n}\n"
},
{
"path": "src/renderer/components/FtListHashtag/FtListHashtag.vue",
"chars": 2188,
"preview": "<template>\n <div\n class=\"ft-list-hashtag ft-list-item\"\n :class=\"{\n list: listType === 'list',\n grid: li"
},
{
"path": "src/renderer/components/FtListLazyWrapper/FtListLazyWrapper.css",
"chars": 71,
"preview": ".grid {\n min-block-size: 264px;\n}\n\n.list {\n min-block-size: 131px;\n}\n"
},
{
"path": "src/renderer/components/FtListLazyWrapper/FtListLazyWrapper.vue",
"chars": 8080,
"preview": "<template>\n <div\n v-if=\"showResult\"\n v-observe-visibility=\"visible ? false : {\n callback: onVisibilityChange"
},
{
"path": "src/renderer/components/FtListPlaylist/FtListPlaylist.scss",
"chars": 74,
"preview": "@use '../../scss-partials/ft-list-item';\n\n.blur {\n filter: blur(20px);\n}\n"
},
{
"path": "src/renderer/components/FtListPlaylist/FtListPlaylist.vue",
"chars": 7952,
"preview": "<template>\n <div\n class=\"ft-list-video ft-list-item\"\n :class=\"{\n [appearance]: true,\n list: listType =="
},
{
"path": "src/renderer/components/FtListVideoLazy.vue",
"chars": 4495,
"preview": "<template>\n <div\n v-observe-visibility=\"visible ? false : {\n callback: onVisibilityChanged\n }\"\n :style=\"{"
},
{
"path": "src/renderer/components/FtListVideoNumbered/FtListVideoNumbered.css",
"chars": 407,
"preview": "/*\n Set a height to invisible/unloaded elements, so that lazy loading actually works.\n If we don't set a height, they "
},
{
"path": "src/renderer/components/FtListVideoNumbered/FtListVideoNumbered.vue",
"chars": 3997,
"preview": "<template>\n <div\n v-observe-visibility=\"visible ? false : {\n callback: onVisibilityChanged\n }\"\n :class=\"{"
},
{
"path": "src/renderer/components/FtLoader/FtLoader.css",
"chars": 1574,
"preview": "/*\n This file is part of FreeTube.\n\n FreeTube is free software: you can redistribute it and/or modify\n it under"
},
{
"path": "src/renderer/components/FtLoader/FtLoader.vue",
"chars": 344,
"preview": "<template>\n <div\n class=\"container\"\n :class=\"{ fullscreen }\"\n >\n <div class=\"spinner\">\n <div class=\"doub"
},
{
"path": "src/renderer/components/FtLogoFull/FtLogoFull.css",
"chars": 170,
"preview": ".primary-color {\n fill: var(--logo-primary-color);\n}\n\n.secondary-color {\n fill: var(--logo-secondary-color);\n}\n\n.terti"
},
{
"path": "src/renderer/components/FtLogoFull/FtLogoFull.vue",
"chars": 10486,
"preview": "<template>\n <svg\n viewBox=\"0 0 640 200\"\n version=\"1.1\"\n xmlns=\"http://www.w3.org/2000/svg\"\n xml:space=\"pres"
},
{
"path": "src/renderer/components/FtNotificationBanner/FtNotificationBanner.css",
"chars": 786,
"preview": ".ftNotificationBanner {\n background-color: var(--primary-color);\n color: var(--text-with-main-color);\n\n /*\n backgrou"
},
{
"path": "src/renderer/components/FtNotificationBanner/FtNotificationBanner.vue",
"chars": 1027,
"preview": "<template>\n <div\n class=\"ftNotificationBanner\"\n tabindex=\"0\"\n role=\"link\"\n :title=\"message\"\n :aria-descr"
},
{
"path": "src/renderer/components/FtPlaylistAddVideoPrompt/FtPlaylistAddVideoPrompt.css",
"chars": 1262,
"preview": ".selected-count {\n text-align: center;\n}\n\n.searchInputsRow {\n display: grid;\n\n /* 2 columns */\n grid-template-column"
},
{
"path": "src/renderer/components/FtPlaylistAddVideoPrompt/FtPlaylistAddVideoPrompt.vue",
"chars": 14151,
"preview": "<template>\n <FtPrompt\n theme=\"flex-column\"\n :label=\"title\"\n :inert=\"showingCreatePlaylistPrompt\"\n @click=\"h"
},
{
"path": "src/renderer/components/FtPlaylistSelector/FtPlaylistSelector.scss",
"chars": 1505,
"preview": ".ft-playlist-selector {\n padding: 6px;\n\n &:hover ,\n &.selected {\n background-color: var(--bg-color);\n\n .thumbna"
},
{
"path": "src/renderer/components/FtPlaylistSelector/FtPlaylistSelector.vue",
"chars": 5591,
"preview": "<template>\n <div\n class=\"ft-playlist-selector grid\"\n :class=\"{ selected }\"\n @click=\"toggleSelection\"\n @keyd"
},
{
"path": "src/renderer/components/FtProfileBubble/FtProfileBubble.css",
"chars": 698,
"preview": ".bubblePadding {\n inline-size: 100px;\n block-size: 115px;\n padding-block: 10px 30px;\n padding-inline: 10px;\n cursor"
},
{
"path": "src/renderer/components/FtProfileBubble/FtProfileBubble.vue",
"chars": 1430,
"preview": "<template>\n <div\n class=\"bubblePadding\"\n tabindex=\"0\"\n role=\"button\"\n :aria-labelledby=\"id\"\n @click=\"cli"
},
{
"path": "src/renderer/components/FtProfileChannelList/FtProfileChannelList.css",
"chars": 204,
"preview": "h2,\n.selectedCount {\n text-align: center;\n}\n\n.card {\n inline-size: 85%;\n margin-block: 0 15px;\n margin-inline: auto;"
},
{
"path": "src/renderer/components/FtProfileChannelList/FtProfileChannelList.vue",
"chars": 6449,
"preview": "<template>\n <div>\n <FtCard class=\"card\">\n <h2>\n {{ $t(\"Profile.Subscription List\") }}\n </h2>\n "
},
{
"path": "src/renderer/components/FtProfileEdit/FtProfileEdit.css",
"chars": 1267,
"preview": "h2 {\n text-align: center;\n}\n\nh3 {\n margin-block: 0 10px;\n text-align: center;\n}\n\n.message {\n color: var(--tertiary-t"
},
{
"path": "src/renderer/components/FtProfileEdit/FtProfileEdit.vue",
"chars": 8115,
"preview": "<template>\n <div>\n <FtCard class=\"card\">\n <h2>{{ editOrCreateProfileLabel }}</h2>\n <FtFlexBox class=\"profi"
},
{
"path": "src/renderer/components/FtProfileFilterChannelsList/FtProfileFilterChannelsList.css",
"chars": 225,
"preview": "h2 {\n text-align: center;\n}\n\n.selected {\n text-align: center;\n}\n\n.card {\n inline-size: 85%;\n margin-block: 0 15px;\n "
},
{
"path": "src/renderer/components/FtProfileFilterChannelsList/FtProfileFilterChannelsList.vue",
"chars": 6384,
"preview": "<template>\n <div>\n <FtCard class=\"card\">\n <h2>\n {{ $t(\"Profile.Other Channels\") }}\n </h2>\n <Ft"
},
{
"path": "src/renderer/components/FtProfileSelector/FtProfileSelector.css",
"chars": 1904,
"preview": ".colorOption {\n inline-size: 40px;\n block-size: 40px;\n cursor: pointer;\n align-items: center;\n display: flex;\n jus"
},
{
"path": "src/renderer/components/FtProfileSelector/FtProfileSelector.vue",
"chars": 5798,
"preview": "<template>\n <div>\n <div\n ref=\"iconButton\"\n class=\"colorOption\"\n :title=\"$t('Profile.Toggle Profile Li"
},
{
"path": "src/renderer/components/FtProgressBar/FtProgressBar.css",
"chars": 158,
"preview": ".progressBar {\n position: fixed;\n block-size: 3px;\n inset-block-end: 0;\n inset-inline-start: 0;\n background-color: "
},
{
"path": "src/renderer/components/FtProgressBar/FtProgressBar.vue",
"chars": 355,
"preview": "<template>\n <div\n class=\"progressBar\"\n :style=\"{ inlineSize: progressBarPercentage + '%' }\"\n />\n</template>\n\n<sc"
},
{
"path": "src/renderer/components/FtPrompt/FtPrompt.css",
"chars": 1247,
"preview": ".prompt {\n position: fixed;\n inset-block-start: 0;\n inset-inline-start: 0;\n inline-size: 100%;\n block-size: 100%;\n "
},
{
"path": "src/renderer/components/FtPrompt/FtPrompt.vue",
"chars": 4345,
"preview": "<template>\n <Teleport to=\".app\">\n <div\n class=\"prompt\"\n tabindex=\"-1\"\n :inert=\"inert\"\n @click.se"
},
{
"path": "src/renderer/components/FtRadioButton/FtRadioButton.css",
"chars": 2554,
"preview": "/* stylelint-disable no-descending-specificity */\n.pure-radiobutton input[type='radio'] {\n border: 0;\n clip-path: circ"
},
{
"path": "src/renderer/components/FtRadioButton/FtRadioButton.vue",
"chars": 943,
"preview": "<template>\n <div class=\"pure-radiobutton filter\">\n <h3 class=\"radioTitle\">\n {{ title }}\n </h3>\n <template"
},
{
"path": "src/renderer/components/FtRefreshWidget/FtRefreshWidget.scss",
"chars": 817,
"preview": "@use '../../scss-partials/utils';\n\n.floatingRefreshSection {\n box-sizing: border-box;\n padding-block: 5px;\n padding-i"
},
{
"path": "src/renderer/components/FtRefreshWidget/FtRefreshWidget.vue",
"chars": 1358,
"preview": "<template>\n <div\n class=\"floatingRefreshSection\"\n >\n <p\n v-if=\"lastRefreshTimestamp\"\n class=\"lastRefre"
},
{
"path": "src/renderer/components/FtSearchFilters/FtSearchFilters.css",
"chars": 1165,
"preview": ".center {\n margin-block-start: 10px;\n text-align: center;\n user-select: none;\n}\n\n.searchRadio {\n padding: 5px;\n}\n\n.r"
},
{
"path": "src/renderer/components/FtSearchFilters/FtSearchFilters.vue",
"chars": 7259,
"preview": "<template>\n <FtPrompt\n theme=\"slim\"\n @click=\"hideSearchFilters\"\n >\n <template #label=\"{ labelId }\">\n <di"
},
{
"path": "src/renderer/components/FtSelect/FtSelect.css",
"chars": 3410,
"preview": "/*\n This file is part of FreeTube.\n\n FreeTube is free software: you can redistribute it and/or modify\n it under"
},
{
"path": "src/renderer/components/FtSelect/FtSelect.vue",
"chars": 2041,
"preview": "<template>\n <div class=\"select\">\n <select\n :id=\"id\"\n :aria-describedby=\"describeById\"\n class=\"select-"
},
{
"path": "src/renderer/components/FtSettingsMenu/FtSettingsMenu.css",
"chars": 2410,
"preview": ".settingsMenu {\n /* top nav + margin */\n inset-block-start: 96px;\n position: sticky;\n display: flex;\n flex-directio"
},
{
"path": "src/renderer/components/FtSettingsMenu/FtSettingsMenu.vue",
"chars": 1625,
"preview": "<template>\n <menu\n class=\"settingsMenu\"\n >\n <h2 class=\"header\">\n <FontAwesomeIcon\n :icon=\"['fas', 's"
},
{
"path": "src/renderer/components/FtSettingsSection/FtSettingsSection.scss",
"chars": 2061,
"preview": ".settingsSection {\n margin-block: 0;\n\n @media only screen and (width <= 800px) {\n inline-size: 100%;\n }\n}\n\n.sectio"
},
{
"path": "src/renderer/components/FtSettingsSection/FtSettingsSection.vue",
"chars": 347,
"preview": "<template>\n <div\n class=\"settingsSection\"\n >\n <div class=\"sectionBody\">\n <h3 class=\"sectionTitle\">\n "
},
{
"path": "src/renderer/components/FtShareButton/FtShareButton.css",
"chars": 1210,
"preview": ".shareLinks {\n display: grid;\n grid-auto-flow: column;\n grid-template-rows: auto auto;\n padding: 12px;\n inline-size"
},
{
"path": "src/renderer/components/FtShareButton/FtShareButton.vue",
"chars": 9636,
"preview": "<template>\n <FtIconButton\n ref=\"iconButton\"\n :title=\"shareTitle\"\n theme=\"secondary\"\n :size=\"size\"\n :icon"
},
{
"path": "src/renderer/components/FtSlider/FtSlider.css",
"chars": 3280,
"preview": ".pure-material-slider {\n display: inline-block;\n inline-size: 380px;\n color: rgb(var(--primary-text-color) 0.87);\n f"
},
{
"path": "src/renderer/components/FtSlider/FtSlider.vue",
"chars": 1430,
"preview": "<template>\n <label\n class=\"pure-material-slider\"\n :for=\"id\"\n >\n <input\n :id=\"id\"\n v-model.number=\"c"
},
{
"path": "src/renderer/components/FtSponsorBlockCategory/FtSponsorBlockCategory.css",
"chars": 237,
"preview": ".sponsorBlockCategory {\n margin-block-start: 30px;\n padding-block: 0;\n padding-inline: 10px;\n}\n\n.sponsorTitle {\n fon"
},
{
"path": "src/renderer/components/FtSponsorBlockCategory/FtSponsorBlockCategory.vue",
"chars": 4578,
"preview": "<template>\n <div class=\"sponsorBlockCategory\">\n <div\n :id=\"id\"\n class=\"sponsorTitle\"\n >\n {{ transl"
},
{
"path": "src/renderer/components/FtSubscribeButton/FtSubscribeButton.css",
"chars": 2769,
"preview": ".buttonList {\n margin: 5px;\n margin-block-end: 10px;\n border-radius: 4px;\n block-size: fit-content;\n box-shadow: 0 "
},
{
"path": "src/renderer/components/FtSubscribeButton/FtSubscribeButton.vue",
"chars": 8721,
"preview": "<template>\n <div\n ref=\"subscribeButton\"\n class=\"ftSubscribeButton\"\n @focusout=\"handleProfileDropdownFocusOut\"\n"
},
{
"path": "src/renderer/components/FtTimestampCatcher.vue",
"chars": 1606,
"preview": "<template>\n <!-- eslint-disable-next-line vuejs-accessibility/click-events-have-key-events -->\n <p\n v-safer-html=\"d"
},
{
"path": "src/renderer/components/FtToast/FtToast.css",
"chars": 795,
"preview": ".toast-holder {\n position: fixed;\n inset-inline-start: 50vw;\n transform: translate(calc(-50% * var(--horizontal-direc"
},
{
"path": "src/renderer/components/FtToast/FtToast.vue",
"chars": 3424,
"preview": "<template>\n <div class=\"toast-holder\">\n <div\n v-for=\"toast in toasts\"\n :key=\"toast.id\"\n class=\"toast\""
},
{
"path": "src/renderer/components/FtToggleSwitch/FtToggleSwitch.scss",
"chars": 1802,
"preview": "/* Thanks to Guus Lieben for the Material Design Switch */\n\n.switch-ctn {\n margin-block: 20px;\n margin-inline: 16px;\n "
},
{
"path": "src/renderer/components/FtToggleSwitch/FtToggleSwitch.vue",
"chars": 1594,
"preview": "<template>\n <div\n class=\"switch-ctn\"\n :class=\"{\n compact,\n disabled: disabled,\n containsTooltip: t"
},
{
"path": "src/renderer/components/FtTooltip/FtTooltip.css",
"chars": 2227,
"preview": ".button {\n background-color: transparent;\n border-style: none;\n color: var(--primary-text-color);\n cursor: pointer;\n"
},
{
"path": "src/renderer/components/FtTooltip/FtTooltip.vue",
"chars": 894,
"preview": "<template>\n <div class=\"tooltip\">\n <button\n :aria-labelledby=\"id\"\n class=\"button\"\n type=\"button\"\n "
},
{
"path": "src/renderer/components/GeneralSettings/GeneralSettings.css",
"chars": 115,
"preview": ".select {\n min-inline-size: 240px;\n inline-size: auto;\n}\n\n.switchGrid {\n gap: 10px;\n margin-block-end: 12px;\n}\n"
},
{
"path": "src/renderer/components/GeneralSettings/GeneralSettings.vue",
"chars": 17299,
"preview": "<template>\n <FtSettingsSection\n :title=\"t('Settings.General Settings.General Settings')\"\n >\n <div class=\"switchC"
},
{
"path": "src/renderer/components/ParentalControlSettings.vue",
"chars": 2308,
"preview": "<template>\n <FtSettingsSection\n :title=\"$t('Settings.Parental Control Settings.Parental Control Settings')\"\n >\n "
},
{
"path": "src/renderer/components/PasswordDialog/PasswordDialog.css",
"chars": 113,
"preview": ".card {\n inline-size: 85%;\n margin: auto;\n box-sizing: border-box;\n}\n\n.passwordInput {\n inline-size: 100%;\n}\n"
},
{
"path": "src/renderer/components/PasswordDialog/PasswordDialog.vue",
"chars": 956,
"preview": "<template>\n <FtCard\n class=\"card\"\n >\n <h3>{{ $t(\"Settings.Password Dialog.Enter Password To Unlock\") }}</h3>\n\n "
},
{
"path": "src/renderer/components/PasswordSettings/PasswordSettings.css",
"chars": 40,
"preview": ".centerButton {\n align-self: center;\n}\n"
},
{
"path": "src/renderer/components/PasswordSettings/PasswordSettings.vue",
"chars": 1704,
"preview": "<template>\n <FtSettingsSection\n :title=\"$t('Settings.Password Settings.Password Settings')\"\n >\n <FtFlexBox\n "
},
{
"path": "src/renderer/components/PlayerSettings/PlayerSettings.css",
"chars": 377,
"preview": ".screenshotFolderContainer {\n align-items: center;\n column-gap: 1rem;\n margin-block: 0;\n margin-inline: auto;\n inli"
},
{
"path": "src/renderer/components/PlayerSettings/PlayerSettings.vue",
"chars": 21075,
"preview": "<template>\n <FtSettingsSection\n :title=\"t('Settings.Player Settings.Player Settings')\"\n >\n <div class=\"switchCol"
},
{
"path": "src/renderer/components/PlaylistInfo/PlaylistInfo.scss",
"chars": 2603,
"preview": ".playListThumbnail {\n inline-size: 100%;\n}\n\n.playlistThumbnail img {\n inline-size: 100%;\n // Ensure placeholder image"
},
{
"path": "src/renderer/components/PlaylistInfo/PlaylistInfo.vue",
"chars": 28330,
"preview": "<template>\n <div\n class=\"playlistInfo\"\n :class=\"{ [theme]: true }\"\n >\n <div\n class=\"playlistThumbnail\"\n "
},
{
"path": "src/renderer/components/PrivacySettings.vue",
"chars": 8466,
"preview": "<template>\n <FtSettingsSection\n :title=\"$t('Settings.Privacy Settings.Privacy Settings')\"\n >\n <div class=\"switch"
},
{
"path": "src/renderer/components/ProxySettings/ProxySettings.css",
"chars": 255,
"preview": ".protocol-dropdown {\n margin-block-end: 1rem;\n}\n\n.proxy-warning {\n padding-block: 12px;\n padding-inline: 4%;\n backgr"
},
{
"path": "src/renderer/components/ProxySettings/ProxySettings.vue",
"chars": 8266,
"preview": "<template>\n <FtSettingsSection\n :title=\"$t('Settings.Proxy Settings.Proxy Settings')\"\n >\n <FtFlexBox class=\"sett"
},
{
"path": "src/renderer/components/SideNav/SideNav.css",
"chars": 4652,
"preview": ".sideNav {\n display: block;\n block-size: calc(100vh - 60px);\n inline-size: 200px;\n overflow-x: hidden;\n position: s"
},
{
"path": "src/renderer/components/SideNav/SideNav.vue",
"chars": 9393,
"preview": "<template>\n <FtFlexBox\n class=\"sideNav\"\n :class=\"[{closed: !isOpen}, applyHiddenLabels]\"\n role=\"navigation\"\n "
},
{
"path": "src/renderer/components/SideNavMoreOptions/SideNavMoreOptions.css",
"chars": 1766,
"preview": ".sideNavMoreOptions {\n display: none;\n}\n\n.navOption {\n position: relative;\n padding: 5px;\n cursor: pointer;\n color:"
},
{
"path": "src/renderer/components/SideNavMoreOptions/SideNavMoreOptions.vue",
"chars": 6613,
"preview": "<template>\n <div\n ref=\"menuRef\"\n class=\"sideNavMoreOptions\"\n >\n <div\n class=\"navOption moreOptionNav\"\n "
}
]
// ... and 246 more files (download for full content)
About this extraction
This page contains the full source code of the FreeTubeApp/FreeTube GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 446 files (4.9 MB), approximately 1.3M tokens, and a symbol index with 719 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.