├── src ├── modules │ ├── loadsettings.js │ ├── timeformat.js │ ├── sort.js │ ├── server.js │ ├── storage.js │ ├── files.js │ └── audio.js ├── assets │ ├── Inter.ttf │ ├── noise.jpeg │ ├── firetail.png │ ├── noise-colour.jpg │ ├── material │ │ ├── filled.woff2 │ │ ├── outlined.woff2 │ │ ├── rounded.woff2 │ │ └── material-icons.css │ ├── songs-banner-new.png │ ├── ft-icon │ │ ├── Firetail-Icon.ttf │ │ └── ft-icon.css │ ├── noise.svg │ ├── no_imagealt.svg │ ├── no_artist.svg │ ├── logo-mono.svg │ ├── no_image.svg │ ├── no_album.svg │ ├── no_genre.svg │ └── crash-tree.svg ├── static │ └── main │ │ ├── icon.png │ │ ├── macos.png │ │ ├── spconnect │ │ ├── logo-mono.svg │ │ ├── spotify.css │ │ └── spotifyconnect.html │ │ └── changelog.md ├── index.css ├── components │ ├── panel │ │ └── panel-components │ │ │ ├── TestModule.vue │ │ │ ├── ArrayButton.vue │ │ │ ├── ModuleError.vue │ │ │ ├── PromptModule.vue │ │ │ ├── MarkdownModule.vue │ │ │ └── Welcome.vue │ ├── sidebar │ │ ├── SideButtons.vue │ │ └── SidePlaylists.vue │ ├── IndSongList.vue │ ├── CommandPalette.vue │ ├── tour │ │ ├── PointingArrow.vue │ │ ├── TourSheet.vue │ │ ├── TourInterface.vue │ │ └── SidebarTour.vue │ ├── TopNav.vue │ ├── StandardButton.vue │ ├── playingbar │ │ ├── PlayingBar.vue │ │ └── queue │ │ │ ├── QueueItem.vue │ │ │ └── QueuePopup.vue │ ├── TopButtons.vue │ ├── BackgroundEffects.vue │ ├── NotificationPopup.vue │ ├── zen │ │ └── ZenMode.vue │ ├── ContextMenu.vue │ └── ItemAdd.vue ├── index.html ├── router │ ├── components │ │ ├── settings │ │ │ ├── options │ │ │ │ ├── TextOption.vue │ │ │ │ ├── SubtitleOption.vue │ │ │ │ ├── ButtonOption.vue │ │ │ │ ├── DropdownOption.vue │ │ │ │ └── SwitchOption.vue │ │ │ ├── sections │ │ │ │ ├── LibrarySection.vue │ │ │ │ ├── UpdatesSection.vue │ │ │ │ ├── IntegrationSection.vue │ │ │ │ ├── AccessibilitySection.vue │ │ │ │ ├── AdvancedSection.vue │ │ │ │ ├── GeneralSection.vue │ │ │ │ ├── AppearanceSection.vue │ │ │ │ └── AboutSection.vue │ │ │ └── SettingsView.vue │ │ ├── HiddenView.vue │ │ ├── SongLoadItem.vue │ │ ├── UnknownView.vue │ │ ├── ListItem.vue │ │ └── HomeView.vue │ └── index.js ├── themes │ ├── cyber.scss │ ├── classic.scss │ └── test.scss ├── store │ ├── index.js │ └── modules │ │ ├── playlist.js │ │ └── panel.js ├── translation │ ├── index.js │ └── locales │ │ ├── de.json │ │ ├── vi.json │ │ └── ar.json └── preload.js ├── .parlance.json ├── webpack.main.config.js ├── webpack.rules.js ├── webpack.renderer.config.js ├── .github └── workflows │ └── main.yml ├── .gitignore ├── package.json ├── FEATURE_CHECKLIST.md ├── forge.config.js └── README.md /src/modules/loadsettings.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/Inter.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kawuchuu/firetail/HEAD/src/assets/Inter.ttf -------------------------------------------------------------------------------- /src/assets/noise.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kawuchuu/firetail/HEAD/src/assets/noise.jpeg -------------------------------------------------------------------------------- /src/assets/firetail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kawuchuu/firetail/HEAD/src/assets/firetail.png -------------------------------------------------------------------------------- /src/static/main/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kawuchuu/firetail/HEAD/src/static/main/icon.png -------------------------------------------------------------------------------- /src/static/main/macos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kawuchuu/firetail/HEAD/src/static/main/macos.png -------------------------------------------------------------------------------- /src/assets/noise-colour.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kawuchuu/firetail/HEAD/src/assets/noise-colour.jpg -------------------------------------------------------------------------------- /src/assets/material/filled.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kawuchuu/firetail/HEAD/src/assets/material/filled.woff2 -------------------------------------------------------------------------------- /src/assets/songs-banner-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kawuchuu/firetail/HEAD/src/assets/songs-banner-new.png -------------------------------------------------------------------------------- /src/assets/material/outlined.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kawuchuu/firetail/HEAD/src/assets/material/outlined.woff2 -------------------------------------------------------------------------------- /src/assets/material/rounded.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kawuchuu/firetail/HEAD/src/assets/material/rounded.woff2 -------------------------------------------------------------------------------- /src/assets/ft-icon/Firetail-Icon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kawuchuu/firetail/HEAD/src/assets/ft-icon/Firetail-Icon.ttf -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, 3 | Arial, sans-serif; 4 | margin: auto; 5 | max-width: 38rem; 6 | padding: 2rem; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/panel/panel-components/TestModule.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /.parlance.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Firetail", 3 | "subprojects": [{ 4 | "name": "Firetail", 5 | "type": "vue-i18n", 6 | "path": "/src/translation/locales/{lang}.json", 7 | "baseLang": "en-US" 8 | }] 9 | } -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Firetail 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/modules/timeformat.js: -------------------------------------------------------------------------------- 1 | export default { 2 | timeFormat(s) { 3 | if (isNaN(s)) return '-:--' 4 | let min = Math.floor(s / 60); 5 | let sec = Math.floor(s - (min * 60)); 6 | if (sec < 10){ 7 | sec = `0${sec}`; 8 | } 9 | return `${min}:${sec}`; 10 | } 11 | } -------------------------------------------------------------------------------- /src/components/panel/panel-components/ArrayButton.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/router/components/settings/options/TextOption.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/noise.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/themes/cyber.scss: -------------------------------------------------------------------------------- 1 | html.dark { 2 | --back-bg: #1a1a1a; 3 | --bg: #292929 !important; 4 | --fg-bg: #3a3b3b !important; 5 | --gradient: linear-gradient(to right, #abf5f4, #6887db, #bf96e6); 6 | } 7 | 8 | .fill { 9 | background: var(--gradient) !important; 10 | } 11 | 12 | .handle { 13 | background-color: #bf96e6 !important; 14 | } -------------------------------------------------------------------------------- /src/components/panel/panel-components/ModuleError.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vuex from 'vuex' 2 | import Vue from 'vue' 3 | import audio from './modules/audio.js' 4 | import nav from './modules/nav.js' 5 | import panel from './modules/panel.js' 6 | import playlist from './modules/playlist.js' 7 | 8 | Vue.use(Vuex) 9 | 10 | const store = new Vuex.Store({ 11 | modules: { 12 | audio, 13 | nav, 14 | panel, 15 | playlist, 16 | } 17 | }) 18 | 19 | export default store -------------------------------------------------------------------------------- /src/store/modules/playlist.js: -------------------------------------------------------------------------------- 1 | const state = () => ({ 2 | playlists: [], 3 | currentPlaylist: { 4 | name: 'Unknown', 5 | desc: 'Unknown', 6 | id: 'Unknown', 7 | songIds: [] 8 | } 9 | }) 10 | 11 | const mutations = { 12 | setPlaylists(state, playlists) { 13 | state.playlists = playlists 14 | }, 15 | setCurrentPlaylist(state, playlist) { 16 | state.currentPlaylist = playlist 17 | } 18 | } 19 | 20 | export default { 21 | namespaced: true, 22 | state, 23 | mutations, 24 | } -------------------------------------------------------------------------------- /src/assets/ft-icon/ft-icon.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'ft-icon'; 3 | src: url('Firetail-Icon.ttf') format('truetype'); 4 | font-weight: normal; 5 | font-style: normal; 6 | font-display: block; 7 | } 8 | 9 | .ft-icon { 10 | font-family: 'ft-icon' !important; 11 | font-weight: normal; 12 | font-style: normal; 13 | font-size: 28px; 14 | line-height: 1; 15 | letter-spacing: normal; 16 | text-transform: none; 17 | display: inline-block; 18 | white-space: nowrap; 19 | word-wrap: normal; 20 | -webkit-font-feature-settings: 'liga'; 21 | -webkit-font-smoothing: antialiased; 22 | } -------------------------------------------------------------------------------- /src/router/components/settings/options/SubtitleOption.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/router/components/HiddenView.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /webpack.main.config.js: -------------------------------------------------------------------------------- 1 | const CopyWebpackPlugin = require('copy-webpack-plugin') 2 | const path = require('path') 3 | const assets = ['static/main'] 4 | const copyPlugins = new CopyWebpackPlugin({ 5 | patterns: assets.map((asset) => ({ 6 | from: path.resolve(__dirname, 'src', asset), 7 | to: path.resolve(__dirname, '.webpack/main', asset) 8 | })) 9 | }) 10 | module.exports = { 11 | /** 12 | * This is the main entry point for your application, it's the first file 13 | * that runs in the main process. 14 | */ 15 | entry: './src/main.js', 16 | // Put your normal webpack config below here 17 | module: { 18 | rules: require('./webpack.rules'), 19 | }, 20 | plugins: [ 21 | copyPlugins 22 | ] 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/sidebar/SideButtons.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /src/modules/sort.js: -------------------------------------------------------------------------------- 1 | export default { 2 | sortArray (array, sortBy) { 3 | function compare(a, b) { 4 | if (a[sortBy].toLowerCase() < b[sortBy].toLowerCase()) return -1; 5 | if (a[sortBy].toLowerCase() > b[sortBy].toLowerCase()) return 1; 6 | } 7 | return array.sort(compare); 8 | }, 9 | sortArrayNum(array, sortBy) { 10 | function compare(a, b) { 11 | if (a[sortBy] < b[sortBy]) return -1; 12 | if (a[sortBy] > b[sortBy]) return 1; 13 | } 14 | return array.sort(compare); 15 | }, 16 | simpleSort(array) { 17 | function compare(a, b) { 18 | if (a.toLowerCase() < b.toLowerCase()) return -1; 19 | if (a.toLowerCase() > b.toLowerCase()) return 1; 20 | } 21 | return array.sort(compare); 22 | }, 23 | shuffle(array) { 24 | return array.sort(() => Math.random() - 0.5); 25 | } 26 | } -------------------------------------------------------------------------------- /src/components/panel/panel-components/PromptModule.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 21 | -------------------------------------------------------------------------------- /src/components/IndSongList.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 40 | 41 | -------------------------------------------------------------------------------- /webpack.rules.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | // Add support for native node modules 3 | { 4 | // We're specifying native_modules in the test because the asset relocator loader generates a 5 | // "fake" .node file which is really a cjs file. 6 | test: /native_modules[/\\].+\.node$/, 7 | use: 'node-loader', 8 | }, 9 | { 10 | test: /[/\\]node_modules[/\\].+\.(m?js|node)$/, 11 | parser: { amd: false }, 12 | use: { 13 | loader: '@vercel/webpack-asset-relocator-loader', 14 | options: { 15 | outputAssetBase: 'native_modules', 16 | }, 17 | }, 18 | }, 19 | // Put your webpack loader rules in this array. This is where you would put 20 | // your ts-loader configuration for instance: 21 | /** 22 | * Typescript Example: 23 | * 24 | * { 25 | * test: /\.tsx?$/, 26 | * exclude: /(node_modules|.webpack)/, 27 | * loaders: [{ 28 | * loader: 'ts-loader', 29 | * options: { 30 | * transpileOnly: true 31 | * } 32 | * }] 33 | * } 34 | */ 35 | ]; 36 | -------------------------------------------------------------------------------- /src/router/components/settings/sections/LibrarySection.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 26 | 27 | -------------------------------------------------------------------------------- /src/components/CommandPalette.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /src/translation/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | //import {ipcRenderer} from 'electron' 4 | 5 | Vue.use(VueI18n) 6 | 7 | let loadLocaleMessages = () => { 8 | const locales = require.context('./locales', true, /[A-Za-z0-9-_,\s]+\.json$/i) 9 | const messages = {} 10 | locales.keys().forEach(key => { 11 | const matched = key.match(/([A-Za-z0-9-_]+)\./i) 12 | if (matched && matched.length > 1) { 13 | const locale = matched[1] 14 | messages[locale] = locales(key) 15 | } 16 | }) 17 | return messages 18 | } 19 | 20 | /* let getLocale = async () => { 21 | let locale = await window.ipcRenderer.invoke('hasCustomLanguage') 22 | console.log(locale) 23 | if (locale) { 24 | i18n.locale = locale 25 | } else { 26 | i18n.locale = navigator.language.split('-')[0] 27 | } 28 | } 29 | getLocale() */ 30 | 31 | let i18n = new VueI18n({ 32 | locale: navigator.language, 33 | fallbackLocale: 'en-US', 34 | messages: loadLocaleMessages(), 35 | silentTranslationWarn: true, 36 | silentFallbackWarn: true, 37 | fallbackRoot: true, 38 | fallbackRootWithEmptyString: true 39 | }) 40 | 41 | export default i18n -------------------------------------------------------------------------------- /src/router/components/settings/sections/UpdatesSection.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 31 | 32 | -------------------------------------------------------------------------------- /src/router/components/SongLoadItem.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /webpack.renderer.config.js: -------------------------------------------------------------------------------- 1 | const rules = require('./webpack.rules'); 2 | const { VueLoaderPlugin } = require('vue-loader') 3 | const CopyWebpackPlugin = require('copy-webpack-plugin') 4 | const path = require('path') 5 | 6 | const assets = ['static'] 7 | const copyPlugins = new CopyWebpackPlugin({ 8 | patterns: assets.map((asset) => ({ 9 | from: path.resolve(__dirname, 'src', asset), 10 | to: path.resolve(__dirname, '.webpack/renderer', asset) 11 | })) 12 | }) 13 | 14 | module.exports = { 15 | // Put your normal webpack config below here 16 | mode: 'development', 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.vue$/, 21 | loader: 'vue-loader' 22 | }, 23 | { 24 | test: /\.css$/, 25 | use: [ 26 | 'vue-style-loader', 27 | 'css-loader' 28 | ], 29 | }, 30 | { 31 | test: /\.s[ac]ss$/i, 32 | use: [ 33 | "style-loader", 34 | "css-loader", 35 | "sass-loader", 36 | ], 37 | }, 38 | { 39 | test: /\.(png|jpg|gif|svg)$/i, 40 | type: 'asset/resource' 41 | } 42 | ] 43 | }, 44 | plugins: [ 45 | new VueLoaderPlugin(), 46 | //copyPlugins 47 | ], 48 | resolve: { 49 | alias: { 50 | 'vue$': 'vue/dist/vue.esm.js' 51 | } 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /src/components/panel/panel-components/MarkdownModule.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 26 | 27 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Continuous 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | defaults: 9 | run: 10 | shell: bash 11 | 12 | jobs: 13 | build: 14 | runs-on: ${{ matrix.os }} 15 | 16 | defaults: 17 | run: 18 | shell: bash 19 | 20 | strategy: 21 | matrix: 22 | os: [ macos-latest, ubuntu-latest, windows-latest ] 23 | 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4.2.2 27 | - name: Get Version 28 | id: package-version 29 | run: | 30 | PACKAGE_JSON_PATH="${1-.}" 31 | PACKAGE_VERSION=$(cat ${PACKAGE_JSON_PATH}/package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[",]//g' | tr -d '[[:space:]]') 32 | echo ::set-output name=current-version::$PACKAGE_VERSION 33 | - name: Get Build Number 34 | id: get-ver-build 35 | run: | 36 | build=$(echo ${{ github.sha }} | cut -c1-7) 37 | echo "::set-output name=build::$build" 38 | - uses: actions/setup-node@v4.1.0 39 | name: Setup Node 40 | with: 41 | node-version: '20' 42 | - name: Install dependencies 43 | run: | 44 | yarn install 45 | - name: Build and publish artifacts 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | run: | 49 | yarn run publish -------------------------------------------------------------------------------- /src/assets/no_imagealt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/no_artist.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/components/tour/PointingArrow.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 34 | 35 | -------------------------------------------------------------------------------- /src/components/TopNav.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 27 | 28 | -------------------------------------------------------------------------------- /src/components/panel/panel-components/Welcome.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 31 | 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | .DS_Store 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | dist_electron/ 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # TypeScript cache 44 | *.tsbuildinfo 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | .env.test 64 | 65 | # parcel-bundler cache (https://parceljs.org/) 66 | .cache 67 | 68 | # next.js build output 69 | .next 70 | 71 | # nuxt.js build output 72 | .nuxt 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless/ 79 | 80 | # FuseBox cache 81 | .fusebox/ 82 | 83 | # DynamoDB Local files 84 | .dynamodb/ 85 | 86 | # Webpack 87 | .webpack/ 88 | 89 | # Vite 90 | .vite/ 91 | 92 | # Electron-Forge 93 | out/ 94 | 95 | .idea/ 96 | dist/ 97 | 98 | build.txt -------------------------------------------------------------------------------- /src/assets/logo-mono.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/components/tour/TourSheet.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 30 | 31 | -------------------------------------------------------------------------------- /src/router/components/settings/options/ButtonOption.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | 22 | -------------------------------------------------------------------------------- /src/assets/no_image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/StandardButton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | 22 | 81 | -------------------------------------------------------------------------------- /src/router/components/settings/sections/IntegrationSection.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 34 | 35 | -------------------------------------------------------------------------------- /src/router/components/settings/sections/AccessibilitySection.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 39 | 40 | -------------------------------------------------------------------------------- /src/assets/material/material-icons.css: -------------------------------------------------------------------------------- 1 | /* fallback */ 2 | @font-face { 3 | font-family: 'Material Icons'; 4 | font-style: normal; 5 | font-weight: 400; 6 | src: url(filled.woff2) format('woff2'); 7 | } 8 | 9 | @font-face { 10 | font-family: 'Material Icons Rounded'; 11 | font-style: normal; 12 | font-weight: 400; 13 | src: url(rounded.woff2) format('woff2'); 14 | } 15 | 16 | @font-face { 17 | font-family: 'Material Icons Outlined'; 18 | font-style: normal; 19 | font-weight: 400; 20 | src: url(outlined.woff2) format('woff2'); 21 | } 22 | 23 | .material-icons { 24 | font-family: 'Material Icons'; 25 | font-weight: normal; 26 | font-style: normal; 27 | font-size: 24px; 28 | line-height: 1; 29 | letter-spacing: normal; 30 | text-transform: none; 31 | display: inline-block; 32 | white-space: nowrap; 33 | word-wrap: normal; 34 | direction: ltr; 35 | -webkit-font-feature-settings: 'liga'; 36 | -webkit-font-smoothing: antialiased; 37 | } 38 | 39 | .material-icons-rounded { 40 | font-family: 'Material Icons Rounded'; 41 | font-weight: normal; 42 | font-style: normal; 43 | font-size: 24px; 44 | line-height: 1; 45 | letter-spacing: normal; 46 | text-transform: none; 47 | display: inline-block; 48 | white-space: nowrap; 49 | word-wrap: normal; 50 | direction: ltr; 51 | -webkit-font-feature-settings: 'liga'; 52 | -webkit-font-smoothing: antialiased; 53 | } 54 | 55 | .material-icons-outlined { 56 | font-family: 'Material Icons Outlined'; 57 | font-weight: normal; 58 | font-style: normal; 59 | font-size: 24px; 60 | line-height: 1; 61 | letter-spacing: normal; 62 | text-transform: none; 63 | display: inline-block; 64 | white-space: nowrap; 65 | word-wrap: normal; 66 | direction: ltr; 67 | -webkit-font-feature-settings: 'liga'; 68 | -webkit-font-smoothing: antialiased; 69 | } -------------------------------------------------------------------------------- /src/static/main/spconnect/logo-mono.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 26 | 27 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/components/tour/TourInterface.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 28 | 29 | 58 | 59 | -------------------------------------------------------------------------------- /src/components/playingbar/PlayingBar.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 46 | 47 | -------------------------------------------------------------------------------- /src/assets/no_album.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/static/main/spconnect/spotify.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: var(--bg); 3 | color: white; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 5 | margin: 0; 6 | 7 | --sub-text: #a49cc0; 8 | --bg: #18171c; 9 | --fg-bg: #2b2838; 10 | } 11 | 12 | .content { 13 | width: 100%; 14 | height: 100vh; 15 | display: none; 16 | align-items: center; 17 | justify-content: center; 18 | flex-direction: column; 19 | text-align: center; 20 | } 21 | 22 | .content.active { 23 | display: flex; 24 | } 25 | 26 | .title { 27 | display: flex; 28 | align-items: center; 29 | margin-bottom: 12px; 30 | } 31 | 32 | .content h1 { 33 | font-size: 2.9em; 34 | letter-spacing: -0.02em; 35 | margin: 0px; 36 | } 37 | 38 | .content p { 39 | max-width: 700px; 40 | font-size: 0.95em; 41 | line-height: 1.6em; 42 | color: var(--sub-text); 43 | } 44 | 45 | @keyframes spin { 46 | 0% { transform: rotate(0deg); } 47 | 100% { transform: rotate(360deg); } 48 | } 49 | 50 | .load-spinner { 51 | border: 4px solid white; 52 | border-left: 4px solid transparent; 53 | border-radius: 50%; 54 | width: 30px; 55 | height: 30px; 56 | animation: spin 1s linear infinite; 57 | margin-right: 20px; 58 | } 59 | 60 | .bottom-logo { 61 | position: absolute; 62 | bottom: 0; 63 | margin-bottom: 30px; 64 | width: 100%; 65 | 66 | display: flex; 67 | align-items: center; 68 | justify-content: center; 69 | } 70 | 71 | .bottom-logo img { 72 | height: 40px; 73 | width: auto; 74 | filter: brightness(10); 75 | } 76 | 77 | .bottom-logo span { 78 | font-size: 1.3em; 79 | font-weight: bold; 80 | margin-left: 5px; 81 | } 82 | 83 | .error-code { 84 | max-width: 800px; 85 | padding: 18px 20px; 86 | background: var(--fg-bg); 87 | font-family: monospace; 88 | text-align: left; 89 | margin-top: 5px; 90 | border-radius: 10px; 91 | } -------------------------------------------------------------------------------- /src/router/components/settings/sections/AdvancedSection.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 44 | 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firetail", 3 | "productName": "Firetail", 4 | "version": "1.0.0-beta", 5 | "description": "Audio Player", 6 | "main": ".webpack/main", 7 | "scripts": { 8 | "start": "electron-forge start -- --enable-logging", 9 | "start-rtl": "electron-forge start -- --force-ui-direction=rtl", 10 | "start-wayland": "electron-forge start -- --enable-features=UseOzonePlatform --ozone-platform=wayland", 11 | "package": "electron-forge package", 12 | "make": "electron-forge make", 13 | "publish": "electron-forge publish", 14 | "lint": "echo \"No linting configured\"" 15 | }, 16 | "keywords": [], 17 | "author": { 18 | "name": "kawuchuu", 19 | "email": "kawuchuu@gmail.com" 20 | }, 21 | "license": "GPL-3.0", 22 | "devDependencies": { 23 | "@electron-forge/cli": "^6.4.2", 24 | "@electron-forge/maker-deb": "^6.4.2", 25 | "@electron-forge/maker-dmg": "^6.4.2", 26 | "@electron-forge/maker-rpm": "^6.4.2", 27 | "@electron-forge/maker-squirrel": "^6.4.2", 28 | "@electron-forge/maker-zip": "^7.6.0", 29 | "@electron-forge/plugin-auto-unpack-natives": "^6.4.2", 30 | "@electron-forge/plugin-webpack": "^6.4.2", 31 | "@electron-forge/publisher-github": "^7.6.0", 32 | "@vercel/webpack-asset-relocator-loader": "1.7.3", 33 | "css-loader": "^6.0.0", 34 | "electron": "^32.3.3", 35 | "electron-devtools-installer": "^3.2.0", 36 | "node-loader": "^2.0.0", 37 | "sass": "^1.81.0", 38 | "sass-loader": "^16.0.3", 39 | "style-loader": "^3.0.0", 40 | "vue-loader": "^15.10.1" 41 | }, 42 | "dependencies": { 43 | "@jimp/plugin-cover": "^1.6.0", 44 | "axios": "1.8.2", 45 | "better-sqlite3": "11.5.0", 46 | "colorthief": "^2.6.0", 47 | "copy-webpack-plugin": "^11.0.0", 48 | "cors": "^2.8.5", 49 | "electron-squirrel-startup": "^1.0.0", 50 | "jimp": "^1.6.0", 51 | "marked": "^15.0.2", 52 | "mime-types": "^2.1.35", 53 | "music-metadata": "^10.6.0", 54 | "node-vibrant": "^3.1.6", 55 | "vue": "2.7.0", 56 | "vue-async-computed": "3.9.0", 57 | "vue-i18n": "8.28.2", 58 | "vue-router": "3.6.5", 59 | "vue-virtual-scroll-list": "https://github.com/kawuchuu/vue-virtual-scroll-list", 60 | "vuex": "3.6.2" 61 | }, 62 | "packageManager": "yarn@1.22.21+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72" 63 | } 64 | -------------------------------------------------------------------------------- /src/assets/no_genre.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/preload.js: -------------------------------------------------------------------------------- 1 | // See the Electron documentation for details on how to use preload scripts: 2 | // https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts 3 | import {contextBridge, ipcRenderer, webUtils} from 'electron' 4 | import {marked} from "marked"; 5 | // i'm aware you shouldn't do this. fixed in firetail 2.0 and won't fix here. 6 | contextBridge.exposeInMainWorld('ipcRenderer', { 7 | invoke: async (channel, data) => { 8 | return await ipcRenderer.invoke(channel, data) 9 | }, 10 | send: (channel, data) => { 11 | ipcRenderer.send(channel, data) 12 | }, 13 | receive: (channel, func) => ipcRenderer.on( 14 | channel, 15 | (event, ...args) => { 16 | return func(event, args[0]) 17 | } 18 | ) 19 | }) 20 | 21 | contextBridge.exposeInMainWorld('process', { 22 | platform: process.platform, 23 | arch: process.arch, 24 | versions: { 25 | electron: process.versions.electron, 26 | node: process.versions.node, 27 | chrome: process.versions.chrome 28 | } 29 | }) 30 | 31 | contextBridge.exposeInMainWorld('os', { 32 | version: async () => await ipcRenderer.invoke('os-version') 33 | }) 34 | 35 | contextBridge.exposeInMainWorld('marked', { 36 | parse: async path => { 37 | return await ipcRenderer.invoke('parse-markdown', path) 38 | } 39 | }) 40 | 41 | contextBridge.exposeInMainWorld('ftStore', { 42 | getItem: key => ipcRenderer.invoke('getKey', key), 43 | setKey: (key, value, category) => ipcRenderer.send('setKey', [key, value, category]), 44 | deleteKey: key => ipcRenderer.send('deleteKey', key), 45 | keyExists: key => ipcRenderer.invoke('keyExists', key), 46 | keys: ipcRenderer.invoke('keys'), 47 | getCategory: category => ipcRenderer.invoke('getCategory', category) 48 | }) 49 | 50 | contextBridge.exposeInMainWorld('ftStoreSync', { 51 | getItem: key => ipcRenderer.sendSync('getKeySync', key), 52 | keyExists: key => ipcRenderer.sendSync('keyExistsSync', key), 53 | getCategory: category => ipcRenderer.sendSync('getCategorySync', category), 54 | }) 55 | 56 | contextBridge.exposeInMainWorld('contextmenu', { 57 | popup: options => ipcRenderer.send('createPopup', options) 58 | }) 59 | 60 | contextBridge.exposeInMainWorld('webUtils', { 61 | getPathForFile: file => webUtils.getPathForFile(file), 62 | }) -------------------------------------------------------------------------------- /src/modules/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { constants, mkdirSync, access } from 'fs' 3 | import cors from 'cors' 4 | import { resolve } from 'path' 5 | //import isPortReachable from 'is-port-reachable' 6 | import { dialog } from 'electron' 7 | 8 | const app = express() 9 | 10 | const startServer = async (appLoc, win) => { 11 | access(`${appLoc}/images/`, constants.F_OK | constants.W_OK, (err) => { 12 | if (err) mkdirSync(`${appLoc}/images/`) 13 | }) 14 | app.use(cors()) 15 | let staticRoot = resolve(__dirname, 'static/main/') 16 | console.log(staticRoot) 17 | app.use('/', express.static(staticRoot)) 18 | app.use('/images', express.static(`${appLoc}/images/`)) 19 | app.use(function(req, res, next) { 20 | res.header("Access-Control-Allow-Origin", "*"); 21 | res.header("Access-Control-Allow-Headers", "X-Requested-With"); 22 | next(); 23 | }); 24 | for (let testPort = 56742; testPort < 56752; testPort++) { 25 | try { 26 | await new Promise((res, rej) => { 27 | app.listen(testPort, 'localhost', () => { 28 | // dialog.showErrorBox("port", testPort); 29 | app.set('port', testPort) 30 | console.log('Server now running...') 31 | res(); 32 | }).on('error', err => { 33 | rej(err); 34 | }); 35 | }); 36 | break; 37 | } catch { 38 | if (testPort >= 56751) { 39 | const msg = "ERROR: Unable to establish a server port. Please close any applications using the following range: 56741-56751." 40 | dialog.showErrorBox("Failed to establish a server port", msg) 41 | } 42 | } 43 | } 44 | 45 | app.get('/', (req, res) => { 46 | res.send('stop peeking!') 47 | }) 48 | app.get('/spconnect', (req, res) => { 49 | if (req.query.error) { 50 | res.sendFile(staticRoot + '/spconnect/spotifyconnect.html') 51 | win.webContents.send('spotifyAuthFinish', {error: req.query.error}) 52 | return; 53 | } 54 | if (req.query.code && req.query.state) { 55 | const state = req.query.state 56 | const code = req.query.code 57 | win.webContents.send('spotifyAuthFinish', {state: state, code: code}) 58 | } 59 | res.sendFile(staticRoot + '/spconnect/spotifyconnect.html') 60 | }) 61 | } 62 | 63 | export default { 64 | startServer, 65 | app 66 | } -------------------------------------------------------------------------------- /src/router/components/settings/SettingsView.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 45 | 46 | -------------------------------------------------------------------------------- /FEATURE_CHECKLIST.md: -------------------------------------------------------------------------------- 1 | # Firetail Feature Checklist 2 | *Created: 02/01/2023, Updated: 12/09/2024* 3 | 4 | --- 5 | 6 | ## Features required for completion before releasing v1.0.0 7 | - Library 8 | - ~~Improve artist detection (use 'artists' and 'albumartist' tags)~~ ✅ 9 | - ~~Recursive directory importing~~ ✅ 10 | - Playlists 11 | - ~~Manually change song position~~ ✅ 12 | - Change playlist position on sidebar 13 | - Remove images from playlists 14 | - Generally fix jankiness 15 | - ~~Finally start making the favourites page~~ ✅ 16 | - Search 17 | - ~~General search on the top bar (search for everything)~~ ✅ 18 | - Search in current viewing list 19 | - Accessibility 20 | - Finish high contrast mode 21 | - Add specific colourblind colour mode? (?) 22 | - Screen reader support 23 | - ~~Queue~~ ✅ 24 | - ~~Queue management popup~~ ✅ 25 | - ~~Only view current queue~~ ✅ 26 | - Bug fixes & improvements 27 | - Random scrolling between pages 28 | - ~~Try to make the sticky bar on song list pages work better~~ ✅ 29 | - Make UI as smooth and quality as possible 30 | - ~~Fix shuffling~~ ✅ 31 | - ~~Clean up A LOT of code.~~ ❓ I'll do a little bit of cleaning up, but this is now a focus for Firetail 2.0. 32 | - ~~Explicit tag~~ ✅ 33 | - ~~RTL UI~~ (mostly done needs a little more tweaking though) 34 | - Updates 35 | - Let user check for updates 36 | - ~~Migrate to Electron Forge~~ ✅ 37 | - Settings 38 | - ~~Improve way settings view is rendered~~ 39 | - Add new setting storage solution 40 | - Implement new setting application solution 41 | 42 | 43 | ## Features that can (probably) wait for later versions 44 | - Appearance 45 | - App theming: both built-in and custom 46 | - Queue 47 | - Edit current queue 48 | - Add song to queue in context menu 49 | - Library 50 | - Genre metadata 51 | - Spotify integration 52 | - ~~Authorisation~~ ❓ 53 | - Artist images 54 | - Pull metadata for songs missing it 55 | - Add local song to playlist 56 | - Last.fm integration 57 | - Authorisation 58 | - Add scrobbling support 59 | - Maybe some metadata support? (spotify does this already though) 60 | - Effects 61 | - Tempo, pitch, reverb effects 62 | - Low and high pass filters? 63 | - Equalisation 64 | - Playlists 65 | - Import m3u playlists? 66 | - Plugins 67 | - Firetail 2.0 now uses Vite, so I'll experiment with this. 68 | - API for stuff like sidebar, screens, settings etc. 69 | - Updates 70 | - Choose update branch 71 | - Accessibility 72 | - Better keyboard-only navigation 73 | - Home 74 | - Make dynamic sections, not hard-coded 75 | - Come up with new ideas as I build it -------------------------------------------------------------------------------- /forge.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | packagerConfig: { 3 | asar: true, 4 | icon: './build/other/icon', 5 | executableName: process.platform === 'linux' ? 'firetail' : 'Firetail' 6 | }, 7 | rebuildConfig: {}, 8 | makers: [ 9 | { 10 | name: '@electron-forge/maker-squirrel', 11 | config: { 12 | setupIcon: './build/other/icon.ico' 13 | }, 14 | }, 15 | /*{ 16 | name: '@electron-forge/maker-dmg', 17 | config: { 18 | title: "Firetail", 19 | background: './build/background.tiff', 20 | icon: './build/other/icon.icns', 21 | contents: (opts) => { return [ 22 | { "x": 448, "y": 344, "type": "link", "path": "/Applications" }, 23 | { "x": 192, "y": 344, "type": "file", "path": opts.appPath } 24 | ]} 25 | } 26 | },*/ 27 | { 28 | name: '@electron-forge/maker-deb', 29 | config: { 30 | options: { 31 | name: 'Firetail', 32 | description: 'Music player', 33 | maintainer: 'kawuchuu', 34 | homepage: 'https://kawuchuu.dev', 35 | categories: ['Audio'], 36 | 37 | } 38 | }, 39 | }, 40 | { 41 | name: '@electron-forge/maker-rpm', 42 | config: {}, 43 | }, 44 | { 45 | name: '@electron-forge/maker-zip' 46 | } 47 | ], 48 | publishers: [ 49 | { 50 | name: '@electron-forge/publisher-github', 51 | config: { 52 | repository: { 53 | owner: 'kawuchuu', 54 | name: 'firetail' 55 | }, 56 | prerelease: true, 57 | force: true, 58 | generateRelease: true, 59 | draft: true 60 | } 61 | } 62 | ], 63 | plugins: [ 64 | { 65 | name: '@electron-forge/plugin-auto-unpack-natives', 66 | config: {}, 67 | }, 68 | { 69 | name: '@electron-forge/plugin-webpack', 70 | config: { 71 | mainConfig: './webpack.main.config.js', 72 | devContentSecurityPolicy: 'default-src \'self\' \'unsafe-inline\' data: accounts.spotify.com api.spotify.com; script-src \'self\' \'unsafe-eval\' \'unsafe-inline\' data:; media-src \'self\' localhost local-resource:; img-src * \'self\' blob: data:;', 73 | devServer: { 74 | hot: true, 75 | liveReload: false 76 | }, 77 | renderer: { 78 | config: './webpack.renderer.config.js', 79 | entryPoints: [ 80 | { 81 | html: './src/index.html', 82 | js: './src/renderer.js', 83 | name: 'main_window', 84 | preload: { 85 | js: './src/preload.js', 86 | }, 87 | }, 88 | ], 89 | }, 90 | }, 91 | }, 92 | ], 93 | }; 94 | -------------------------------------------------------------------------------- /src/modules/storage.js: -------------------------------------------------------------------------------- 1 | import {app} from 'electron' 2 | import {resolve} from 'path' 3 | import {readFileSync, writeFileSync, writeFile, accessSync, constants} from 'fs' 4 | 5 | class FiretailStorage { 6 | configFilePath = resolve(app.getPath('userData'), 'config.json') 7 | config = null 8 | categories = null 9 | 10 | constructor() { 11 | try { 12 | this.initConfig(); 13 | } catch(err) { 14 | writeFileSync(this.configFilePath, '{"categories": {}}'); 15 | this.initConfig(); 16 | } 17 | } 18 | 19 | initConfig() { 20 | try { 21 | accessSync(this.configFilePath, constants.F_OK) 22 | const data = readFileSync(this.configFilePath, {}); 23 | this.config = JSON.parse(data) 24 | this.categories = this.config['categories'] 25 | } catch(err) { 26 | throw err; 27 | } 28 | } 29 | 30 | getItem(key) { 31 | if (this.config[key] === undefined) throw `Key "${key}" does not exist`; 32 | return this.config[key] 33 | } 34 | 35 | setKey(key, value, category) { 36 | if (!key || value === undefined) throw `No key or value was provided` 37 | if (key === 'categories') throw 'The "categories" key is reserved and cannot be set' 38 | this.config[key] = value 39 | if (category) { 40 | if (!this.categories[category]) { 41 | this.categories[category] = [] 42 | } 43 | this.searchKeyInCategoryAndRemove(key) 44 | this.categories[category].push(key) 45 | this.config['categories'] = this.categories 46 | } 47 | this.writeConfig() 48 | } 49 | 50 | getCategory(category) { 51 | if (!this.categories[category]) throw `Category ${category} does not exist` 52 | return this.categories[category] 53 | } 54 | 55 | keyExists(key) { 56 | if (!key) throw `No key was provided` 57 | return !!this.config[key] 58 | } 59 | 60 | writeConfig() { 61 | const mainConfig = this.config 62 | mainConfig.categories = this.categories 63 | writeFile(this.configFilePath, JSON.stringify(mainConfig), (err) => { 64 | if (err) throw err; 65 | }) 66 | } 67 | 68 | deleteKey(key) { 69 | if (!key) throw `No key was provided` 70 | delete this.config[key] 71 | this.searchKeyInCategoryAndRemove(key) 72 | } 73 | 74 | searchKeyInCategoryAndRemove(key) { 75 | Object.keys(this.categories).forEach(item => { 76 | const index = this.categories[item].indexOf(key) 77 | console.log(index) 78 | if (index !== -1) this.categories[item].splice(index, 1) 79 | console.log(this.categories[item]) 80 | }) 81 | } 82 | 83 | get keys() { 84 | return Object.keys(this.config) 85 | } 86 | 87 | get fullConfig() { 88 | return this.config 89 | } 90 | } 91 | 92 | export default FiretailStorage -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

A fun and beautiful desktop music player 🎵

4 |
5 | 6 | --- 7 | 8 | ## Releases 9 | 10 | [Click here](https://github.com/kawuchuu/firetail/releases/latest) to download the latest stable release for your system. 11 | 12 | If you'd like to try the latest unstable 'continuous' release, check them out [here.](https://github.com/kawuchuu/firetail/releases/continuous) (Please note these builds are incomplete and may not function as expected) 13 | 14 | ## Platform Information 15 | ### Windows 16 | - Requires Windows 10 and up, and a compatible x64 processor 17 | - You'll need to download the '.exe' file to install Firetail 18 | - SmartScreen may warn you that Firetail is unsafe. This is due to the app being unsigned, as well as having low traffic. To continue launching the installer, click 'more info', then 'run anyway'. If you don't trust the provided installer, you are always welcome to build Firetail yourself; build instructions are below. 19 | 20 | ### macOS 21 | - Requires macOS 10.15 and up, only supports Apple Silicon Macs 22 | - You'll need to download the '.dmg' file to install Firetail 23 | - Due to limitations put in place by Apple, macOS will likely report unsigned apps downloaded from the internet as "damaged" or "cannot be opened". 24 | If it says it is "damaged", type the following terminal command (assuming you placed the app in the default Applications folder): `xattr -c /Applications/Firetail.app` 25 | Unfortunately there appears to be no way around this without code signing, which requires a paid Apple Developer account and will reveal some personal details about myself, which I'm unwilling to share. 26 | 27 | ### Linux 28 | - Requires a distribution and desktop environment compatible with up-to-date versions of Chromium. Pre-built binaries only support x64 processors, however Firetail can be built for arm64 processors. 29 | - Users of distros supporting APT (Debian, Ubuntu, Mint) should download the '.deb' file, users of distros supporting RPM (Red Hat, Fedora, openSUSE) should download the '.rpm' file, and users of other distros should download the '.AppImage' file. 30 | - Other methods of installation may be supported in the future, such as Flatpak and the AUR. 31 | - For now, Firetail works best on X11; some features do not work correctly on Wayland. 32 | 33 | ## Build for your platform 34 | 35 | ### Prerequisites 36 | - Python 3.x is required for all platforms to install dependencies. 37 | 38 | - To install dependencies and build on Windows, you'll need to install the 'Desktop development with C++' component with Visual Studio Build Tools or Visual Studio Community via Visual Studio Installer. 39 | 40 | ### Building 41 | 42 | Clone repository: 43 | ``` 44 | git clone https://github.com/kawuchuu/firetail.git . 45 | ``` 46 | Install dependencies (`yarn` is preferred over `npm`): 47 | ``` 48 | yarn install 49 | ``` 50 | **(Optional)** If you want to run in developer mode *without* building: 51 | ``` 52 | yarn start 53 | ``` 54 | Build artifacts: 55 | ``` 56 | yarn make 57 | ``` -------------------------------------------------------------------------------- /src/router/components/UnknownView.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 54 | 55 | -------------------------------------------------------------------------------- /src/components/TopButtons.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 60 | 61 | -------------------------------------------------------------------------------- /src/router/components/settings/options/DropdownOption.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 56 | 57 | -------------------------------------------------------------------------------- /src/themes/classic.scss: -------------------------------------------------------------------------------- 1 | html.dark { 2 | --text: #dfdfdf !important; 3 | --back-bg: var(--fg-bg) !important; 4 | --li-hv: #212121; 5 | --hl-txt: #eb6e64 !important; 6 | --hl-op: #eb6e641a !important; 7 | --gradient1: #e74e8e; 8 | --gradient2: #ef9135; 9 | } 10 | 11 | .side-bar { 12 | height: calc(100vh - 86px - 50px); 13 | position: relative; 14 | top: 50px; 15 | background: var(--fg-bg) !important; 16 | border-right: solid 2px var(--bd); 17 | box-shadow: 1px 0 5px rgba(0, 0, 0, .15); 18 | } 19 | 20 | .nav-buttons { 21 | .router-link-active .item-sidebar { 22 | background: none; 23 | color: var(--hl-txt); 24 | } 25 | .item-sidebar, .list-subtitle, .special-button { 26 | opacity: 1; 27 | } 28 | .list-subtitle { 29 | border-bottom: solid 2px var(--bd); 30 | } 31 | .router-link-active .item-sidebar::before { 32 | content: ""; 33 | width: 3px; 34 | height: 30px; 35 | background: var(--hl-txt); 36 | border-radius: 10px; 37 | position: absolute; 38 | } 39 | } 40 | 41 | .top-bar { 42 | width: 100vw !important; 43 | position: fixed !important; 44 | left: 0 !important; 45 | z-index: 11; 46 | background-color: var(--fg-bg) !important; 47 | border-bottom: solid 2px var(--bd); 48 | box-shadow: 0 1px 5px rgba(0, 0, 0, .15); 49 | } 50 | 51 | .windows-custom-buttons { 52 | background-color: var(--fg-bg) !important; 53 | } 54 | 55 | .container { 56 | top: 50px; 57 | border-radius: 0px; 58 | } 59 | 60 | .colour-bg { 61 | display: none; 62 | } 63 | 64 | .playing-bar { 65 | border-top: solid 2px var(--bd) !important; 66 | box-shadow: 0 -1px 5px rgba(0, 0, 0, .15); 67 | } 68 | 69 | .list-section { 70 | margin: 0px !important; 71 | } 72 | 73 | .list-section.sticky { 74 | border-bottom: solid 2px var(--bd) !important; 75 | padding: 0px !important; 76 | } 77 | 78 | .root-wrapper .wrapper { 79 | padding: 0px !important; 80 | } 81 | 82 | .results-link { 83 | border-bottom: solid 1px var(--bd); 84 | //height: 35px !important; 85 | border-radius: 0px !important; 86 | } 87 | 88 | .results-link:hover { 89 | background-color: var(--li-hv) !important; 90 | } 91 | 92 | .seek-bar { 93 | background: var(--bd) !important; 94 | } 95 | 96 | .fill { 97 | background: linear-gradient(to right, var(--gradient1), var(--gradient2)) !important; 98 | } 99 | 100 | .handle { 101 | background-color: var(--gradient2) !important; 102 | } 103 | 104 | .panel { 105 | border: solid 2px var(--bd); 106 | border-radius: 20px !important; 107 | 108 | input, textarea, .label-wrapper { 109 | background: var(--fg-bg) !important; 110 | } 111 | } 112 | 113 | .sticky-bg .bg-inner, .list-section.sticky { 114 | background: var(--bg) !important; 115 | } 116 | 117 | .sticky-bg .bg-inner h3 { 118 | margin: 0px 100px !important; 119 | } 120 | 121 | .resize-line { 122 | border-left: solid 2px #8a8a8a !important; 123 | transform: translateX(1px); 124 | } 125 | 126 | .sidebar-resizer:active { 127 | .resize-line { 128 | border-color: var(--hl-txt) !important; 129 | } 130 | } 131 | 132 | .darwin { 133 | .nav-buttons.macos { 134 | margin-top: 0px; 135 | } 136 | 137 | .top-bar .inner { 138 | padding-left: 80px; 139 | } 140 | } -------------------------------------------------------------------------------- /src/static/main/spconnect/spotifyconnect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Connecting to Spotify... 9 | 10 | 11 |
12 |
13 |
14 |

Connecting to Spotify...

15 |
16 |

You'll be redirected to Spotify's authorisation page in a few seconds. Check the requested permissions, then click 'Agree'.

17 |
18 |
19 |
20 |

Authorised

21 |
22 |

We have received a response from Spotify. You may now close this tab and go back to the Firetail app.

23 |
24 |
25 |

An error occurred

26 |

We could not redirect you to Spotify's authorisation page. Sorry for the inconvenience.
You may now close this tab.

27 |
28 |
29 |

An error occurred

30 |

We could not connect your Spotify account. See the error message below for further information:

31 |
32 | Loading... 33 |
34 |

You may now close this tab.

35 |
36 |
37 |

Authorisation was cancelled

38 |

Access to your account was denied. Ensure you read the requested permissions and click 'Agree'.
You may now close this tab.

39 |
40 | 44 | 71 | 72 | -------------------------------------------------------------------------------- /src/components/playingbar/queue/QueueItem.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 42 | 43 | -------------------------------------------------------------------------------- /src/store/modules/panel.js: -------------------------------------------------------------------------------- 1 | import store from ".." 2 | import tr from '../../translation' 3 | 4 | const state = () => ({ 5 | currentPanelComponent: 'None', 6 | currentPanelProps: { 7 | topMsg: '', 8 | msg: "", 9 | buttons: [] 10 | }, 11 | active: false, 12 | closing: false, 13 | preventLoad: [] 14 | }) 15 | 16 | const mutations = { 17 | updatePanelProps(state, newProps) { 18 | state.currentPanelProps = newProps 19 | }, 20 | updateActive(state, newValue) { 21 | if (!state.closing) { 22 | state.active = newValue 23 | if (!newValue) { 24 | state.closing = true 25 | setTimeout(() => { 26 | state.closing = false 27 | state.currentPanelComponent = 'None' 28 | this.commit('panel/updatePanelProps', {}) 29 | }, 300) 30 | } 31 | } else { 32 | state.active = false 33 | } 34 | }, 35 | updatePanelComponent(state, component) { 36 | state.currentPanelComponent = component 37 | }, 38 | updatePreventLoad(state, name) { 39 | state.preventLoad.push(name) 40 | }, 41 | invokeNewPanel(state, props) { 42 | state.currentPanelProps = props.newProps 43 | state.currentPanelComponent = props.component 44 | this.commit('panel/updateActive', true) 45 | }, 46 | showNewPrompt(state, content) { 47 | let newProps = { 48 | topMsg: content.title, 49 | msg: content.message 50 | } 51 | if (content.buttons == 'clearLibrary') { 52 | newProps['buttons'] = [ 53 | { 54 | label: tr.t('BUTTONS.CANCEL'), 55 | onClick() { 56 | store.commit('panel/updateActive', false) 57 | } 58 | }, 59 | { 60 | label: tr.t('BUTTONS.OK'), 61 | onClick() { 62 | window.ipcRenderer.send('deleteLibrary') 63 | store.commit('panel/updateActive', false) 64 | } 65 | } 66 | ] 67 | } 68 | if (content.buttons === 'dismiss') { 69 | newProps['buttons'] = [ 70 | { 71 | label: tr.t('BUTTONS.OK'), 72 | onClick() { 73 | store.commit('panel/updateActive', false) 74 | } 75 | } 76 | ] 77 | } 78 | if (content.buttons == 'spotifyLogout') { 79 | newProps['buttons'] = [ 80 | { 81 | label: 'Cancel', 82 | onClick() { 83 | store.commit('panel/updateActive', false) 84 | } 85 | }, 86 | { 87 | label: 'Log Out', 88 | onClick() { 89 | localStorage.removeItem('sp-token') 90 | localStorage.removeItem('sp-name') 91 | localStorage.removeItem('sp-uri') 92 | store.commit('nav/updateSpotifyActive', false) 93 | store.commit('panel/updateActive', false) 94 | } 95 | } 96 | ] 97 | } 98 | state.currentPanelProps = newProps 99 | state.currentPanelComponent = 'PromptModule.vue' 100 | this.commit('panel/updateActive', true) 101 | } 102 | } 103 | 104 | export default { 105 | namespaced: true, 106 | state, 107 | mutations 108 | } -------------------------------------------------------------------------------- /src/assets/crash-tree.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | 40 | 41 | 42 | 44 | 45 | 46 | 47 | 48 | 50 | 52 | 54 | 56 | 58 | 59 | -------------------------------------------------------------------------------- /src/router/components/settings/options/SwitchOption.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 63 | 64 | -------------------------------------------------------------------------------- /src/router/components/settings/sections/GeneralSection.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 67 | 68 | -------------------------------------------------------------------------------- /src/components/playingbar/queue/QueuePopup.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/BackgroundEffects.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 27 | 28 | -------------------------------------------------------------------------------- /src/router/components/ListItem.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 43 | 44 | -------------------------------------------------------------------------------- /src/router/components/settings/sections/AppearanceSection.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 90 | 91 | -------------------------------------------------------------------------------- /src/translation/locales/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "SIDEBAR": { 3 | "HOME": "", 4 | "SETTINGS": "Einstellungen", 5 | "LIBRARY": "Bibliothek", 6 | "FAVOURITE": "Favoriten", 7 | "SONGS": "Lieder", 8 | "ARTISTS": "Künstler", 9 | "ALBUMS": "Alben", 10 | "PLAYLISTS": "Playlisten", 11 | "CREATE_PLAYLIST": "Playlist erstellen" 12 | }, 13 | "ROUTER": { 14 | "ALL_SONGS": "Lieder", 15 | "ARTISTS": "Künstler", 16 | "ALBUMS": "Alben", 17 | "FAVOURITES": "Favoriten", 18 | "HOME": "", 19 | "UNKNOWN": "", 20 | "UNKNOWN_DESC": "" 21 | }, 22 | "TOP_TITLE": { 23 | "COUNT_TYPE_SONGS": "{count} Lied | " 24 | }, 25 | "SONG_LIST": { 26 | "LIST_TITLE": "Titel", 27 | "LIST_ARTIST": "Künstler", 28 | "LIST_ALBUM": "Album", 29 | "LIST_DURATION": "Länge" 30 | }, 31 | "PLAYING_BAR": { 32 | "SONG_TITLE_NOT_PLAYING": "" 33 | }, 34 | "TOP_BAR": { 35 | "ADD_SONGS": "Lieder hinzufügen", 36 | "NUKE": "" 37 | }, 38 | "SETTINGS": { 39 | "TITLE": "Einstellungen", 40 | "SUBTITLES": { 41 | "GENERAL": "", 42 | "LIBRARY": "Bibliothek", 43 | "APPEARANCE": "Aussehen", 44 | "ACCESSIBILITY": "", 45 | "ADVANCED": "Erweitert", 46 | "UPDATE": "Aktualisierungen", 47 | "ABOUT": "Über" 48 | }, 49 | "BUTTON": { 50 | "CLEAR_LIBRARY": "", 51 | "REFRESH": "", 52 | "CHECK_UPDATE": "" 53 | }, 54 | "DROP_DOWN": { 55 | "APP_THEME": { 56 | "FIRETAIL": "Firetail", 57 | "CLASSIC": "Klassisch", 58 | "CYBER": "Cyber" 59 | }, 60 | "COLOUR_THEME": { 61 | "SYSTEM": "System", 62 | "DARK": "Dunkel", 63 | "LIGHT": "Hell" 64 | } 65 | }, 66 | "LANGUAGE": "Wähle eine Sprache", 67 | "START_PAGE": "", 68 | "CLEAR_LIBRARY": "", 69 | "REFRESH_METADATA": "", 70 | "APP_THEME": "", 71 | "COLOUR_THEME": "", 72 | "COLOUR_BAR": "", 73 | "INCREASE_PERFORMANCE": "", 74 | "USE_HIGH_CONTRAST": "", 75 | "REDUCE_MOTION": "", 76 | "BOLD_TEXT": "", 77 | "SHOW_FILE_CODEC": "", 78 | "FORCE_RTL": "", 79 | "CHECK_UPDATE": "", 80 | "UPDATE_BRANCH": "", 81 | "ABOUT": { 82 | "VERSION": "Version {version}", 83 | "COPYRIGHT": "© {year} {author}", 84 | "BUG_REPORT": "", 85 | "BUG_REPORT_LINK": "" 86 | } 87 | }, 88 | "POPUPS": { 89 | "CODEC_INFO": { 90 | "TITLE": "", 91 | "ALBUM": "", 92 | "YEAR": "", 93 | "TRACK_NUMBER": "", 94 | "DISC": "", 95 | "CODEC": "", 96 | "CONTAINER": "", 97 | "BITRATE": "", 98 | "SAMPLE_RATE": "", 99 | "BIT_DEPTH": "", 100 | "CHANNELS": "" 101 | } 102 | }, 103 | "PANEL": { 104 | "PLAYLIST": { 105 | "TITLE": "", 106 | "EDIT_TITLE": "", 107 | "NAME_INPUT": "", 108 | "DESC_INPUT": "", 109 | "DEFAULT_NAME": "", 110 | "DEFAULT_DESC": "", 111 | "REMOVE_IMAGE": "", 112 | "CHOOSE_IMAGE": "" 113 | } 114 | }, 115 | "BUTTONS": { 116 | "CREATE": "", 117 | "OK": "", 118 | "CANCEL": "", 119 | "YES": "", 120 | "NO": "", 121 | "SAVE": "" 122 | }, 123 | "TOOLTIP": { 124 | "PLAY_PAUSE": "", 125 | "PREVIOUS": "", 126 | "NEXT": "", 127 | "SHUFFLE": "", 128 | "REPEAT": "", 129 | "QUEUE": "", 130 | "VOLUME": "", 131 | "ZEN_MODE": "", 132 | "FAVOURITE": "" 133 | }, 134 | "SEARCH": { 135 | "TITLE": { 136 | "SONG": "", 137 | "ALBUM": "", 138 | "ARTIST": "", 139 | "PLAYLIST": "" 140 | }, 141 | "EMPTY_INPUT": "", 142 | "NO_RESULTS": "" 143 | }, 144 | "CONTEXT_MENU": { 145 | "SONG_LIST_ITEM": { 146 | "ADD_QUEUE": "", 147 | "GO_ARTIST": "", 148 | "GO_ALBUM": "", 149 | "VIEW_EXPLORER": "", 150 | "VIEW_FINDER": "", 151 | "VIEW_FILEMGR": "", 152 | "ADD_FAVOURITE": "", 153 | "ADD_PLAYLIST": "", 154 | "REMOVE_FAVOURITE": "", 155 | "REMOVE_PLAYLIST": "", 156 | "DELETE": "" 157 | }, 158 | "PLAYLIST_SIDE_ITEM": { 159 | "EDIT_PLAYLIST": "", 160 | "RENAME": "", 161 | "REMOVE_PLAYLIST": "" 162 | } 163 | }, 164 | "APP_NAME": "", 165 | "LANG_NAME": "Deutsche" 166 | } -------------------------------------------------------------------------------- /src/themes/test.scss: -------------------------------------------------------------------------------- 1 | html.dark { 2 | --bg: #18171c !important; 3 | --fg-bg: #2b2838 !important; 4 | --sub-fg: #232029 !important; 5 | } 6 | 7 | #app.test { 8 | /* honestly i don't know what the direction is here but whatever as long as it no longer looks like a spotify clone */ 9 | 10 | /* .root-sl { 11 | background: #18171c; 12 | } */ 13 | 14 | .playing-bar { 15 | background-color: #1f1e25; 16 | border-top: solid 1px var(--bd) !important; 17 | } 18 | 19 | .colour-bg { 20 | filter: saturate(0.8); 21 | } 22 | 23 | .standard .bg-gradient { 24 | //background: linear-gradient(rgb(5, 5, 5), transparent) !important; 25 | //background: #18171c; 26 | } 27 | 28 | .list-gradient-fade { 29 | display: none; 30 | } 31 | 32 | .bg-banner { 33 | width: 95%; 34 | height: 100%; 35 | max-height: 350px; 36 | background-image: url('../assets/songs-banner.svg'); 37 | background-size: cover; 38 | background-position: center 85%; 39 | //opacity: 0.2; 40 | } 41 | 42 | .sticky-bg { 43 | //backdrop-filter: blur(15px); 44 | } 45 | 46 | .list-section { 47 | border-color: #5f587c; 48 | } 49 | 50 | .list-section.sticky { 51 | //border: none; 52 | box-shadow: 0px 5px 5px rgba(0,0,0,.1); 53 | } 54 | } 55 | 56 | html.light { 57 | --sub-fg: #DEDBE4 !important; 58 | --fg-bg: #D7D2E8 !important; 59 | } 60 | 61 | :root.light #app.test { 62 | .bg-banner { 63 | background-image: url('../assets/songs-banner-light.svg'); 64 | } 65 | 66 | .list-section { 67 | border-color: #5f587c73; 68 | } 69 | 70 | .sticky-bg { 71 | //background: #f0ecff !important; 72 | background: #ffffff50 !important; 73 | backdrop-filter: blur(20px); 74 | } 75 | 76 | ::-webkit-scrollbar { 77 | //width: 16px !important; 78 | background: var(--bg); 79 | -webkit-app-region: no-drag; 80 | } 81 | 82 | ::-webkit-scrollbar-thumb { 83 | border: solid 4px var(--bg); 84 | background: #322d4750; 85 | border-radius: 20px; 86 | min-height: 60px; 87 | } 88 | 89 | ::-webkit-scrollbar-thumb:hover { 90 | background: #322d47; 91 | } 92 | 93 | ::-webkit-scrollbar-thumb:active { 94 | background: var(--hl-txt); 95 | } 96 | 97 | ::-webkit-scrollbar-button { 98 | display: none 99 | } 100 | 101 | .nav-button, .search-btn, .top-button-container { 102 | background: #ffffff7c; 103 | backdrop-filter: blur(20px); 104 | } 105 | 106 | .playing-bar { 107 | background: var(--bg); 108 | color: var(--text); 109 | 110 | .play-pause-icon:hover, .play-pause-icon:active { 111 | text-shadow: none; 112 | opacity: 0.8 !important; 113 | } 114 | 115 | .play-pause-icon:active { 116 | opacity: 0.6 !important; 117 | } 118 | } 119 | 120 | .seek-bar, .vol-bar { 121 | background: #00000025; 122 | 123 | .seek-hover-indicate { 124 | color: #ffffff; 125 | } 126 | 127 | .fill-hover { 128 | background: #00000036; 129 | } 130 | } 131 | 132 | .side-bar { 133 | background: #dfdfdf; 134 | color: var(--text); 135 | 136 | .item-sidebar, .special-button { 137 | color: var(--text); 138 | } 139 | 140 | .special-button .material-icons, .list-subtitle { 141 | border-color: var(--text); 142 | } 143 | 144 | /* .router-link-exact-active .item-sidebar, .router-link-active .item-sidebar { 145 | color: white; 146 | } */ 147 | 148 | .beta-tag { 149 | border-color: var(--text); 150 | color: var(--text); 151 | } 152 | 153 | .firetail-icon { 154 | filter: none; 155 | } 156 | } 157 | 158 | .bg-gradient { 159 | opacity: 0.35; 160 | } 161 | 162 | li:hover { 163 | background: #00000010; 164 | } 165 | 166 | .results-link.hactive { 167 | background: #00000028; 168 | } 169 | 170 | .results-link.hactive:hover { 171 | background: #00000022; 172 | } 173 | 174 | .artist-inner { 175 | border-color: #5f587c73; 176 | } 177 | } -------------------------------------------------------------------------------- /src/components/NotificationPopup.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 63 | 64 | -------------------------------------------------------------------------------- /src/components/zen/ZenMode.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 87 | 88 | -------------------------------------------------------------------------------- /src/router/components/HomeView.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 64 | 65 | -------------------------------------------------------------------------------- /src/components/ContextMenu.vue: -------------------------------------------------------------------------------- 1 | 15 | 104 | 105 | -------------------------------------------------------------------------------- /src/translation/locales/vi.json: -------------------------------------------------------------------------------- 1 | { 2 | "SIDEBAR": { 3 | "HOME": "Bắt đầu", 4 | "SETTINGS": "Cài đặt", 5 | "LIBRARY": "Thư viện", 6 | "FAVOURITE": "Ưa thích", 7 | "SONGS": "Bài hát", 8 | "ARTISTS": "Nghệ sĩ", 9 | "ALBUMS": "Đĩa", 10 | "PLAYLISTS": "Danh sách phát", 11 | "CREATE_PLAYLIST": "Tạo danh sách phát" 12 | }, 13 | "ROUTER": { 14 | "ALL_SONGS": "Bài hát", 15 | "ARTISTS": "Nghệ sĩ", 16 | "ALBUMS": "Đĩa", 17 | "FAVOURITES": "Ưa thích", 18 | "HOME": "Bắt đầu", 19 | "UNKNOWN": "Tuyến bạn đang tìm kiếm không thể thấy được.", 20 | "UNKNOWN_DESC": "Xin lỗi vì điều đó. Nếu bạn có lý do để tin rằng đã có lỗi nào, vui lòng tạo một vấn đề tại đây." 21 | }, 22 | "TOP_TITLE": { 23 | "COUNT_TYPE_SONGS": "{count} bài hát" 24 | }, 25 | "SONG_LIST": { 26 | "LIST_TITLE": "Tiêu đề", 27 | "LIST_ARTIST": "Nghệ sĩ", 28 | "LIST_ALBUM": "Đĩa", 29 | "LIST_DURATION": "Độ dài" 30 | }, 31 | "PLAYING_BAR": { 32 | "SONG_TITLE_NOT_PLAYING": "Không có bài hát đang phát..." 33 | }, 34 | "TOP_BAR": { 35 | "ADD_SONGS": "Thêm bài hát", 36 | "NUKE": "Xóa thư viện" 37 | }, 38 | "SETTINGS": { 39 | "TITLE": "Cài đặt", 40 | "SUBTITLES": { 41 | "GENERAL": "Chung", 42 | "LIBRARY": "Thư viện", 43 | "APPEARANCE": "Giao diện", 44 | "ACCESSIBILITY": "Trở nặng", 45 | "ADVANCED": "Nâng cao", 46 | "UPDATE": "Cập nhật", 47 | "ABOUT": "Giới thiệu" 48 | }, 49 | "BUTTON": { 50 | "CLEAR_LIBRARY": "Xóa thư viện", 51 | "REFRESH": "Tải lại", 52 | "CHECK_UPDATE": "Kiểm tra cập nhật" 53 | }, 54 | "DROP_DOWN": { 55 | "APP_THEME": { 56 | "FIRETAIL": "Firetail", 57 | "CLASSIC": "Cổ điển", 58 | "CYBER": "Đam mê mạng" 59 | }, 60 | "COLOUR_THEME": { 61 | "SYSTEM": "Hệ thống", 62 | "DARK": "Tối", 63 | "LIGHT": "Sáng" 64 | } 65 | }, 66 | "LANGUAGE": "Chọn một ngôn ngữ", 67 | "START_PAGE": "Chọn màn hình để hiển thị khi Firetail khởi động", 68 | "CLEAR_LIBRARY": "Xóa thư viện nhạc của Firetail", 69 | "REFRESH_METADATA": "Tải lại siêu dữ liệu của thư viện", 70 | "APP_THEME": "Giao diện ứng dụng (cần tải lại)", 71 | "COLOUR_THEME": "Mầu", 72 | "COLOUR_BAR": "Hiển thị màu đĩa trên thanh trạng thái phát", 73 | "INCREASE_PERFORMANCE": "Giảm hiệu ứng thay cho hiệu suất", 74 | "USE_HIGH_CONTRAST": "Sử dụng giao diện có độ tương phản cao", 75 | "REDUCE_MOTION": "Giảm chuyển động", 76 | "BOLD_TEXT": "Tăng độ đậm phông chứ", 77 | "SHOW_FILE_CODEC": "Xem thông tin mã hóa cho tệp", 78 | "FORCE_RTL": "Buộc giao diện RTL", 79 | "CHECK_UPDATE": "Kiểm tra cập nhật ứng dụng", 80 | "UPDATE_BRANCH": "Chọn kênh cho cập nhật ứng dụng", 81 | "ABOUT": { 82 | "VERSION": "Phiên bản {version}", 83 | "COPYRIGHT": "© {year} {author}", 84 | "BUG_REPORT": "Đã thấy một lỗi? Vui lòng báo cáo {link}", 85 | "BUG_REPORT_LINK": "đây trên GitHub." 86 | } 87 | }, 88 | "POPUPS": { 89 | "CODEC_INFO": { 90 | "TITLE": "Thông tin", 91 | "ALBUM": "Đĩa: ", 92 | "YEAR": "Năm: ", 93 | "TRACK_NUMBER": "Số trên đĩa: ", 94 | "DISC": "Số đĩa: ", 95 | "CODEC": "Loại mã hóa: ", 96 | "CONTAINER": "Thùng: ", 97 | "BITRATE": "Tốc độ bit: ", 98 | "SAMPLE_RATE": "Tỷ lệ mẫu: ", 99 | "BIT_DEPTH": "Độ sâu bit: ", 100 | "CHANNELS": "Số kênh: " 101 | } 102 | }, 103 | "PANEL": { 104 | "PLAYLIST": { 105 | "TITLE": "Tạo danh sách phát", 106 | "EDIT_TITLE": "Sửa danh sách phát", 107 | "NAME_INPUT": "Tên", 108 | "DESC_INPUT": "Sự miêu tả", 109 | "DEFAULT_NAME": "Danh sách phát của tôi", 110 | "DEFAULT_DESC": "Sự miêu tả danh sách phát của tôi...", 111 | "REMOVE_IMAGE": "Xóa hình ảnh", 112 | "CHOOSE_IMAGE": "Chọn hình ảnh" 113 | } 114 | }, 115 | "BUTTONS": { 116 | "CREATE": "Tạo", 117 | "OK": "OK", 118 | "CANCEL": "Hủy", 119 | "YES": "Có", 120 | "NO": "Không", 121 | "SAVE": "Lưu" 122 | }, 123 | "TOOLTIP": { 124 | "PLAY_PAUSE": "Phát/tạm dừng", 125 | "PREVIOUS": "Nhảy lại", 126 | "NEXT": "Nhảy tiếp", 127 | "SHUFFLE": "Ngẫu nhiên", 128 | "REPEAT": "Lập lại", 129 | "QUEUE": "Hàng phát", 130 | "VOLUME": "Âm lượng", 131 | "ZEN_MODE": "Chế độ Zen", 132 | "FAVOURITE": "Thích" 133 | }, 134 | "SEARCH": { 135 | "TITLE": { 136 | "SONG": "Bài hát", 137 | "ALBUM": "Đĩa", 138 | "ARTIST": "Nghệ sĩ", 139 | "PLAYLIST": "Danh sách phát" 140 | }, 141 | "EMPTY_INPUT": "Bắt đầu tìm kiếm…", 142 | "NO_RESULTS": "Không có kết quả nào…" 143 | }, 144 | "CONTEXT_MENU": { 145 | "SONG_LIST_ITEM": { 146 | "ADD_QUEUE": "Thêm vào hàng phát", 147 | "GO_ARTIST": "Đi đến nghệ sĩ", 148 | "GO_ALBUM": "Đi đến đĩa", 149 | "VIEW_EXPLORER": "Hiển thị trong File Explorer", 150 | "VIEW_FINDER": "Hiển thị trong Finder", 151 | "VIEW_FILEMGR": "Hiển thị trong trình quản lý tệp", 152 | "ADD_FAVOURITE": "Thêm vào ưa thích", 153 | "ADD_PLAYLIST": "Thêm vào danh sách phát", 154 | "REMOVE_FAVOURITE": "Xóa khỏi ưa thích", 155 | "REMOVE_PLAYLIST": "Xoá khỏi danh sách phát", 156 | "DELETE": "Xóa khỏi thư viện" 157 | }, 158 | "PLAYLIST_SIDE_ITEM": { 159 | "EDIT_PLAYLIST": "Sửa danh sách phát", 160 | "RENAME": "Đổi tên", 161 | "REMOVE_PLAYLIST": "Xóa danh sách phát" 162 | } 163 | }, 164 | "APP_NAME": "Firetail", 165 | "LANG_NAME": "Tiếng việt" 166 | } -------------------------------------------------------------------------------- /src/modules/files.js: -------------------------------------------------------------------------------- 1 | import * as metadata from 'music-metadata' 2 | import time from '../modules/timeformat' 3 | import { writeFile, statSync, readdirSync } from 'fs' 4 | import { promises as fs, constants as fsConstants } from 'fs' 5 | import { app, BrowserWindow } from 'electron' 6 | import mime from 'mime-types' 7 | import { resolve } from 'path' 8 | import { Jimp } from "jimp"; 9 | 10 | let randomString = length => { 11 | let text = '' 12 | let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' 13 | for (let i = 0; i < length; i++) { 14 | text += characters.charAt(Math.floor(Math.random() * characters.length)) 15 | } 16 | return text 17 | } 18 | 19 | const getFiles = async (dir) => { 20 | const dirents = await fs.readdir(dir, { withFileTypes: true }); 21 | const files = await Promise.all(dirents.map((dirent) => { 22 | const res = resolve(dir, dirent.name); 23 | return dirent.isDirectory() ? getFiles(res) : res; 24 | })); 25 | return files.flat() 26 | } 27 | 28 | export default { 29 | async processFiles(files) { 30 | let processFiles = [] 31 | for (const index in files) { 32 | const file = files[index] 33 | const stat = statSync(file) 34 | if (stat.isDirectory()) { 35 | const dirFiles = await getFiles(file) 36 | processFiles = processFiles.concat(await this.processFiles(dirFiles)) 37 | } else { 38 | const fileName = resolve(file).split('/') 39 | const ext = fileName[fileName.length - 1].split('.').pop() 40 | const isAudio = mime.lookup(ext) 41 | if (isAudio && isAudio.startsWith('audio')) { 42 | processFiles.push([file, fileName[fileName.length - 1]]) 43 | } 44 | } 45 | } 46 | return processFiles 47 | /*const stuff = await this.addFiles(processFiles) 48 | return stuff*/ 49 | }, 50 | async addFiles(songs) { 51 | let path = app.getPath('userData') 52 | let getData = new Promise(resolve => { 53 | let toAdd = [] 54 | let imgUsed = [] 55 | let progress = 0; 56 | songs.forEach(async f => { 57 | let id = randomString(10) 58 | let meta = await metadata.parseFile(f[0]).catch(err => { 59 | console.log(err); 60 | }) 61 | if (!meta) return; 62 | let explicit = null 63 | if (meta.native.iTunes) { 64 | const result = meta.native.iTunes.find(tag => tag.id == 'rtng'); 65 | if (result) { 66 | explicit = result.value 67 | } 68 | } 69 | let metaObj = { 70 | title: meta.common.title ? meta.common.title : f[1], 71 | artist: meta.common.artist ? meta.common.artist : 'Unknown Artist', 72 | allArtists: meta.common.artists ? JSON.stringify(meta.common.artists) : null, 73 | albumArtist: meta.common.albumartist ? meta.common.albumartist : meta.common.artist ? meta.common.artist : 'Unknown Artist', 74 | album: meta.common.album ? meta.common.album : 'Unknown Album', 75 | duration: meta.format.duration ? time.timeFormat(meta.format.duration) : 0, 76 | realdur: meta.format.duration ? meta.format.duration : 0, 77 | path: f[0], 78 | id: id, 79 | hasImage: meta.common.picture ? 1 : 0, 80 | trackNum: meta.common.track.no ? meta.common.track.no : null, 81 | year: meta.common.year ? `${meta.common.year}` : null, 82 | disc: meta.common.disk ? meta.common.disk.no : null, 83 | explicit, 84 | genre: meta.common.genre ? JSON.stringify(meta.common.genre) : null 85 | } 86 | let artistAlbum = `${metaObj.albumArtist}${meta.common.album}`.replace(/[`~!@#$%^&*()_|+\-=?;:'",.<> {}[\]\\/]/gi, '') 87 | toAdd.push(metaObj) 88 | progress++ 89 | BrowserWindow.getAllWindows()[0].webContents.send('doneProgress', [progress, songs.length]) 90 | if (toAdd.length == songs.length) { 91 | BrowserWindow.getAllWindows()[0].webContents.send('startOrFinish', false) 92 | resolve(toAdd) 93 | } 94 | if (meta.common.picture && imgUsed.indexOf(artistAlbum) == -1) { 95 | imgUsed.push(artistAlbum) 96 | let pic = meta.common.picture[0] 97 | writeFile(`${path}/images/${artistAlbum}.jpg`, pic.data, err => { 98 | if (err) console.log(err) 99 | }) 100 | } 101 | }) 102 | }) 103 | return await getData 104 | }, 105 | async savePlaylistImage(buffer, id) { 106 | console.log(buffer, id) 107 | if (buffer) { 108 | const playlistImgPath = resolve(app.getPath('userData'), 'images/playlist') 109 | try { 110 | await fs.access(playlistImgPath, fsConstants.R_OK | fsConstants.W_OK) 111 | } catch(e) { 112 | await fs.mkdir(playlistImgPath) 113 | } 114 | try { 115 | const image = await Jimp.read(Buffer.from(buffer)) 116 | await image.cover({w: 256, h: 256}) 117 | await image.write(`${playlistImgPath}/${id}.jpg`) 118 | } catch(err) { 119 | console.error(err) 120 | } 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /src/static/main/changelog.md: -------------------------------------------------------------------------------- 1 | Starting from version 1.0.0, the format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 2 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 3 | 4 | ## [Unreleased] 5 | 6 | ### Added 7 | 8 | - Added favourites page 9 | - Added playlists system 10 | - Added global search 11 | - Added queue viewer 12 | - Added sidebar resizing 13 | - Added accessibility features: 14 | - High contrast style 15 | - Reduced motion 16 | - Increased font weight 17 | - Screen reader support 18 | - Added performance mode for low-end devices 19 | - Added total time in song list headers 20 | - Added album artist name to album view 21 | - Added song details when hovering over song title/artist in playing bar 22 | - Added additional 'advanced' information to about section in settings page 23 | - Added third-party licence and changelog information 24 | - Added right-to-left appearance 25 | - Added more languages 26 | - Added light colour theme 27 | - Added ability to use system colour theme 28 | - Added ability to view selected song in OS default file manager 29 | - Added the following song metadata: 30 | - Disc number 31 | - Album artist 32 | - All artists 33 | - Genres 34 | - Explicit 35 | 36 | ### Changed 37 | - Now uses custom title bar on Windows and macOS platforms 38 | - Top bar icons no longer display text 39 | - Settings navigation button moved to top bar 40 | - New header backgrounds 41 | - Refreshed icon set 42 | - New appearance for artist and album view with nothing selected 43 | - Refreshed fullscreen/zen mode UI 44 | - Main content container now rounded, playing bar no longer appears separate 45 | - Album colour now appears as a soft glow in bottom-left corner 46 | - Removed Firetail name and logo from sidebar 47 | - Moved favourite icon in song list to second-last column 48 | - Artist/album list now uses virtual scrolling 49 | - Improved language/i18n support 50 | - File importing is now recursive 51 | - Numerous UI tweaks 52 | 53 | ### Removed 54 | - Spotify integration. Will likely be added again in future versions. 55 | - Unused home navigation in sidebar 56 | - Unused 'Add to queue' button in context menu 57 | 58 | ### Fixed 59 | - Shuffling now works as expected 60 | - Repeat queue loops correctly 61 | 62 | ## 0.7.0 63 | 64 | Additions/changes since 0.6.0: 65 | * Full UI overhaul 66 | * Added albums page 67 | * Added settings page 68 | * Lists now highlightable w/ right click menu 69 | * Added basic Spotify integration 70 | * Added full screen 'zen mode' 71 | * Song list optimisations 72 | * Added context and track repeat options 73 | * Added import progress notification 74 | * Added drag and drop imports for files and folders 75 | * Added ability to open music files with Firetail 76 | 77 | ## 0.6.0 78 | Released: 14 November 2020 79 | 80 | Additions/changes since 0.5.2: 81 | * Begun rewriting the project once again 82 | * Better performance overall 83 | * Faster importing speeds 84 | * New panel system in the works 85 | * Experimental Spotify integration support 86 | 87 | ## 0.5.2 88 | Released: 12 June 2020 89 | 90 | Additions/changes since 0.5.1: 91 | * Updated app icon 92 | * New icons inside app 93 | * Other UI tweaks 94 | * Ability to add images to playlists 95 | * Ability to set playlist descriptions (still not visible yet) 96 | * Added some new keyboard controls 97 | * Bug fixes 98 | 99 | ## 0.5.1 100 | Released: 2 April 2020 101 | 102 | Additions/changes since 0.5.0: 103 | * Added new app icon 104 | * Updated albums tab UI 105 | * Added playlists (incomplete) 106 | * Minor UI tweaks 107 | * Bug fixes 108 | 109 | ## 0.5.0 110 | Released: 9 February 2020 111 | 112 | This release is the beginning of a rewrite. Expect broken and missing features. 113 | 114 | Features available in this release: 115 | * Basic controls (play, pause, skip, previous, shuffle, repeat, seek etc.) 116 | * Add songs to library 117 | * Display song metadata 118 | * Remove library 119 | * Artists & albums tabs 120 | 121 | ## 0.4.0 122 | Released: 1 December 2019 123 | 124 | This release is incomplete due to a rewrite. Expect broken features. 125 | 126 | Additions/changes since 0.3.1: 127 | * New library system 128 | * Ability to drag songs into app to add them to library 129 | * Artists/albums tabs are now functional 130 | * Sidebar can now be hidden/shown 131 | * Song lists now show title and number of songs 132 | * Artist and song name are now shown on song lists (if they exist) 133 | * Effects now work properly 134 | * Added support for native media controls on Windows 135 | * Custom CSS file support 136 | * Minor UI tweaks 137 | * New black theme 138 | * Ability to enable native window frame on Windows 139 | * Bug fixes 140 | 141 | ## 0.3.1 142 | Released: 12 July 2019 143 | 144 | Additions/changes since 0.3.0: 145 | * Huge amount of bug fixes 146 | * Redesigned mini player 147 | * New colour accent 148 | * New button style 149 | * Other minor UI tweaks 150 | * New changelog menu 151 | * Restructured filesystem 152 | 153 | ## 0.3.0 154 | Released: 9 June 2019 155 | 156 | Additions/changes since 0.1.0: 157 | * Project name changed 158 | * New settings panel 159 | * Mini player 160 | * UI tweaks 161 | * Better volume controls 162 | * New library management method (still incomplete, however functional) 163 | 164 | ## 0.2.1 165 | Released: 3 April 2019 166 | 167 | Additions/changes since 0.2.0: 168 | * UI improvements 169 | * Added basic settings 170 | * Added light theme 171 | * Added Discord Rich Presence 172 | * Added basic playlists (incomplete) 173 | 174 | ## 0.2.0 175 | Released: 24 January 2019 176 | 177 | Additions/changes since 0.1.0: 178 | * UI Improvements 179 | * Added audio effects 180 | * Added mini player 181 | * Improved volume control 182 | 183 | ## 0.1.0 184 | Released: 22 November 2018 185 | 186 | This is the first official beta release for Audiation! -------------------------------------------------------------------------------- /src/components/ItemAdd.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 78 | 79 | -------------------------------------------------------------------------------- /src/modules/audio.js: -------------------------------------------------------------------------------- 1 | /* 2 | This new audio module will be implemented after Firetail 1.0 is released 3 | due to the complexity of replacing the current audio system. 4 | */ 5 | 6 | import store from '../store/index.js' 7 | import {bus} from '../renderer.js' 8 | 9 | class AudioPlayer { 10 | audio = new Audio() 11 | _currentTime = 0 12 | duration = 0 13 | volume = 1 14 | muted = false 15 | _queue = [] 16 | _index = 0 17 | _currentSong = null 18 | _paused = true 19 | stopped = true 20 | updateLocal = 0 21 | playbackRate = 1 22 | 23 | constructor() { 24 | this.audio.addEventListener('timeupdate', (evt) => { this.timeUpdate(evt) }) 25 | this.audio.addEventListener('ended', () => { this.ended() }) 26 | this.audio.addEventListener('pause', () => { 27 | store.commit('audio/updatePause', true) 28 | }) 29 | this.audio.addEventListener('play', () => { 30 | store.commit('audio/updatePause', false) 31 | }) 32 | this.metadata = new Metadata(this) 33 | } 34 | 35 | get currentTime() { 36 | return this._currentTime 37 | } 38 | 39 | set currentTime(time) { 40 | this.audio.currentTime = time 41 | this._currentTime = time 42 | } 43 | 44 | get paused() { 45 | return this._paused 46 | } 47 | 48 | get queue() { 49 | return this._queue 50 | } 51 | 52 | get index() { 53 | return this._index 54 | } 55 | 56 | set queue(songs) { 57 | this._queue = songs 58 | } 59 | 60 | get currentSong() { 61 | return this._currentSong 62 | } 63 | 64 | set currentSong(id) { 65 | this._currentSong = id 66 | store.commit('audio/updateCurrentSongStringPlain', id) 67 | } 68 | 69 | set index(id) { 70 | this._index = id 71 | console.log(this.index) 72 | store.commit('audio/updateCurrentSongIndexPlain', id) 73 | } 74 | 75 | async playSong(song) { 76 | if (!song.path) return 77 | this.audio.src = `local-resource://${song.path}` 78 | //console.log(this.audio.currentTime) 79 | this.currentSong = song.id 80 | store.commit('audio/updatePause', false) 81 | this.metadata.setSongMetadata(song) 82 | await this.audio.play().catch(err => { 83 | let msg = err.toString() 84 | if (err.toString().includes('supported source was found')) { 85 | msg = 'The file you requested could not be played. Make sure the file exists and try again.' 86 | } 87 | bus.$emit('notifySwag', { 88 | title: "Unfortunately, an error occurred", 89 | message: msg, 90 | icon: "error" 91 | }) 92 | }) 93 | window.localStorage.setItem('lastPlayed', JSON.stringify({ 94 | song: song, 95 | view: store.state.nav.playingView, 96 | songIndex: this._index 97 | })) 98 | window.localStorage.setItem('lastPlayTime', 0.1) 99 | } 100 | 101 | togglePlay() { 102 | if (this.audio.paused) this.audio.play() 103 | else this.audio.pause() 104 | return this.audio.paused 105 | } 106 | 107 | nextSong() { 108 | this.index++ 109 | console.log('NEXT SONG ' + this.index) 110 | this.currentSong = this._queue[this.index] 111 | this.playSong(this.currentSong) 112 | } 113 | 114 | prevSong() { 115 | if (this._currentTime > 5) { 116 | this._currentTime = 0 117 | } else { 118 | this.index-- 119 | this.currentSong = this._queue[this.index] 120 | this.playSong(this.currentSong) 121 | } 122 | } 123 | 124 | timeUpdate(evt) { 125 | if (this.updateLocal === 10) { 126 | window.localStorage.setItem('lastPlayTime', this._currentTime) 127 | this.updateLocal = 0 128 | } else { 129 | this.updateLocal++ 130 | } 131 | this._currentTime = this.audio.currentTime 132 | this.duration = this.audio.duration 133 | store.commit('audio/timeUpdate', [this.duration, this._currentTime]) 134 | } 135 | 136 | ended() { 137 | this.nextSong() 138 | } 139 | } 140 | 141 | class Metadata { 142 | title = null 143 | artist = null 144 | album = null 145 | path = null 146 | 147 | constructor(audioPlayer) { 148 | this.audioPlayer = audioPlayer 149 | } 150 | 151 | setSongMetadata(song) { 152 | store.commit('audio/songMetadata', [song.title, song.artist]) 153 | this.updateMediaSession(song) 154 | } 155 | 156 | updateMediaSession(song) { 157 | let metadata = { 158 | title: song.title, 159 | artist: song.artist, 160 | album: song.album, 161 | src: song.path 162 | } 163 | if (song.hasImage === 1) { 164 | let port = store.state.nav.port 165 | let artistAlbum = '' 166 | if (song.id === 'customSong') { 167 | artistAlbum = song.customImage 168 | } else { 169 | artistAlbum = `http://localhost:${port}/images/${(song.artist + song.album).replace(/[`~!@#$%^&*()_|+\-=?;:'",.<> {}[\]\\/]/gi, '')}.jpg` 170 | } 171 | metadata['artwork'] = [{src: artistAlbum, sizes: '512x512', type: 'image/jpeg'}] 172 | } 173 | navigator.mediaSession.metadata = new window.MediaMetadata(metadata) 174 | navigator.mediaSession.setActionHandler('previoustrack', this.prevSong) 175 | navigator.mediaSession.setActionHandler('nexttrack', this.nextSong) 176 | try { 177 | navigator.mediaSession.setPositionState({ 178 | duration: this.audioPlayer.duration, 179 | playbackRate: this.audioPlayer.playbackRate, 180 | position: this.audioPlayer.currentTime 181 | }); 182 | } catch {} 183 | } 184 | } 185 | 186 | export default AudioPlayer -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import SongList from './components/SongList.vue' 4 | //import SimpleSongList from './components/SimpleSongList/SimpleSongList' 5 | import Unknown from './components/UnknownView.vue' 6 | import ArtistAlbumView from './components/ArtistAlbumView.vue' 7 | import store from '../store' 8 | import tr from '../translation' 9 | import Settings from './components/settings/SettingsView.vue' 10 | import Home from './components/HomeView.vue' 11 | import HiddenView from './components/HiddenView.vue' 12 | import VirtualList from 'vue-virtual-scroll-list' 13 | import sort from '../modules/sort.js' 14 | //import { bus } from '../main' 15 | 16 | Vue.component('virtual-list', VirtualList) 17 | 18 | /* class VueRouterEx extends VueRouter { 19 | constructor(options) { 20 | super(options) 21 | this.matcher 22 | const {addRoutes} = this.matcher 23 | const {routes} = options 24 | 25 | this.routes = routes 26 | 27 | this.matcher.addRoutes = newRoutes => { 28 | this.routes.push(...newRoutes) 29 | addRoutes(newRoutes) 30 | } 31 | } 32 | } */ 33 | 34 | Vue.use(VueRouter) 35 | 36 | const router = new VueRouter({ 37 | routes: [ 38 | { 39 | path: '/songs', 40 | component: SongList, 41 | name: 'NoUpdate' 42 | }, 43 | { 44 | path: '/artists', 45 | component: ArtistAlbumView, 46 | name: tr.t('ROUTER.ARTISTS'), 47 | children: [ 48 | { 49 | path: '', 50 | component: SongList, 51 | } 52 | ] 53 | }, 54 | { 55 | path: '/albums', 56 | component: ArtistAlbumView, 57 | name: tr.t('ROUTER.ALBUMS'), 58 | children: [ 59 | { 60 | path: '', 61 | component: SongList, 62 | } 63 | ] 64 | }, 65 | { 66 | path: '/genres', 67 | component: ArtistAlbumView, 68 | name: 'Genres', 69 | children: [ 70 | { 71 | path: '', 72 | component: SongList, 73 | } 74 | ] 75 | }, 76 | { 77 | path: '/playlist', 78 | component: SongList, 79 | name: 'Playlist' 80 | }, 81 | { 82 | path: '/settings', 83 | component: Settings, 84 | name: tr.t('SETTINGS.TITLE') 85 | }, 86 | { 87 | path: '/home', 88 | component: Home, 89 | name: 'Home' 90 | }, 91 | { 92 | path: '*', 93 | component: Unknown, 94 | name: 'Unknown' 95 | }, 96 | { 97 | path: '/', 98 | component: HiddenView, 99 | name: 'Hidden' 100 | }, 101 | { 102 | path: '/liked', 103 | component: SongList, 104 | name: 'Favourites' 105 | } 106 | ], 107 | history: true, 108 | root: '/' 109 | }) 110 | 111 | let isDoneOnce = false 112 | 113 | window.ipcRenderer.receive('updateNav', (event, checkNav) => { 114 | store.commit('nav/updateCheckNav', checkNav) 115 | }) 116 | 117 | router.beforeEach(async (to, from, next) => { 118 | document.getElementById('main-container').scrollTo(0, 0) 119 | if (to.path != '/playlist') { 120 | store.commit('audio/updateCurrentList', []) 121 | } 122 | if (to.query.column && to.query.q) { 123 | store.dispatch('audio/getSpecificSongs', { 124 | column: [to.query.column, to.query.albumArtist ? to.query.albumArtist : null], 125 | q: to.query.q 126 | }) 127 | } else if (to.query.view == 'firetailnoselect') { 128 | store.commit('audio/getNoSongs') 129 | } else if (to.path == '/playlist' && to.query.id) { 130 | const playlist = await window.ipcRenderer.invoke('getSpecificPlaylist', to.query.id) 131 | const sortedSongs = sort.sortArrayNum(JSON.parse(playlist[0].songIds), 'position') 132 | const ids = [] 133 | sortedSongs.forEach(song => { 134 | ids.push(song.id) 135 | }) 136 | const songs = await window.ipcRenderer.invoke('getSomeFromColumnMatches', ids) 137 | const sortedSongsToUse = [] 138 | sortedSongs.forEach(song => { 139 | sortedSongsToUse.push(songs[0].find(item => item.id == song.id)) 140 | }) 141 | store.commit('audio/updateCurrentListNoSort', sortedSongsToUse) 142 | store.commit('audio/setCurrentListDur', songs[1]) 143 | store.commit('playlist/setCurrentPlaylist', playlist[0]) 144 | } else if (to.path == '/liked') { 145 | store.dispatch('audio/getAllFavourites') 146 | } else { 147 | store.dispatch('audio/getAllSongs') 148 | } 149 | if (to.path === '/genres') { 150 | const genreSongs = await window.ipcRenderer.invoke('getGenreResults', to.query.genre) 151 | store.commit('audio/updateCurrentList', genreSongs[0]) 152 | store.commit('audio/setCurrentListDur', genreSongs[1]) 153 | } 154 | if (to.query.view) { 155 | store.commit('nav/updateCurrentView', to.query.view) 156 | } 157 | if (to.query.hideTop || to.name == 'Unknown') { 158 | store.commit('nav/updateTopVisible', false) 159 | } else { 160 | store.commit('nav/updateTopVisible', true) 161 | } 162 | if (to.name != 'NoUpdate') { 163 | store.commit('nav/updateScreenTitle', to.name) 164 | } else { 165 | store.commit('nav/updateScreenTitle', to.query.name) 166 | } 167 | next() 168 | if (!isDoneOnce) { 169 | isDoneOnce = true 170 | window.ipcRenderer.send('clearHistory') 171 | store.commit('nav/updateCheckNav', { 172 | back: false, 173 | true: false 174 | }) 175 | } 176 | }) 177 | 178 | export default router -------------------------------------------------------------------------------- /src/components/sidebar/SidePlaylists.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 100 | 101 | -------------------------------------------------------------------------------- /src/router/components/settings/sections/AboutSection.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 106 | 107 | -------------------------------------------------------------------------------- /src/components/tour/SidebarTour.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 80 | 81 | -------------------------------------------------------------------------------- /src/translation/locales/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "SIDEBAR": { 3 | "HOME": "منزل", 4 | "SETTINGS": "Settings", 5 | "LIBRARY": "Library", 6 | "FAVOURITE": "مفضل", 7 | "SONGS": "أغنية", 8 | "ARTISTS": "فنان", 9 | "ALBUMS": "ألبوم تواقيع", 10 | "PLAYLISTS": "Playlists", 11 | "CREATE_PLAYLIST": "Create Playlist" 12 | }, 13 | "ROUTER": { 14 | "ALL_SONGS": "أغنية", 15 | "ARTISTS": "فنان", 16 | "ALBUMS": "ألبوم تواقيع", 17 | "FAVOURITES": "مفضل", 18 | "HOME": "منزل", 19 | "UNKNOWN": "The route you were looking for could not be found.", 20 | "UNKNOWN_DESC": "Sorry about that. If you believe this is an error, please create an issue here." 21 | }, 22 | "TOP_TITLE": { 23 | "COUNT_TYPE_SONGS": "{count} song | {count} songs" 24 | }, 25 | "SONG_LIST": { 26 | "LIST_TITLE": "Title", 27 | "LIST_ARTIST": "Artist", 28 | "LIST_ALBUM": "Album", 29 | "LIST_DURATION": "Duration" 30 | }, 31 | "PLAYING_BAR": { 32 | "SONG_TITLE_NOT_PLAYING": "No song playing..." 33 | }, 34 | "TOP_BAR": { 35 | "ADD_SONGS": "Add Songs", 36 | "NUKE": "Remove Library" 37 | }, 38 | "SETTINGS": { 39 | "TITLE": "Settings", 40 | "SUBTITLES": { 41 | "GENERAL": "General", 42 | "LIBRARY": "Library", 43 | "APPEARANCE": "Appearance", 44 | "ACCESSIBILITY": "Accessibility", 45 | "ADVANCED": "Advanced", 46 | "UPDATE": "Updates", 47 | "ABOUT": "About" 48 | }, 49 | "BUTTON": { 50 | "CLEAR_LIBRARY": "Clear Library", 51 | "REFRESH": "Refresh", 52 | "CHECK_UPDATE": "Check for updates" 53 | }, 54 | "DROP_DOWN": { 55 | "LANG_AUTO": "Auto", 56 | "APP_THEME": { 57 | "FIRETAIL": "Firetail", 58 | "CLASSIC": "Classic", 59 | "CYBER": "Cyber" 60 | }, 61 | "COLOUR_THEME": { 62 | "SYSTEM": "System", 63 | "DARK": "Dark", 64 | "LIGHT": "Light" 65 | }, 66 | "START_SCREEN": { 67 | "HOME": "Home", 68 | "SONGS": "Songs", 69 | "ARTISTS": "Artists", 70 | "ALBUMS": "Albums", 71 | "FAVOURITES": "Favorites" 72 | }, 73 | "UPDATE_BRANCH": { 74 | "STABLE": "Stable", 75 | "PREVIEW": "Preview", 76 | "DEVELOPMENT": "Development" 77 | } 78 | }, 79 | "LANGUAGE": "Select a language", 80 | "START_PAGE": "Choose which screen is displayed when Firetail is launched", 81 | "CLEAR_LIBRARY": "Clear your Firetail music library", 82 | "REFRESH_METADATA": "Refresh your library\u0027s metadata", 83 | "APP_THEME": "App theme (requires reload for now)", 84 | "COLOUR_THEME": "Color theme", 85 | "COLOUR_BAR": "Show album color on playing bar", 86 | "INCREASE_PERFORMANCE": "Reduce effects to increase performance", 87 | "USE_HIGH_CONTRAST": "Use high contrast style", 88 | "REDUCE_MOTION": "Reduce motion", 89 | "BOLD_TEXT": "Increase font weight", 90 | "SHOW_FILE_CODEC": "Show file codec information", 91 | "FORCE_RTL": "Force UI to right-to-left", 92 | "CHECK_UPDATE": "Check for app updates", 93 | "UPDATE_BRANCH": "Choose a channel for app updates", 94 | "ABOUT": { 95 | "VERSION": "Version {version}", 96 | "COPYRIGHT": "© {year} {author}", 97 | "BUG_REPORT": "Found a bug? Please report it {link}", 98 | "BUG_REPORT_LINK": "here on GitHub.", 99 | "LICENSE_CHANGELOG": "{license}, {changelog}", 100 | "THIRD_PARTY_LICENSE": "Third-party licenses", 101 | "VIEW_CHANGELOG": "view changelog", 102 | "ADVANCED_INFO_LABEL": "Advanced information", 103 | "ADVANCED_INFO": { 104 | "BUILD": "Build: {build}", 105 | "PLATFORM": "Platform: {platform}", 106 | "PLATFORM_VER": "Platform version: {version}", 107 | "ARCHITECTURE": "Architecture: {arch}", 108 | "ELECTRON": "Electron: {version}", 109 | "CHROMIUM": "Chromium: {version}", 110 | "NODE": "Node: {version}" 111 | } 112 | } 113 | }, 114 | "POPUPS": { 115 | "CODEC_INFO": { 116 | "TITLE": "Details", 117 | "ALBUM": "Album: ", 118 | "YEAR": "Year: ", 119 | "TRACK_NUMBER": "Track No.: ", 120 | "DISC": "Disc: ", 121 | "CODEC": "Codec: ", 122 | "CONTAINER": "Container: ", 123 | "BITRATE": "Bit rate: ", 124 | "SAMPLE_RATE": "Sample rate: ", 125 | "BIT_DEPTH": "Bit depth: ", 126 | "CHANNELS": "No. Channels: " 127 | } 128 | }, 129 | "PANEL": { 130 | "PLAYLIST": { 131 | "TITLE": "Create Playlist", 132 | "EDIT_TITLE": "Edit Playlist", 133 | "NAME_INPUT": "Name", 134 | "DESC_INPUT": "Description", 135 | "DEFAULT_NAME": "My Playlist", 136 | "DEFAULT_DESC": "My playlist description...", 137 | "REMOVE_IMAGE": "Remove Image", 138 | "CHOOSE_IMAGE": "Choose image" 139 | }, 140 | "PROMPT": { 141 | "RESTART": { 142 | "TITLE": "إعادة التشغيل مطلوبة", 143 | "MESSAGE": "يتطلب هذا الخيار إعادة تشغيل Firetail ليصبح ساري المفعول بالكامل." 144 | } 145 | } 146 | }, 147 | "BUTTONS": { 148 | "CREATE": "Create", 149 | "OK": "نعم", 150 | "CANCEL": "Cancel", 151 | "YES": "Yes", 152 | "NO": "No", 153 | "SAVE": "Save" 154 | }, 155 | "TOOLTIP": { 156 | "PLAY_PAUSE": "Play/pause", 157 | "PREVIOUS": "Previous", 158 | "NEXT": "Next", 159 | "SHUFFLE": "Shuffle", 160 | "REPEAT": "Repeat", 161 | "QUEUE": "Queue", 162 | "VOLUME": "Volume", 163 | "ZEN_MODE": "Zen Mode", 164 | "FAVOURITE": "Favorite" 165 | }, 166 | "SEARCH": { 167 | "TITLE": { 168 | "SONG": "Songs", 169 | "ALBUM": "Albums", 170 | "ARTIST": "Artists", 171 | "PLAYLIST": "Playlists" 172 | }, 173 | "EMPTY_INPUT": "Start your search...", 174 | "NO_RESULTS": "No results found..." 175 | }, 176 | "CONTEXT_MENU": { 177 | "SONG_LIST_ITEM": { 178 | "ADD_QUEUE": "Add to queue", 179 | "GO_ARTIST": "Go to artist", 180 | "GO_ALBUM": "Go to album", 181 | "VIEW_EXPLORER": "Show in File Explorer", 182 | "VIEW_FINDER": "Show in Finder", 183 | "VIEW_FILEMGR": "Show in File Manager", 184 | "ADD_FAVOURITE": "Add to favorites", 185 | "ADD_PLAYLIST": "Add to playlist", 186 | "REMOVE_FAVOURITE": "Remove from favorites", 187 | "REMOVE_PLAYLIST": "Remove from playlist", 188 | "DELETE": "Remove from library" 189 | }, 190 | "PLAYLIST_SIDE_ITEM": { 191 | "EDIT_PLAYLIST": "Edit playlist", 192 | "RENAME": "Rename", 193 | "REMOVE_PLAYLIST": "Remove playlist" 194 | } 195 | }, 196 | "APP_NAME": "Firetail", 197 | "LANG_NAME": "عربي", 198 | "RTL": true 199 | } --------------------------------------------------------------------------------