├── .eslintignore ├── src ├── react-app-env.d.ts ├── setupTests.js ├── App.test.js ├── reportWebVitals.js ├── lib │ ├── interfaces.component.tsx │ ├── theme.json │ └── icons.component.tsx ├── index.tsx ├── components │ ├── base.component.tsx │ ├── experimental.component.tsx │ ├── settings.component.tsx │ ├── navbar.component.tsx │ ├── preferences.component.tsx │ ├── download.component.tsx │ ├── sidebar.component.tsx │ ├── search.component.tsx │ ├── controls.component.tsx │ ├── app.component.tsx │ ├── home.component.tsx │ └── about.component.tsx ├── App.tsx ├── logo.svg └── App.css ├── src-tauri ├── src │ ├── build.rs │ └── main.rs ├── icons │ ├── 32x32.png │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── StoreLogo.png │ ├── Square30x30Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ └── Square310x310Logo.png ├── .gitignore ├── rustfmt.toml ├── Cargo.toml └── tauri.conf.json ├── public ├── google14f52748d0656c83.html ├── favicon.ico ├── logo192.png ├── logo512.png ├── robots.txt ├── manifest.json └── index.html ├── .prettierignore ├── .prettierrc.json ├── .vscode └── settings.json ├── Privacy.md ├── .gitignore ├── .eslintrc.yml ├── .github ├── pull_request_template.md ├── workflows │ ├── semgrep.yml │ ├── lint.yml │ ├── codeql-analysis.yml │ ├── tauri-test-build.yml │ └── tauri-publish.yml └── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml ├── tsconfig.json ├── .env.example ├── scripts ├── postbuild.js └── prebuild.js ├── LICENSE ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src-tauri/src/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /public/google14f52748d0656c83.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google14f52748d0656c83.html -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanleyowen/loofi/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanleyowen/loofi/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanleyowen/loofi/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Allow: / -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanleyowen/loofi/HEAD/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanleyowen/loofi/HEAD/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanleyowen/loofi/HEAD/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanleyowen/loofi/HEAD/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | node_modules 4 | *.min. 5 | ./src/lib/icons.component.tsx 6 | *.yml 7 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanleyowen/loofi/HEAD/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanleyowen/loofi/HEAD/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanleyowen/loofi/HEAD/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | WixTools 5 | -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanleyowen/loofi/HEAD/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanleyowen/loofi/HEAD/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanleyowen/loofi/HEAD/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanleyowen/loofi/HEAD/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true 6 | } -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanleyowen/loofi/HEAD/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanleyowen/loofi/HEAD/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanleyowen/loofi/HEAD/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanleyowen/loofi/HEAD/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stanleyowen/loofi/HEAD/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.format.enable": true, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": "explicit" 5 | }, 6 | "editor.defaultFormatter": "esbenp.prettier-vscode", 7 | "editor.formatOnSave": true 8 | } 9 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom' -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import App from './App' 3 | 4 | test('renders learn react link', () => { 5 | render() 6 | const linkElement = screen.getByText(/learn react/i) 7 | expect(linkElement).toBeInTheDocument() 8 | }) -------------------------------------------------------------------------------- /Privacy.md: -------------------------------------------------------------------------------- 1 | **Privacy Policy** 2 | 3 | *Personal Information Collection* 4 | 5 | Loofi does not collect, store, share or publish any personal information. 6 | 7 | *Non-Personal Information Collection* 8 | 9 | Loofi does not collect, store, share or publish any non-personal information. -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr( 2 | all(not(debug_assertions), target_os = "windows"), 3 | windows_subsystem = "windows" 4 | )] 5 | 6 | fn main() { 7 | tauri::Builder::default() 8 | .run(tauri::generate_context!()) 9 | .expect("error while running tauri application"); 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pnp 2 | .pnp.js 3 | coverage 4 | build 5 | .eslintcache 6 | debug.log 7 | .netlify 8 | 9 | .DS_Store 10 | .env 11 | .env.local 12 | .env.development 13 | .env.development.local 14 | .env.test.local 15 | .env.production 16 | .env.production.local 17 | 18 | App.min.css 19 | **/img/*[.png][.jpg] 20 | dist 21 | docs 22 | node_modules 23 | dist 24 | $HOME -------------------------------------------------------------------------------- /src-tauri/rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | hard_tabs = false 3 | tab_spaces = 2 4 | newline_style = "Auto" 5 | use_small_heuristics = "Default" 6 | reorder_imports = true 7 | reorder_modules = true 8 | remove_nested_parens = true 9 | edition = "2018" 10 | merge_derives = true 11 | use_try_shorthand = false 12 | use_field_init_shorthand = false 13 | force_explicit_abi = true 14 | imports_granularity = "Crate" 15 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry) 5 | getFID(onPerfEntry) 6 | getFCP(onPerfEntry) 7 | getLCP(onPerfEntry) 8 | getTTFB(onPerfEntry) 9 | }) 10 | } 11 | 12 | export default reportWebVitals -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | es2021: true 4 | extends: 5 | - 'eslint:recommended' 6 | - 'plugin:react/recommended' 7 | - 'plugin:@typescript-eslint/recommended' 8 | parser: '@typescript-eslint/parser' 9 | parserOptions: 10 | ecmaFeatures: 11 | jsx: true 12 | ecmaVersion: 12 13 | sourceType: module 14 | plugins: 15 | - 'react' 16 | - '@typescript-eslint' 17 | rules: {} 18 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **Resolved / Related Issues** 2 | Items resolved / related issues by this PR. 3 | - Closes #issue... 4 | - Related #issue... 5 | 6 | **Details of Changes** 7 | Add details of changes here. 8 | - Added... 9 | - Fixed... 10 | 11 | **Validation** 12 | How did you test these changes? 13 | - [ ] Built and ran the app 14 | - [ ] Tested the changes for accessibility 15 | 16 | **Screenshots (optional)** 17 | Add screenshots here. -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: {} 3 | push: 4 | branches: 5 | - main 6 | name: Semgrep 7 | jobs: 8 | semgrep: 9 | name: Scan 10 | runs-on: ubuntu-latest 11 | if: (github.actor != 'dependabot[bot]') 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: returntocorp/semgrep-action@v1 15 | with: 16 | auditOn: push 17 | publishToken: ${{ secrets.SEMGREP_APP_TOKEN }} 18 | publishDeployment: 1255 -------------------------------------------------------------------------------- /src/lib/interfaces.component.tsx: -------------------------------------------------------------------------------- 1 | export interface Properties { 2 | action: number; 3 | activeTab: string; 4 | history: [string]; 5 | } 6 | 7 | export interface Song { 8 | playing: boolean; 9 | title: string; 10 | author: string; 11 | image: string; 12 | audio: HTMLAudioElement; 13 | } 14 | 15 | export interface AppInterface { 16 | properties: Properties; 17 | handleChange: any; 18 | } 19 | 20 | export interface BaseLayout { 21 | song: Song; 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [14.x, 15.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: rm -rf node_modules && yarn install 24 | - run: yarn lint -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Loofi", 3 | "name": "Loofi | LoFi Streaming", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "app" 3 | version = "0.1.0" 4 | description = "A Tauri App" 5 | authors = ["you"] 6 | license = "" 7 | repository = "" 8 | default-run = "app" 9 | edition = "2018" 10 | build = "src/build.rs" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [build-dependencies] 15 | tauri-build = { version = "1.0.2", features = [] } 16 | 17 | [dependencies] 18 | serde_json = "1.0" 19 | serde = { version = "1.0", features = ["derive"] } 20 | tauri = { version = "1.0.2", features = ["api-all", "updater"] } 21 | 22 | [features] 23 | default = [ "custom-protocol" ] 24 | custom-protocol = [ "tauri/custom-protocol" ] 25 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */ 2 | 3 | import React from 'react'; 4 | import * as ReactDOMClient from 'react-dom/client'; 5 | import { StyledEngineProvider } from '@mui/material/styles'; 6 | 7 | import App from './App'; 8 | import reportWebVitals from './reportWebVitals'; 9 | 10 | ReactDOMClient.createRoot(document.getElementById('root')!).render( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | 18 | // If you want to start measuring performance in your app, pass a function 19 | // to log results (for example: reportWebVitals(console.log)) 20 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 21 | reportWebVitals(); 22 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Beta Section Configuration 2 | # Configure `REACT_APP_STABLE` and `REACT_APP_BETA` variables 3 | # only in case `REACT_APP_ALLOW_BETA` variable is set to true 4 | REACT_APP_ALLOW_BETA = false 5 | REACT_APP_STABLE = false 6 | REACT_APP_BETA = false 7 | 8 | # Firebase Configuration 9 | # For more information, visit https://firebase.google.com/docs/database/web/start#initialize_the_javascript_sdk 10 | REACT_APP_API_KEY = ****************** 11 | REACT_APP_AUTH_DOMAIN = ****************** 12 | REACT_APP_DB_URL = ****************** 13 | REACT_APP_PROJECT_ID = ****************** 14 | REACT_APP_STORAGE_BUCKET = ****************** 15 | REACT_APP_SENDER_ID = ****************** 16 | REACT_APP_ID = ****************** 17 | REACT_APP_MEASUREMENT_ID = ****************** -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: 9 | - cron: '15 7 * * 1' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ 'javascript' ] 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v2 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v1 31 | with: 32 | languages: ${{ matrix.language }} 33 | 34 | - name: Autobuild 35 | uses: github/codeql-action/autobuild@v1 36 | 37 | - name: Perform CodeQL Analysis 38 | uses: github/codeql-action/analyze@v1 -------------------------------------------------------------------------------- /scripts/postbuild.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const package = require('../package.json'); 3 | 4 | let data = package; 5 | 6 | Object.keys(data.devDependencies).map((dep) => { 7 | if ( 8 | dep.startsWith('@types/') || 9 | dep.startsWith('@typescript-eslint/') || 10 | dep.startsWith('clean') 11 | ) { 12 | data.dependencies = { 13 | ...data.dependencies, 14 | [dep]: data.devDependencies[dep], 15 | }; 16 | delete data.devDependencies[dep]; 17 | } 18 | }); 19 | 20 | data.dependencies = Object.keys(data.dependencies) 21 | .sort() 22 | .reduce((obj, key) => { 23 | obj[key] = data.dependencies[key]; 24 | return obj; 25 | }, {}); 26 | 27 | fs.writeFile('package.json', JSON.stringify(data, null, 2), (err) => { 28 | if (err) console.log(err); 29 | else console.log('Postbuild Process Completed'); 30 | }); 31 | -------------------------------------------------------------------------------- /scripts/prebuild.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const package = require('../package.json'); 3 | 4 | let data = package; 5 | 6 | Object.keys(data.dependencies).map((dep) => { 7 | if ( 8 | dep.startsWith('@types/') || 9 | dep.startsWith('@typescript-eslint/') || 10 | dep.startsWith('clean') 11 | ) { 12 | data.devDependencies = { 13 | ...data.devDependencies, 14 | [dep]: data.dependencies[dep], 15 | }; 16 | delete data.dependencies[dep]; 17 | } 18 | }); 19 | 20 | data.devDependencies = Object.keys(data.devDependencies) 21 | .sort() 22 | .reduce((obj, key) => { 23 | obj[key] = data.devDependencies[key]; 24 | return obj; 25 | }, {}); 26 | 27 | fs.writeFile('package.json', JSON.stringify(data, null, 2), (err) => { 28 | if (err) console.log(err); 29 | else console.log('Prebuild Process Completed'); 30 | }); 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Loofi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/lib/theme.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "Default", 4 | "image": false 5 | }, 6 | { 7 | "type": "Nature", 8 | "image": "https://user-images.githubusercontent.com/69080584/134890221-013b18f1-e051-4a69-9431-1d8259069ae7.png" 9 | }, 10 | { 11 | "type": "Sunset", 12 | "image": "https://user-images.githubusercontent.com/69080584/134889846-88916431-6f24-4a2b-bde7-f42c5d3a82a6.png" 13 | }, 14 | { 15 | "type": "Green Grass", 16 | "image": "https://user-images.githubusercontent.com/69080584/134890409-db19b5e3-8120-4a93-a82a-523af6824798.png" 17 | }, 18 | { 19 | "type": "Mountain", 20 | "image": "https://user-images.githubusercontent.com/69080584/134889338-20a409b3-9a5a-4172-91b6-b1ade6436285.png" 21 | }, 22 | { 23 | "type": "River", 24 | "image": "https://user-images.githubusercontent.com/69080584/134890896-1e4420ff-adbe-4f34-a1b4-19522e0ef460.jpg" 25 | }, 26 | { 27 | "type": "Dandelion", 28 | "image": "https://user-images.githubusercontent.com/69080584/134891156-985e1185-0faa-4bda-a3e3-bd6d7548ee8d.png" 29 | }, 30 | { 31 | "type": "Forest", 32 | "image": "https://user-images.githubusercontent.com/69080584/134891765-e06e90e8-c74b-4426-a65f-dffd6121f728.png" 33 | } 34 | ] -------------------------------------------------------------------------------- /src/components/base.component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Home from './home.component'; 4 | import Search from './search.component'; 5 | import Settings from './settings.component'; 6 | import Download from './download.component'; 7 | 8 | // eslint-disable-next-line 9 | const BaseLayout = ({ 10 | song, 11 | properties, 12 | songData, 13 | handleSong, 14 | updateAppToLatestVersion, 15 | }: any) => { 16 | return ( 17 |
18 | {properties.activeTab === 'home' ? ( 19 | 25 | ) : properties.activeTab === 'search' ? ( 26 | 32 | ) : properties.activeTab === 'download' ? ( 33 | 34 | ) : ( 35 | 36 | )} 37 |
38 | ); 39 | }; 40 | 41 | export default BaseLayout; 42 | -------------------------------------------------------------------------------- /src/components/experimental.component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Alert, AlertTitle, Switch } from '@mui/material'; 3 | 4 | import { SaveLocation, AutoPlay } from '../lib/icons.component'; 5 | 6 | // eslint-disable-next-line 7 | const Experimental = () => { 8 | return ( 9 |
10 | 14 | Experimental Features 15 | These experimental features are all still under active 16 | development and subject to non-backward compatible changes or 17 | removal in any future version. Use of these features are not 18 | recommended in production environments. 19 | 20 |
21 |
22 | 23 |
24 |

25 | Automatically play another music 26 |

27 |
28 |
29 | 30 |
31 |
32 | ); 33 | }; 34 | 35 | export default Experimental; 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report an issue to help us improve 3 | labels: [bug] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: Description 8 | description: A clear and concise description of what the bug is. 9 | validations: 10 | required: true 11 | - type: textarea 12 | attributes: 13 | label: Steps To Reproduce 14 | description: Steps to reproduce the behavior. 15 | placeholder: | 16 | 1. Go to '....' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See the error 20 | validations: 21 | required: false 22 | - type: textarea 23 | attributes: 24 | label: Expected behavior 25 | description: A clear and concise description of what you expected to happen. 26 | validations: 27 | required: true 28 | - type: input 29 | id: app-version 30 | attributes: 31 | label: Loofi Version 32 | description: What version of Loofi are you using? 33 | placeholder: v1.0.0 34 | validations: 35 | required: true 36 | - type: input 37 | attributes: 38 | label: Operating System Version 39 | description: What operating system version are you using? On Windows, click Start button > Settings > System > About. On macOS, click the Apple Menu > About This Mac. On Linux, use lsb_release or uname -a. 40 | placeholder: e.g. Windows 10 21H1, macOS Catalina 10.15.7, or Ubuntu 20.04 41 | validations: 42 | required: true 43 | - type: textarea 44 | attributes: 45 | label: Additional Information 46 | description: | 47 | A list of assets (e.g. screenshots) relevant to this bug. 48 | 49 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from 'react'; 2 | import { BrowserRouter as Router } from 'react-router-dom'; 3 | 4 | import { Properties } from './lib/interfaces.component'; 5 | import AppLayout from './components/app.component'; 6 | import SideBar from './components/sidebar.component'; 7 | import './App.css'; 8 | 9 | // eslint-disable-next-line 10 | export default function App() { 11 | const [properties, setProperties] = useState({ 12 | action: 0, 13 | activeTab: window.localStorage.getItem('tab-session') ?? 'home', 14 | history: [window.localStorage.getItem('tab-session') ?? 'home'], 15 | }); 16 | 17 | useEffect(() => { 18 | for (let i = 0; i < 79; i++) { 19 | const div = document.createElement('div'); 20 | div.style.opacity = `${Math.random() * (0.075 - 0.025) + 0.025}`; 21 | document.querySelector('.backdrop-overlay')?.appendChild(div); 22 | } 23 | }, []); 24 | 25 | const handleChange = useCallback( 26 | (a: any) => { 27 | if (a.goForward || a.goBackward) 28 | setProperties({ 29 | ...properties, 30 | action: a.goBackward 31 | ? properties.action - 1 32 | : properties.action + 1, 33 | [a.id]: a.value, 34 | }); 35 | else { 36 | properties.history.splice( 37 | properties.action + 1, 38 | properties.history.length - (properties.action + 1), 39 | a.value 40 | ); 41 | setProperties({ 42 | ...properties, 43 | action: properties.action + 1, 44 | [a.id]: a.value, 45 | }); 46 | } 47 | window.localStorage.setItem('tab-session', a.value); 48 | }, 49 | [properties] 50 | ); 51 | 52 | return ( 53 | 54 | 55 | 56 | 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loofi", 3 | "version": "1.1.2", 4 | "private": true, 5 | "author": "Stanley Owen ", 6 | "dependencies": { 7 | "@emotion/react": "^11.10.6", 8 | "@emotion/styled": "^11.10.6", 9 | "@mui/lab": "^5.0.0-alpha.128", 10 | "@mui/material": "^5.12.2", 11 | "@types/node": "^18.16.1", 12 | "@types/react": "^18.2.0", 13 | "@types/react-dom": "^18.2.1", 14 | "@types/react-router-dom": "^5.3.3", 15 | "@typescript-eslint/eslint-plugin": "^5.59.1", 16 | "@typescript-eslint/parser": "^5.59.1", 17 | "axios": "^1.3.6", 18 | "firebase": "^9.20.0", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "react-router-dom": "^6.10.0", 22 | "react-scripts": "^5.0.1", 23 | "web-vitals": "^3.3.1" 24 | }, 25 | "homepage": "./", 26 | "scripts": { 27 | "tauri": "tauri", 28 | "start": "react-scripts start", 29 | "dev": "yarn tauri dev", 30 | "build": "yarn cross-env GENERATE_SOURCEMAP=false CI='' react-scripts build", 31 | "test": "react-scripts test", 32 | "eject": "react-scripts eject", 33 | "release": "yarn build && electron-builder --publish=always", 34 | "lint": "eslint -c .eslintrc.yml --ext .tsx ./src" 35 | }, 36 | "build": { 37 | "win": { 38 | "target": [ 39 | { 40 | "target": "nsis" 41 | } 42 | ] 43 | }, 44 | "linux": { 45 | "target": [ 46 | "AppImage", 47 | "deb" 48 | ] 49 | } 50 | }, 51 | "eslintConfig": { 52 | "extends": [ 53 | "react-app", 54 | "react-app/jest" 55 | ] 56 | }, 57 | "browserslist": { 58 | "production": [ 59 | ">0.2%", 60 | "not dead", 61 | "not op_mini all" 62 | ], 63 | "development": [ 64 | "last 1 chrome version", 65 | "last 1 firefox version", 66 | "last 1 safari version" 67 | ] 68 | }, 69 | "devDependencies": { 70 | "@tauri-apps/api": "^1.2.0", 71 | "@tauri-apps/cli": "^1.2.3", 72 | "@testing-library/jest-dom": "^5.16.5", 73 | "@testing-library/react": "^14.0.0", 74 | "@testing-library/user-event": "^14.4.3", 75 | "cross-env": "^7.0.3", 76 | "dotenv": "^16.0.3", 77 | "eslint": "^8.39.0", 78 | "eslint-plugin-react": "^7.32.2", 79 | "typescript": "^5.0.4" 80 | } 81 | } -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "package": { 3 | "productName": "Loofi Desktop", 4 | "version": "1.1.2" 5 | }, 6 | "build": { 7 | "distDir": "../build", 8 | "devPath": "http://localhost:3000", 9 | "beforeDevCommand": "cross-env BROWSER=none && yarn start", 10 | "beforeBuildCommand": "node ./scripts/prebuild.js && yarn build && node ./scripts/postbuild.js", 11 | "withGlobalTauri": true 12 | }, 13 | "tauri": { 14 | "bundle": { 15 | "active": true, 16 | "targets": "all", 17 | "identifier": "com.tauri.loofi", 18 | "icon": [ 19 | "icons/32x32.png", 20 | "icons/128x128.png", 21 | "icons/128x128@2x.png", 22 | "icons/icon.icns", 23 | "icons/icon.ico" 24 | ], 25 | "resources": [], 26 | "externalBin": [], 27 | "copyright": "MIT", 28 | "category": "DeveloperTool", 29 | "shortDescription": "", 30 | "longDescription": "", 31 | "deb": { 32 | "depends": [] 33 | }, 34 | "macOS": { 35 | "frameworks": [], 36 | "minimumSystemVersion": "", 37 | "exceptionDomain": "", 38 | "signingIdentity": null, 39 | "entitlements": null, 40 | "license": "../LICENSE" 41 | }, 42 | "windows": { 43 | "certificateThumbprint": null, 44 | "digestAlgorithm": "sha256", 45 | "timestampUrl": "" 46 | } 47 | }, 48 | "updater": { 49 | "active": true, 50 | "endpoints": [ 51 | "https://loofi-updater.onrender.com/updater/{{target}}/{{arch}}/{{current_version}}" 52 | ], 53 | "dialog": false, 54 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEZBQTc1M0JBRDk2MzcwNjUKUldSbGNHUFp1bE9uK2o4U3Z4Y0pkais4OUtIZ2ZjTVVDZXpvMm9Vc2FuTysvdTJvSmRPV3daeTMK", 55 | "windows": { 56 | "installMode": "passive" 57 | } 58 | }, 59 | "allowlist": { 60 | "all": true 61 | }, 62 | "windows": [ 63 | { 64 | "title": "Loofi | LoFi Streaming", 65 | "width": 800, 66 | "height": 600, 67 | "resizable": true, 68 | "fullscreen": false 69 | } 70 | ], 71 | "security": { 72 | "csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'; script-src *" 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🎶 Loofi 2 | 3 | ![Loofi](https://user-images.githubusercontent.com/69080584/233798174-acdad5e8-fe93-414d-95a1-db321ab5323c.png) 4 | 5 |
6 | 7 | [![Version](https://img.shields.io/github/package-json/v/stanleyowen/loofi/master?color=61dafb&label=version)](https://github.com/stanleyowen/loofi/releases) 8 | [![GitHub Forks](https://img.shields.io/github/forks/stanleyowen/loofi?color=61dafb)](https://github.com/stanleyowen/loofi/network) 9 | [![Github Stars](https://img.shields.io/github/stars/stanleyowen/loofi?color=61dafb)](https://github.com/stanleyowen/loofi/stargazers) 10 | [![MIT License](https://img.shields.io/github/license/stanleyowen/loofi?color=61dafb)](https://github.com/stanleyowen/loofi/blob/master/LICENSE) 11 | 12 | [![CodeQL](https://github.com/stanleyowen/loofi/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/stanleyowen/loofi/actions/workflows/codeql-analysis.yml) 13 | [![Semgrep](https://github.com/stanleyowen/loofi/actions/workflows/semgrep.yml/badge.svg)](https://github.com/stanleyowen/loofi/actions/workflows/semgrep.yml) 14 | [![Tauri Test Build](https://github.com/stanleyowen/loofi/actions/workflows/tauri-test-build.yml/badge.svg)](https://github.com/stanleyowen/loofi/actions/workflows/tauri-test-build.yml) 15 | [![Netlify Status](https://api.netlify.com/api/v1/badges/4ce8d1c2-6e6d-482f-93e3-b1c824c14944/deploy-status)](https://app.netlify.com/sites/loofi/deploys) 16 | [![Tauri Publish](https://github.com/stanleyowen/loofi/actions/workflows/tauri-publish.yml/badge.svg)](https://github.com/stanleyowen/loofi/actions/workflows/tauri-publish.yml) 17 | 18 | [![Windows Support](https://shields.io/badge/Windows--9cf?logo=Windows&style=social)](https://github.com/stanleyowen/loofi/releases) 19 | [![macOS Support](https://shields.io/badge/MacOS--9cf?logo=Apple&style=social)](https://github.com/stanleyowen/loofi/releases) 20 | [![Linux Support](https://img.shields.io/badge/-Linux-grey?logo=linux)](https://github.com/stanleyowen/loofi/releases) 21 | 22 |
23 |
24 | 25 | Loofi is an **open source** LoFi Streaming of application built with **Typescript, Electron, React, Firebase**. 26 | 27 | If you find this project useful, leave a 🌟 to keep a beginner motivated 😊. 28 | 29 | ## Stargazers and Contributors 30 | 31 | [![Stargazers for @stanleyowen/loofi](https://reporoster.com/stars/dark/stanleyowen/loofi)](https://github.com/stanleyowen/loofi/stargazers) 32 | [![Forkers for @stanleyowen/loofi](https://reporoster.com/forks/dark/stanleyowen/loofi)](https://github.com/stanleyowen/loofi/network/members) 33 | 34 | ## LICENSE 35 | 36 | [MIT](LICENSE) 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for LoFi Player 3 | labels: [enhancement] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: Problem Description 8 | description: Please add a clear and concise description of the problem you are seeking to solve with this feature request. 9 | - type: textarea 10 | attributes: 11 | label: Solution/Idea 12 | description: Describe the solution you'd like in a clear and concise manner. 13 | validations: 14 | required: true 15 | - type: textarea 16 | attributes: 17 | label: Alternatives 18 | description: A clear and concise description of any alternative solutions or features you've considered. 19 | validations: 20 | required: true 21 | - type: markdown 22 | attributes: 23 | value: | 24 | --- 25 | 26 | Please include a list of what the feature should and shouldn't do by filling in the table below. 27 | 28 | 'Must' implies that the feature should not ship without this capability. 29 | 'Should' is something we should push hard for, but is not absolutely required to ship. 30 | 'Could' is a nice-to-have; a good stretch goal that isn't painful if we don't achieve it. 31 | 'Won't' is a clear statement that the proposal/feature will intentionally not have that capability. 32 | 33 | This list will evolve and grow as the proposal becomes more refined over time. 34 | A good rule of thumb is to start your proposal with no more than 7 high-level requirements. 35 | - type: textarea 36 | attributes: 37 | label: Priorities 38 | description: Describe all the elements of the idea and how important they are. 39 | value: | 40 | | Capability | Priority | 41 | | :---------- | :------- | 42 | | This proposal will allow developers to accomplish W | Must | 43 | | This proposal will allow end users to accomplish X | Should | 44 | | This proposal will allow developers to accomplish Y | Could | 45 | | This proposal will allow end users to accomplish Z | Won't | 46 | - type: input 47 | id: app-version 48 | attributes: 49 | label: LoFi Player Version 50 | description: What version of LoFi Player are you using? 51 | placeholder: v1.0.0 52 | validations: 53 | required: true 54 | - type: input 55 | attributes: 56 | label: Operating System Version 57 | description: What operating system version are you using? On Windows, click Start button > Settings > System > About. On macOS, click the Apple Menu > About This Mac. On Linux, use lsb_release or uname -a. 58 | placeholder: e.g. Windows 10 21H1, macOS Catalina 10.15.7, or Ubuntu 20.04 59 | validations: 60 | required: true 61 | - type: textarea 62 | attributes: 63 | label: Additional comment 64 | description: Add any other comment or screenshots about the feature request here. -------------------------------------------------------------------------------- /.github/workflows/tauri-test-build.yml: -------------------------------------------------------------------------------- 1 | name: 'Tauri Test Build' 2 | on: [pull_request] 3 | 4 | jobs: 5 | test-tauri: 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | platform: [macos-latest, ubuntu-latest, windows-latest] 10 | 11 | runs-on: ${{ matrix.platform }} 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v4 15 | 16 | - name: install dependencies (ubuntu only) 17 | if: matrix.platform == 'ubuntu-latest' 18 | run: | 19 | sudo apt-get update 20 | sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libsoup2.4-dev libjavascriptcoregtk-4.1-dev 21 | sudo ln -sf /usr/lib/x86_64-linux-gnu/pkgconfig/webkit2gtk-4.1.pc /usr/lib/x86_64-linux-gnu/pkgconfig/webkit2gtk-4.0.pc 22 | sudo ln -sf /usr/lib/x86_64-linux-gnu/pkgconfig/javascriptcoregtk-4.1.pc /usr/lib/x86_64-linux-gnu/pkgconfig/javascriptcoregtk-4.0.pc 23 | sudo ln -sf /usr/include/webkitgtk-4.1 /usr/include/webkitgtk-4.0 24 | sudo ln -sf /usr/lib/x86_64-linux-gnu/libwebkit2gtk-4.1.so /usr/lib/x86_64-linux-gnu/libwebkit2gtk-4.0.so 25 | sudo ln -sf /usr/lib/x86_64-linux-gnu/libjavascriptcoregtk-4.1.so /usr/lib/x86_64-linux-gnu/libjavascriptcoregtk-4.0.so 26 | 27 | - name: Rust setup 28 | uses: dtolnay/rust-toolchain@stable 29 | with: 30 | # Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds. 31 | targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} 32 | toolchain: 1.79.0 33 | 34 | - name: Rust cache 35 | uses: swatinem/rust-cache@v2 36 | with: 37 | workspaces: './src-tauri -> target' 38 | 39 | - name: Sync node version and setup node 40 | uses: actions/setup-node@v4 41 | with: 42 | node-version: 'lts/*' 43 | cache: 'yarn' 44 | 45 | - name: Install frontend dependencies 46 | run: yarn install 47 | 48 | - name: Building tauri app 49 | uses: tauri-apps/tauri-action@v0 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} 53 | TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} 54 | REACT_APP_ALLOW_BETA: false 55 | REACT_APP_API_KEY: ${{ secrets.REACT_APP_API_KEY }} 56 | REACT_APP_AUTH_DOMAIN: ${{ secrets.REACT_APP_AUTH_DOMAIN }} 57 | REACT_APP_DB_URL: ${{ secrets.REACT_APP_DB_URL }} 58 | REACT_APP_PROJECT_ID: ${{ secrets.REACT_APP_PROJECT_ID }} 59 | REACT_APP_STORAGE_BUCKET: ${{ secrets.REACT_APP_STORAGE_BUCKET }} 60 | REACT_APP_SENDER_ID: ${{ secrets.REACT_APP_SENDER_ID }} 61 | REACT_APP_ID: ${{ secrets.REACT_APP_ID }} 62 | REACT_APP_MEASUREMENT_ID: ${{ secrets.REACT_APP_MEASUREMENT_ID }} -------------------------------------------------------------------------------- /src/components/settings.component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Tab, Tabs } from '@mui/material'; 3 | 4 | import About from './about.component'; 5 | import Preferences from './preferences.component'; 6 | import Experimental from './experimental.component'; 7 | import { 8 | AboutSolid, 9 | AboutOutline, 10 | PreferencesSolid, 11 | PreferencesOutline, 12 | ExperimentalSolid, 13 | ExperimentalOutline, 14 | } from '../lib/icons.component'; 15 | 16 | interface TabPanelProps { 17 | children?: React.ReactNode; 18 | index: number; 19 | value: number; 20 | } 21 | function TabPanel(props: TabPanelProps) { 22 | const { children, value, index, ...other } = props; 23 | return ( 24 | 33 | ); 34 | } 35 | 36 | // eslint-disable-next-line 37 | const Settings = ({ updateAppToLatestVersion }: any) => { 38 | const [tabIndex, setTabIndex] = useState(0); 39 | 40 | return ( 41 |
42 | setTabIndex(index)} 46 | className="w-20 tab" 47 | > 48 | {['Preferences', 'Experimental', 'About'].map((tab, index) => { 49 | const component: { [key: string]: any } = { 50 | AboutSolid, 51 | AboutOutline, 52 | PreferencesSolid, 53 | PreferencesOutline, 54 | ExperimentalSolid, 55 | ExperimentalOutline, 56 | }; 57 | const SolidIcon = component[`${tab}Solid`]; 58 | const OutlineIcon = component[`${tab}Outline`]; 59 | return ( 60 | 64 | ) : ( 65 | 66 | ) 67 | } 68 | key={index} 69 | label={window.innerWidth > 850 ? tab : ''} 70 | iconPosition="start" 71 | /> 72 | ); 73 | })} 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 |
85 | ); 86 | }; 87 | 88 | export default Settings; 89 | -------------------------------------------------------------------------------- /.github/workflows/tauri-publish.yml: -------------------------------------------------------------------------------- 1 | name: 'Tauri Publish' 2 | on: 3 | push: 4 | branches: [main] 5 | 6 | jobs: 7 | publish-tauri: 8 | permissions: 9 | contents: write 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | platform: [macos-latest, ubuntu-latest, windows-latest] 14 | 15 | runs-on: ${{ matrix.platform }} 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | 20 | - name: install dependencies (ubuntu only) 21 | if: matrix.platform == 'ubuntu-latest' 22 | run: | 23 | sudo apt-get update 24 | sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libsoup2.4-dev libjavascriptcoregtk-4.1-dev 25 | sudo ln -sf /usr/lib/x86_64-linux-gnu/pkgconfig/webkit2gtk-4.1.pc /usr/lib/x86_64-linux-gnu/pkgconfig/webkit2gtk-4.0.pc 26 | sudo ln -sf /usr/lib/x86_64-linux-gnu/pkgconfig/javascriptcoregtk-4.1.pc /usr/lib/x86_64-linux-gnu/pkgconfig/javascriptcoregtk-4.0.pc 27 | sudo ln -sf /usr/include/webkitgtk-4.1 /usr/include/webkitgtk-4.0 28 | sudo ln -sf /usr/lib/x86_64-linux-gnu/libwebkit2gtk-4.1.so /usr/lib/x86_64-linux-gnu/libwebkit2gtk-4.0.so 29 | sudo ln -sf /usr/lib/x86_64-linux-gnu/libjavascriptcoregtk-4.1.so /usr/lib/x86_64-linux-gnu/libjavascriptcoregtk-4.0.so 30 | 31 | - name: Rust setup 32 | uses: dtolnay/rust-toolchain@stable 33 | with: 34 | # Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds. 35 | targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} 36 | toolchain: 1.79.0 37 | 38 | - name: Rust cache 39 | uses: swatinem/rust-cache@v2 40 | with: 41 | workspaces: './src-tauri -> target' 42 | 43 | - name: Sync node version and setup node 44 | uses: actions/setup-node@v4 45 | with: 46 | node-version: 'lts/*' 47 | cache: 'yarn' 48 | 49 | - name: Install frontend dependencies 50 | run: yarn install 51 | 52 | - name: Building tauri app 53 | uses: tauri-apps/tauri-action@v0 54 | env: 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} 57 | TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} 58 | REACT_APP_ALLOW_BETA: false 59 | REACT_APP_API_KEY: ${{ secrets.REACT_APP_API_KEY }} 60 | REACT_APP_AUTH_DOMAIN: ${{ secrets.REACT_APP_AUTH_DOMAIN }} 61 | REACT_APP_DB_URL: ${{ secrets.REACT_APP_DB_URL }} 62 | REACT_APP_PROJECT_ID: ${{ secrets.REACT_APP_PROJECT_ID }} 63 | REACT_APP_STORAGE_BUCKET: ${{ secrets.REACT_APP_STORAGE_BUCKET }} 64 | REACT_APP_SENDER_ID: ${{ secrets.REACT_APP_SENDER_ID }} 65 | REACT_APP_ID: ${{ secrets.REACT_APP_ID }} 66 | REACT_APP_MEASUREMENT_ID: ${{ secrets.REACT_APP_MEASUREMENT_ID }} 67 | with: 68 | tagName: v__VERSION__ 69 | releaseName: 'Loofi v__VERSION__' 70 | releaseBody: 'See the assets to download this version and install.' 71 | releaseDraft: true 72 | prerelease: false -------------------------------------------------------------------------------- /src/components/navbar.component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Tooltip, IconButton } from '@mui/material'; 3 | import { ChevronLeft, ChevronRight, GitHub } from '../lib/icons.component'; 4 | 5 | // eslint-disable-next-line 6 | const Navbar = ({ properties, handleChange }: any) => { 7 | const [property, setProperty] = useState({ 8 | disablePrevious: true, 9 | disableForward: true, 10 | }); 11 | 12 | useEffect(() => { 13 | setProperty({ 14 | disablePrevious: 15 | properties.history?.length > 1 && properties.action > 0 16 | ? false 17 | : true, 18 | disableForward: 19 | properties.action + 1 < properties.history?.length 20 | ? false 21 | : true, 22 | }); 23 | }, [properties]); 24 | 25 | const triggerAction = (type: 'next' | 'previous') => { 26 | if (type === 'previous') 27 | handleChange({ 28 | id: 'activeTab', 29 | value: properties.history[properties.action - 1], 30 | goBackward: true, 31 | }); 32 | else 33 | handleChange({ 34 | id: 'activeTab', 35 | value: properties.history[properties.action + 1], 36 | goForward: true, 37 | }); 38 | }; 39 | 40 | return ( 41 |
42 |
43 | 44 |
45 | triggerAction('previous')} 47 | disabled={property.disablePrevious} 48 | > 49 | 50 | 51 |
52 |
53 |
54 |
55 | 60 |
61 | triggerAction('next')} 63 | disabled={property.disableForward} 64 | > 65 | 66 | 67 |
68 |
69 |
70 |
71 | 76 | 81 | 82 | 83 | 84 |
85 |
86 | ); 87 | }; 88 | 89 | export default Navbar; 90 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 16 | 17 | 18 | Loofi | LoFi Streaming 19 | 20 | 21 | 75 | 76 |
77 |
78 |
79 |
80 |
81 | 82 |
83 | 84 | 85 | -------------------------------------------------------------------------------- /src/components/preferences.component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Switch, Button, Accordion, AccordionSummary } from '@mui/material'; 3 | 4 | import Theme from '../lib/theme.json'; 5 | import { 6 | Themes, 7 | Expand, 8 | ThemesApp, 9 | SaveLocation, 10 | } from '../lib/icons.component'; 11 | 12 | // eslint-disable-next-line 13 | const About = () => { 14 | const [activeTab, setActiveTab] = useState( 15 | JSON.parse(localStorage.getItem('theme-session') || `{}`).type 16 | ); 17 | const [continuePreviousSession, setContinuePreviousSession] = 18 | React.useState( 19 | localStorage.getItem('continue-previous-session') === 'true' 20 | ? true 21 | : false 22 | ); 23 | function setContinuePreviousState(e: boolean) { 24 | setContinuePreviousSession(e); 25 | localStorage.setItem('continue-previous-session', e.toString()); 26 | } 27 | 28 | const setTheme = (type: string, url: string | boolean) => { 29 | setActiveTab(type); 30 | const background = document.getElementById('backdrop-image'); 31 | if (url && background) background.style.background = `url(${url})`; 32 | else background?.removeAttribute('style'); 33 | localStorage.setItem('theme-session', JSON.stringify({ type, url })); 34 | }; 35 | 36 | useEffect(() => { 37 | document.getElementById('themes')?.childNodes.forEach((tab) => { 38 | const childId = tab.textContent?.toLowerCase(); 39 | if (childId && activeTab) { 40 | if (activeTab.toLowerCase() === childId) 41 | document.getElementById(childId)?.classList.add('active'); 42 | else 43 | document 44 | .getElementById(childId) 45 | ?.classList.remove('active'); 46 | } 47 | }); 48 | }, [activeTab]); 49 | 50 | return ( 51 |
52 | 53 | }> 54 |
55 | 56 |

Themes

57 |
58 |
59 |
60 | {Theme.map((theme) => { 61 | return ( 62 | 77 | ); 78 | })} 79 |
80 |
81 |
82 |
83 | 84 |
85 |

Continue where you left off

86 |
87 |
88 | setContinuePreviousState(e.target.checked)} 92 | /> 93 |
94 |
95 | ); 96 | }; 97 | 98 | export default About; 99 | -------------------------------------------------------------------------------- /src/components/download.component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Alert, AlertTitle, Button, Tooltip } from '@mui/material'; 3 | import { Windows, MacOS, Linux, ExternalLink } from '../lib/icons.component'; 4 | import axios from 'axios'; 5 | 6 | const About = () => { 7 | const [properties, setProperties] = useState({ 8 | isLoading: true, 9 | downloadURL: {}, 10 | }); 11 | 12 | useEffect(() => { 13 | axios.get('https://loofi-updater.onrender.com/latest').then((res) => { 14 | setProperties({ 15 | isLoading: false, 16 | downloadURL: res.data, 17 | }); 18 | }); 19 | }, []); 20 | 21 | return ( 22 |
23 | 27 | INFO 28 | Currently, Loofi doesn't support 32-bit{' '} 29 | systems. If you are running a 32-bit system, you 30 | can still use it Loofi via Web. By default, Loofi installation 31 | are on Intel x86_64 architecture. To install Loofi 32 | on ARM architecture, please refer to source code. 33 | 34 |
35 |
36 |
37 | 38 |

Windows

39 |

40 | {properties?.downloadURL['windows-x86_64']?.msi 41 | ?.split('/') 42 | ?.pop()} 43 |

44 | 57 |
58 |
59 |
60 |
61 | 62 |

macOS

63 |

64 | {properties?.downloadURL['darwin-x86_64']?.dmg 65 | ?.split('/') 66 | ?.pop()} 67 |

68 | 81 |
82 |
83 |
84 |
85 | 86 |

Linux

87 |

88 | {properties?.downloadURL['linux-x86_64']?.appImage 89 | ?.split('/') 90 | ?.pop()} 91 |

92 | 105 |
106 |
107 | 123 |
124 |
125 | ); 126 | }; 127 | 128 | export default About; 129 | -------------------------------------------------------------------------------- /src/components/sidebar.component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { 3 | Button, 4 | Dialog, 5 | Tooltip, 6 | DialogActions, 7 | DialogContent, 8 | DialogContentText, 9 | } from '@mui/material'; 10 | import { 11 | ExperimentalOutline, 12 | Download, 13 | HomeSolid, 14 | HomeOutline, 15 | SettingsSolid, 16 | SettingsOutline, 17 | SearchSolid, 18 | SearchOutline, 19 | } from '../lib/icons.component'; 20 | 21 | // eslint-disable-next-line 22 | const SideBar = ({ handleChange, properties }: any) => { 23 | const [isOpen, setDialog] = useState(false); 24 | const list = ['Home', 'Search', 'Settings']; 25 | 26 | if (!window.__TAURI_METADATA__) list.splice(list.length - 1, 0, 'Download'); 27 | 28 | useEffect(() => { 29 | document 30 | .getElementById('tabs') 31 | ?.childNodes.forEach((tab) => 32 | (tab.childNodes[1] as HTMLElement).innerText.toLowerCase() === 33 | properties.activeTab 34 | ? (tab as HTMLElement).classList.add('active') 35 | : (tab as HTMLElement).classList.remove('active') 36 | ); 37 | }, [properties]); 38 | 39 | const switchTab = (target: string) => { 40 | if (target !== properties.activeTab) 41 | handleChange({ id: 'activeTab', value: target }); 42 | }; 43 | 44 | return ( 45 |
46 |
47 | {list.map((tab, index) => { 48 | const components: { [key: string]: any } = { 49 | Download, 50 | HomeSolid, 51 | HomeOutline, 52 | SearchSolid, 53 | SearchOutline, 54 | SettingsSolid, 55 | SettingsOutline, 56 | }; 57 | const SolidIcon = components[`${tab}Solid`]; 58 | const OutlineIcon = components[`${tab}Outline`]; 59 | return ( 60 | switchTab(tab.toLowerCase())} 66 | > 67 | 85 | 86 | ); 87 | })} 88 | {process.env.REACT_APP_ALLOW_BETA === 'true' && 89 | !window.__TAURI_METADATA__ ? ( 90 | process.env.REACT_APP_CONTEXT === 'production' ? ( 91 | 101 | ) : ( 102 | 116 | ) 117 | ) : null} 118 |
119 | 120 | setDialog(false)}> 121 | 122 | 123 |

Stability: Experimental

124 |

125 | This is an experimental feature which is still under 126 | active development and subject to non-backward 127 | compatible changes or removal in any future version. 128 | Use of the feature is not recommended in production 129 | environments. 130 |

131 |
132 |
133 | 134 | 135 | 145 | 146 |
147 |
148 | ); 149 | }; 150 | 151 | export default SideBar; 152 | -------------------------------------------------------------------------------- /src/components/search.component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Close } from '../lib/icons.component'; 3 | import { Skeleton, TextField, IconButton } from '@mui/material'; 4 | 5 | const Search = ({ song, songData, handleSong }: any) => { 6 | const items: any = { 7 | music: [], 8 | author: [], 9 | }; 10 | const [results, setResult] = useState({ 11 | music: [], 12 | author: [], 13 | }); 14 | const [keyword, setKeyword] = useState(''); 15 | const [isFetching, setFetching] = useState(false); 16 | 17 | const triggerAudio = ( 18 | e: React.MouseEvent, 19 | data: any 20 | ) => { 21 | e.preventDefault(); 22 | if (song.playing) { 23 | handleSong({ id: 'playing', value: false }); 24 | setTimeout(() => handleSong(data), 1); 25 | } else handleSong(data); 26 | (e.target as Element).classList.toggle('pause'); 27 | }; 28 | 29 | useEffect(() => { 30 | const btn = document.getElementById( 31 | (song.title + song.author).replace(/\s/g, '-') 32 | ); 33 | song.playing 34 | ? btn?.classList.add('pause') 35 | : btn?.classList.remove('pause'); 36 | }, [song]); 37 | 38 | useEffect(() => { 39 | if (keyword) { 40 | setFetching(true); 41 | const music: any = []; 42 | const author: any = []; 43 | for (let i = 0; i < songData.length; i++) { 44 | if (String(songData[i].title).toLowerCase().includes(keyword)) 45 | music.push(songData[i]); 46 | if (String(songData[i].author).toLowerCase().includes(keyword)) 47 | author.push(songData[i]); 48 | } 49 | setResult({ music, author }); 50 | setFetching(false); 51 | } 52 | }, [keyword, songData]); 53 | 54 | if (keyword) { 55 | if (isFetching) 56 | for (let i = 0; i < 4; i++) { 57 | items.music.push( 58 |
59 |
60 | 65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 |
73 |
74 |
75 | ); 76 | } 77 | else if (results?.music?.length > 0) 78 | results.music.map((music: any, index: any) => { 79 | return items.music.push( 80 |
81 |
82 | 83 |
84 |
85 |

{music.title}

86 |

{music.author}

87 |
88 | 96 |
97 |
98 |
99 | ); 100 | }); 101 | if (results?.author?.length > 0) 102 | results.author.map((music: any, index: any) => { 103 | return items.author.push( 104 |
105 |
106 | 107 |
108 |
109 |

{music.title}

110 |

{music.author}

111 |
112 | 120 |
121 |
122 |
123 | ); 124 | }); 125 | } 126 | 127 | const ClearQuery = (e: React.MouseEvent) => { 128 | e.preventDefault(); 129 | setKeyword(''); 130 | document.getElementById('search-query')?.focus(); 131 | }; 132 | 133 | const CloseButton = () => ( 134 | 139 | 140 | 141 | ); 142 | 143 | return ( 144 |
145 |
146 | setKeyword(e.target.value)} 152 | id="search-query" 153 | autoFocus 154 | autoComplete="off" 155 | InputProps={{ 156 | endAdornment: , 157 | }} 158 | /> 159 |
160 | {items.music.length !== 0 ? ( 161 |
162 |
163 | Songs 164 |
165 |
166 | {items.music} 167 |
168 |
169 | ) : null} 170 | {items.author.length !== 0 ? ( 171 |
172 |
173 | Artists 174 |
175 |
176 | {items.author} 177 |
178 |
179 | ) : null} 180 | {keyword && 181 | !isFetching && 182 | results.music?.length === 0 && 183 | results.author?.length === 0 ? ( 184 |
185 | No Results Found for {keyword} 186 |
187 | ) : null} 188 |
189 | ); 190 | }; 191 | 192 | export default Search; 193 | -------------------------------------------------------------------------------- /src/components/controls.component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Slider, IconButton, Button } from '@mui/material'; 3 | import { 4 | Play, 5 | Pause, 6 | Audio, 7 | MutedAudio, 8 | SkipNext, 9 | SkipPrevious, 10 | } from '../lib/icons.component'; 11 | 12 | interface Queue { 13 | queue: []; 14 | currentIndex: number; 15 | } 16 | 17 | interface Property { 18 | duration: number; 19 | progress: number; 20 | volume: number; 21 | muted: boolean; 22 | } 23 | 24 | // eslint-disable-next-line 25 | const Controls = ({ song, songData, handleSong }: any) => { 26 | const [queue, setQueue] = useState({ 27 | queue: [], 28 | currentIndex: 0, 29 | }); 30 | const [property, setProperty] = useState({ 31 | duration: 0, 32 | progress: 0, 33 | volume: Number(localStorage.getItem('volume')) ?? 50, 34 | muted: JSON.parse(String(localStorage.getItem('muted')) ?? false), 35 | }); 36 | const handleQueue = (a: string, b: any) => setQueue({ ...queue, [a]: b }); 37 | const handleChange = (a: string, b: any) => 38 | setProperty({ ...property, [a]: b }); 39 | 40 | useEffect(() => { 41 | localStorage.setItem('muted', String(property.muted)); 42 | localStorage.setItem('volume', String(property.volume)); 43 | song.audio.volume = property.muted ? 0 : property.volume / 100; 44 | }, [song.audio, property.volume, property.muted]); 45 | 46 | useEffect(() => { 47 | // async function shuffle(rawData: any) { 48 | // let index = songData.length 49 | // let randIndex 50 | // while(index !== 0) { 51 | // randIndex = Math.floor(Math.random() * index) 52 | // index-- 53 | // // [rawData[index], rawData[randIndex]] = [rawData[randIndex], rawData[index]] 54 | // } 55 | 56 | // } 57 | if (songData?.length > 0) handleQueue('queue', songData); 58 | }, [songData]); // eslint-disable-line 59 | 60 | useEffect(() => { 61 | song.playing ? song.audio.play() : song.audio.pause(); 62 | }, [song]); 63 | 64 | const skipAudio = (index: number, type: 'next' | 'previous') => { 65 | const nextIndex = 66 | type === 'next' 67 | ? index + 1 >= queue.queue.length 68 | ? 0 69 | : index + 1 70 | : index - 1 === -1 71 | ? 0 72 | : index - 1; 73 | 74 | handleQueue('currentIndex', nextIndex); 75 | const data: any[string] = queue.queue[nextIndex]; 76 | if (song.playing) { 77 | handleSong({ id: 'playing', value: false }); 78 | setTimeout(() => handleSong(data), 10); 79 | } else handleSong(data); 80 | }; 81 | 82 | const parseTime = (time: number) => { 83 | const minutes = Math.floor(time / 60); 84 | const second = Math.floor(time - minutes * 60); 85 | return `${minutes < 10 ? `0${minutes}` : minutes}:${ 86 | second < 10 ? `0${second}` : second 87 | }`; 88 | }; 89 | 90 | const triggerDuration = (time: number | number[]) => { 91 | handleSong({ id: 'playing', value: false }); 92 | song.audio.currentTime = (Number(time) / 100) * property.duration; 93 | handleChange('progress', (Number(time) / 100) * property.duration); 94 | document.getElementById('current-duration')!.innerText = parseTime( 95 | song.audio.currentTime 96 | ); 97 | handleSong({ id: 'playing', value: true }); 98 | }; 99 | 100 | song.audio.onloadeddata = () => 101 | handleChange('duration', song.audio.duration); 102 | song.audio.ontimeupdate = () => { 103 | handleChange( 104 | 'progress', 105 | (song.audio.currentTime / song.audio.duration) * 100 106 | ); 107 | document.getElementById('current-duration')!.innerText = parseTime( 108 | song.audio.currentTime 109 | ); 110 | }; 111 | 112 | const triggerAudio = (e: React.MouseEvent) => { 113 | e.preventDefault(); 114 | handleSong({ id: 'playing', value: !song.playing }); 115 | (e.target as Element).classList.toggle('pause'); 116 | }; 117 | 118 | return ( 119 | 198 | ); 199 | }; 200 | 201 | export default Controls; 202 | -------------------------------------------------------------------------------- /src/components/app.component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from 'react'; 2 | import { initializeApp } from 'firebase/app'; 3 | import { getDatabase, ref, onValue } from 'firebase/database'; 4 | import { getFirestore, collection, addDoc } from 'firebase/firestore'; 5 | import { 6 | Alert, 7 | Slide, 8 | Snackbar, 9 | LinearProgress, 10 | SlideProps, 11 | AlertTitle, 12 | Button, 13 | } from '@mui/material'; 14 | import { 15 | checkUpdate, 16 | installUpdate, 17 | onUpdaterEvent, 18 | } from '@tauri-apps/api/updater'; 19 | import { relaunch } from '@tauri-apps/api/process'; 20 | 21 | import Navbar from './navbar.component'; 22 | import BaseLayout from './base.component'; 23 | import Controls from './controls.component'; 24 | import packageInfo from '../../package.json'; 25 | import { AppInterface } from '../lib/interfaces.component'; 26 | import { ExternalLink } from '../lib/icons.component'; 27 | 28 | type TransitionProps = Omit; 29 | 30 | async function unlistenUpdaterEvent() { 31 | await onUpdaterEvent(({ error, status }) => { 32 | // This will log all updater events, including status updates and errors. 33 | console.log('Updater event', error, status); 34 | }); 35 | } 36 | 37 | // eslint-disable-next-line 38 | const App = ({ properties, handleChange }: AppInterface) => { 39 | const musicSession = 40 | localStorage.getItem('continue-previous-session') === 'true' 41 | ? JSON.parse(localStorage.getItem('music-session') || '{}') 42 | : {}; 43 | const [data, setData] = useState([]); 44 | const [isOffline, setConnectionState] = useState(false); 45 | const [isUpToDate, setUpToDate] = useState(false); 46 | const [transition, setTransition] = useState< 47 | React.ComponentType | undefined 48 | >(undefined); 49 | const [updateDialog, setUpdateDialog] = useState(false); 50 | const [song, setSong] = useState({ 51 | playing: false, 52 | title: musicSession.title ? musicSession.title : 'Underwater', 53 | author: musicSession.author ? musicSession.author : 'LiQWYD', 54 | image: musicSession.image 55 | ? musicSession.image 56 | : 'https://user-images.githubusercontent.com/69080584/129511233-dd5a0eac-2675-415e-ae4c-6cc530a23629.png', 57 | audio: musicSession.audio 58 | ? new Audio(musicSession.audio) 59 | : new Audio( 60 | 'https://user-images.githubusercontent.com/69080584/129511300-e88655e9-687f-4d0b-acb4-b32c0fa988cf.mp4' 61 | ), 62 | }); 63 | 64 | function Transition(props: TransitionProps) { 65 | return ; 66 | } 67 | 68 | async function updateAppToLatestVersion(via: 'button' | 'auto', cb?: any) { 69 | // Only run this function if the app is running in Tauri production environment 70 | if ( 71 | window.__TAURI_METADATA__ && 72 | process.env.NODE_ENV === 'production' 73 | ) { 74 | try { 75 | const { shouldUpdate, manifest } = await checkUpdate(); 76 | if (shouldUpdate) { 77 | setTransition(() => Transition); 78 | setUpdateDialog(manifest); 79 | } else if (via === 'button') { 80 | setUpToDate(true); 81 | setTimeout(() => setUpToDate(false), 5000); 82 | cb(true); 83 | } 84 | } catch (error) { 85 | console.error(error); 86 | } 87 | 88 | unlistenUpdaterEvent(); 89 | } 90 | } 91 | 92 | useEffect(() => { 93 | initializeApp({ 94 | apiKey: process.env.REACT_APP_API_KEY, 95 | authDomain: process.env.REACT_APP_AUTH_DOMAIN, 96 | databaseURL: process.env.REACT_APP_DB_URL, 97 | projectId: process.env.REACT_APP_PROJECT_ID, 98 | }); 99 | 100 | onValue(ref(getDatabase(), 'loofi-music/'), (snapshot) => { 101 | const rawData = snapshot.val(); 102 | let index = rawData.length, 103 | randIndex; // eslint-disable-line 104 | while (index !== 0) { 105 | randIndex = Math.floor(Math.random() * index); 106 | index--; 107 | [rawData[index], rawData[randIndex]] = [ 108 | rawData[randIndex], 109 | rawData[index], 110 | ]; 111 | } 112 | setData(rawData); 113 | }); 114 | 115 | if (document.readyState === 'complete') { 116 | onValue(ref(getDatabase(), '.info/connected'), (snapshot) => { 117 | if (!snapshot.val()) { 118 | setTransition(() => Transition); 119 | setConnectionState(true); 120 | } else setConnectionState(false); 121 | }); 122 | } 123 | 124 | window.onerror = (msg, url, lineNo, columnNo, error) => { 125 | async function sendData() { 126 | await addDoc(collection(getFirestore(), 'logs'), { 127 | message: String(msg), 128 | url: String(url), 129 | location: String(lineNo) + ' ' + String(columnNo), 130 | error: String(error), 131 | }); 132 | } 133 | sendData(); 134 | return false; 135 | }; 136 | 137 | const themeURL = JSON.parse( 138 | localStorage.getItem('theme-session') || `{}` 139 | ).url; 140 | const backgroundElement = document.getElementById('backdrop-image'); 141 | 142 | if (backgroundElement && themeURL) 143 | backgroundElement.style.background = `url(${themeURL})`; 144 | 145 | updateAppToLatestVersion('auto'); 146 | }, []); // eslint-disable-line 147 | 148 | const handleSong = useCallback( 149 | (a: any) => { 150 | if (!a.id && !a.value) { 151 | localStorage.setItem('music-session', JSON.stringify(a)); 152 | a.audio === song.audio.getAttribute('src') 153 | ? setSong({ ...song, playing: !song.playing }) 154 | : setSong({ 155 | ...a, 156 | audio: new Audio(a.audio), 157 | image: a.image, 158 | playing: true, 159 | }); 160 | } else setSong({ ...song, [a.id]: a.value }); 161 | }, 162 | [song] 163 | ); 164 | 165 | return ( 166 |
167 | {data.length === 0 ? : null} 168 |
172 |
173 | 177 | 184 |
185 | 186 | 192 | 193 | 194 | 195 | You are offline. Some functionality may be unavailable. 196 | 197 | 198 | 199 | 200 | 201 | Loofi Desktop is up to date. 202 | 203 | 204 | 205 | 209 | 210 | 211 | Update Available 212 | 213 |

214 | A new version of Loofi Desktop is available! Version{' '} 215 | {updateDialog.version} is now available—you have{' '} 216 | {packageInfo.version}.
217 | 223 | View the release notes 224 | 225 |
226 |
227 | Would you like to update now? 228 |

229 | 237 | 243 |
244 |
245 |
246 |
247 | ); 248 | }; 249 | 250 | export default App; 251 | -------------------------------------------------------------------------------- /src/components/home.component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Skeleton } from '@mui/material'; 3 | 4 | interface Music { 5 | title: string; 6 | author: string; 7 | image: string; 8 | audio: HTMLAudioElement; 9 | } 10 | 11 | const Home = ({ song, songData, handleSong }: any) => { 12 | const [greeting, setGreeting] = useState(); 13 | 14 | const triggerAudio = ( 15 | e: React.MouseEvent, 16 | data: any 17 | ) => { 18 | e.preventDefault(); 19 | if (song.playing) { 20 | handleSong({ id: 'playing', value: false }); 21 | setTimeout(() => handleSong(data), 1); 22 | } else handleSong(data); 23 | (e.target as Element).classList.toggle('pause'); 24 | }; 25 | 26 | useEffect(() => { 27 | const currentHour = new Date().getHours(); 28 | if (currentHour < 12) setGreeting('Morning'); 29 | else if (currentHour < 18) setGreeting('Afternoon'); 30 | else setGreeting('Evening'); 31 | }, []); 32 | 33 | useEffect(() => { 34 | const btn = document.getElementById( 35 | (song.title + song.author).replace(/\s/g, '-') 36 | ); 37 | song.playing 38 | ? btn?.classList.add('pause') 39 | : btn?.classList.remove('pause'); 40 | }, [song]); 41 | 42 | function SkeletonPreview(count: number, type: 'large' | 'small') { 43 | const skeleton = []; 44 | for (let i = 0; i < count; i++) { 45 | skeleton.push( 46 | type === 'small' ? ( 47 |
48 |
49 | 55 |

56 | 61 | 62 |

63 |
64 |
65 | ) : ( 66 |
67 |
68 | 73 |
74 | 75 | 76 | 77 | 78 | 79 | 80 |
81 |
82 |
83 | ) 84 | ); 85 | } 86 | return skeleton; 87 | } 88 | 89 | return ( 90 |
91 |

Good {greeting}

92 |
93 | {songData?.length !== 0 94 | ? songData.map((music: Music, index: number) => { 95 | if (index > 5) return; 96 | return ( 97 |
98 |
99 | 100 |

101 | {music.title} 102 |

103 | 112 |
113 |
114 | ); 115 | }) 116 | : SkeletonPreview(6, 'small')} 117 |
118 |
119 | {songData?.length !== 0 && songData?.length >= 7 120 | ? songData.map((music: Music, index: number) => { 121 | if (index < 6 || index > 17) return; // eslint-disable-line 122 | return ( 123 |
124 |
125 | 126 |
127 |
128 |

129 | {music.title} 130 |

131 |

132 | {music.author} 133 |

134 |
135 | 144 |
145 |
146 |
147 | ); 148 | }) 149 | : SkeletonPreview(8, 'large')} 150 |
151 |
152 | {songData?.length !== 0 && songData?.length >= 19 153 | ? songData.map((music: Music, index: number) => { 154 | if (index < 17 || index > 22) return; // eslint-disable-line 155 | return ( 156 |
157 |
158 | 159 |

160 | {music.title} 161 |

162 | 171 |
172 |
173 | ); 174 | }) 175 | : null} 176 |
177 |
178 | {songData?.length !== 0 && songData?.length >= 7 179 | ? songData.map((music: Music, index: number) => { 180 | if (index < 22) return; // eslint-disable-line 181 | return ( 182 |
183 |
184 | 185 |
186 |
187 |

188 | {music.title} 189 |

190 |

191 | {music.author} 192 |

193 |
194 | 203 |
204 |
205 |
206 | ); 207 | }) 208 | : null} 209 |
210 |
211 | ); 212 | }; 213 | 214 | export default Home; 215 | -------------------------------------------------------------------------------- /src/components/about.component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Button, Tooltip, Accordion, AccordionSummary } from '@mui/material'; 3 | import packageInfo from '../../package.json'; 4 | import { 5 | Expand, 6 | Warning, 7 | License, 8 | Feedback, 9 | Resources, 10 | Checkmark, 11 | Changelog, 12 | AboutOutline, 13 | Contributors, 14 | PrivacyPolicy, 15 | CopyToClipboard as CopyToClipboardIcon, 16 | CheckUpdate, 17 | } from '../lib/icons.component'; 18 | import { LoadingButton } from '@mui/lab'; 19 | 20 | const About = ({ updateAppToLatestVersion }: any) => { 21 | const [copiedToClipboard, setCopiedToClipboard] = useState< 22 | boolean | string 23 | >(false); 24 | const [isFetching, setIsFetching] = useState(false); 25 | const [isUpToDate, setIsUpToDate] = useState(false); 26 | 27 | const CopyToClipboard = (e: React.MouseEvent) => { 28 | e.preventDefault(); 29 | navigator.clipboard.writeText(`Version: ${packageInfo.version}`).then( 30 | () => 31 | // Set copiedToClipboard to true 32 | // If Text is Copied to Clipboard Successfully 33 | setCopiedToClipboard(true), 34 | () => 35 | // Set copiedToClipboard to error 36 | // If Text isn't Copied to Clipboard Successfully 37 | setCopiedToClipboard('error') 38 | ); 39 | setTimeout(() => setCopiedToClipboard(false), 5000); 40 | }; 41 | 42 | return ( 43 |
44 |
45 |
46 | 47 |
48 |

Loofi

49 |

Version: {packageInfo.version}

50 |
51 |
52 | 63 | 83 | 84 | {window.__TAURI_METADATA__ ? ( 85 | 94 | { 106 | setIsFetching(true); 107 | updateAppToLatestVersion('button').then( 108 | (res: boolean) => { 109 | setIsUpToDate(res); 110 | setIsFetching(false); 111 | } 112 | ); 113 | }} 114 | > 115 | 116 | 117 | 118 | ) : null} 119 |
120 | 121 | 122 | }> 123 |
124 | 125 |

Helpful Resources

126 |
127 |
128 |
129 |
130 | 139 |
140 |
141 | 150 |
151 |
152 | 161 |
162 |
163 |
164 | 165 | 166 | }> 167 |
168 | 169 |

Privacy Policy

170 |
171 |
172 |
173 |

174 | Personal Information Collection 175 |

176 |

177 | Loofi does not collect, store, share or publish any 178 | personal information. 179 |

180 |

181 | Non-Personal Information Collection 182 |

183 |

184 | Loofi collects and stores data which are useful for 185 | logging, bugs, and fix crashes. All information sent is 186 | anonymous and free of any user or contextual data. 187 |

188 |
189 |
190 | 191 | 192 | }> 193 |
194 | 195 |

License

196 |
197 |
198 |
199 |

200 | Loofi is an open source project published under the MIT 201 | License. You can view the source code and contribute to 202 | this project on{' '} 203 | 208 | GitHub 209 | 210 | . 211 |

212 | 213 |

214 | Copyright (c) 2023 Loofi 215 |

216 |

217 | Permission is hereby granted, free of charge, to any 218 | person obtaining a copy of this software and 219 | associated documentation files (the 220 | "Software"), to deal in the Software 221 | without restriction, including without limitation 222 | the rights to use, copy, modify, merge, publish, 223 | distribute, sublicense, and/or sell copies of the 224 | Software, and to permit persons to whom the Software 225 | is furnished to do so, subject to the following 226 | conditions: 227 |

228 |

229 | The above copyright notice and this permission 230 | notice shall be included in all copies or 231 | substantial portions of the Software. 232 |

233 |

234 | THE SOFTWARE IS PROVIDED "AS IS" WITHOUT 235 | WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 236 | BUT NOT LIMITED TO THE WARRANTIES OF 237 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE 238 | AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 239 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 240 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 241 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 242 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 243 | DEALINGS IN THE SOFTWARE. 244 |

245 |
246 |

Third party library:

247 |

248 | 253 | @mui-org/material-ui 254 | 255 |

256 | 257 |

MIT License

258 |

259 | Copyright (c) 2014 Call-Em-All 260 |

261 |

262 | Permission is hereby granted, free of charge, to any 263 | person obtaining a copy of this software and 264 | associated documentation files (the 265 | "Software"), to deal in the Software 266 | without restriction, including without limitation 267 | the rights to use, copy, modify, merge, publish, 268 | distribute, sublicense, and/or sell copies of the 269 | Software, and to permit persons to whom the Software 270 | is furnished to do so, subject to the following 271 | conditions: 272 |

273 |

274 | The above copyright notice and this permission 275 | notice shall be included in all copies or 276 | substantial portions of the Software. 277 |

278 |

279 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT 280 | WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 281 | BUT NOT LIMITED TO THE WARRANTIES OF 282 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE 283 | AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 284 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 285 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 286 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 287 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 288 | DEALINGS IN THE SOFTWARE. 289 |

290 |
291 |
292 |
293 |
294 | ); 295 | }; 296 | 297 | export default About; 298 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --black: 0 0 0; 3 | --white: 255 255 255; 4 | --gray: 205 205 205; 5 | --dark-gray: 56 56 56; 6 | --light-gray: 244 244 244; 7 | --blue: 25 118 210; 8 | } 9 | 10 | body { 11 | margin: 0; 12 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 13 | 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 14 | 'Helvetica Neue', sans-serif; 15 | -moz-osx-font-smoothing: grayscale; 16 | -webkit-font-smoothing: antialiased; 17 | } 18 | 19 | h1, 20 | h2, 21 | h3, 22 | h4, 23 | h5, 24 | h6, 25 | p { 26 | margin: 0; 27 | padding: 0; 28 | } 29 | 30 | /* App Background */ 31 | .backdrop { 32 | top: 0; 33 | left: 0; 34 | z-index: -2; 35 | width: 100%; 36 | height: 100%; 37 | position: fixed; 38 | } 39 | .acrylic-material, 40 | .backdrop-image, 41 | .backdrop-overlay { 42 | top: 0; 43 | left: 0; 44 | width: 100%; 45 | height: 100%; 46 | position: absolute; 47 | } 48 | .backdrop-overlay { 49 | top: 50%; 50 | left: 50%; 51 | z-index: -1; 52 | width: 200%; 53 | height: 250%; 54 | display: flex; 55 | position: fixed; 56 | display: -webkit-box; 57 | display: -ms-flexbox; 58 | -ms-flex-wrap: wrap; 59 | flex-wrap: wrap; 60 | -webkit-transform: translateX(-50%) translateY(-50%) rotate(45deg); 61 | -ms-transform: translateX(-50%) translateY(-50%) rotate(45deg); 62 | transform: translateX(-50%) translateY(-50%) rotate(45deg); 63 | } 64 | .backdrop-overlay div { 65 | width: 10%; 66 | height: 10%; 67 | margin-right: 0.75%; 68 | margin-bottom: 0.75%; 69 | border-radius: 1.5vh; 70 | border: 1px solid rgb(var(--black)); 71 | } 72 | .border-box { 73 | box-sizing: border-box !important; 74 | } 75 | .backdrop-image { 76 | background: url('https://user-images.githubusercontent.com/69080584/135237771-30c727d6-e88f-48e1-a65c-4fcef9e91113.png') 77 | center/cover no-repeat; 78 | -webkit-filter: blur(25px); 79 | filter: blur(25px); 80 | -webkit-transform: scale(1.1); 81 | -ms-transform: scale(1.1); 82 | transform: scale(1.1); 83 | } 84 | .acrylic-material { 85 | z-index: 1; 86 | backdrop-filter: none; 87 | -webkit-backdrop-filter: none; 88 | } 89 | .acrylic-material::before { 90 | content: ''; 91 | opacity: 0.6; 92 | width: 100%; 93 | height: 100%; 94 | position: absolute; 95 | background-blend-mode: normal, color, luminosity; 96 | background: linear-gradient( 97 | 0deg, 98 | rgba(239, 239, 239, 0.3), 99 | rgba(239, 239, 239, 0.3) 100 | ), 101 | rgba(239, 239, 239, 0.95) 102 | url('https://user-images.githubusercontent.com/69080584/135238252-5fd29825-32dc-4035-bd75-98eabc473ac4.png') 103 | center/196px repeat; 104 | background: -o-linear-gradient( 105 | bottom, 106 | rgba(239, 239, 239, 0.3), 107 | rgba(239, 239, 239, 0.3) 108 | ), 109 | rgba(239, 239, 239, 0.95) 110 | url('https://user-images.githubusercontent.com/69080584/135238252-5fd29825-32dc-4035-bd75-98eabc473ac4.png') 111 | center/196px repeat; 112 | background: -webkit-gradient( 113 | linear, 114 | left bottom, 115 | left top, 116 | from(rgba(239, 239, 239, 0.3)), 117 | to(rgba(239, 239, 239, 0.3)) 118 | ), 119 | rgba(239, 239, 239, 0.95) 120 | url('https://user-images.githubusercontent.com/69080584/135238252-5fd29825-32dc-4035-bd75-98eabc473ac4.png') 121 | center/196px repeat; 122 | } 123 | 124 | /* Sidebar */ 125 | .sidebar { 126 | width: 225px; 127 | height: 100%; 128 | padding: 10px; 129 | position: fixed; 130 | padding-top: 30px; 131 | box-sizing: border-box; 132 | background: rgb(var(--white) / 40%); 133 | -webkit-transition: width 0.4s ease-in-out; 134 | -moz-transition: width 0.4s ease-in-out; 135 | -o-transition: width 0.4s ease-in-out; 136 | transition: width 0.4s ease-in-out; 137 | } 138 | #tabs { 139 | height: 100%; 140 | display: flex; 141 | flex-direction: column; 142 | } 143 | #tabs .active { 144 | background: rgb(var(--blue) / 15%); 145 | } 146 | .tab:not(#settings).active svg, 147 | .tab:not(#settings) .Mui-selected svg { 148 | animation: zoomIn 0.3s !important; 149 | } 150 | #settings.active svg { 151 | transition: all 0.5s !important; 152 | transform: rotate(360deg); 153 | } 154 | #tabs :nth-child(3) { 155 | margin-top: auto; 156 | } 157 | #tabs svg { 158 | font-size: 1.5em !important; 159 | } 160 | .tab { 161 | font-weight: 525; 162 | color: rgb(var(--black) / 70%) !important; 163 | } 164 | .tab:hover, 165 | .active { 166 | color: rgb(var(--black)) !important; 167 | } 168 | 169 | /* Navbar */ 170 | .navbar { 171 | width: 100%; 172 | height: 75px; 173 | padding: 15px; 174 | display: flex; 175 | box-sizing: border-box; 176 | background-color: rgb(var(--white) / 50%); 177 | } 178 | .navbar div.mrl-10:last-child { 179 | margin-left: auto; 180 | } 181 | .navbar .MuiIconButton-root:active svg { 182 | animation: zoomIn 0.3s !important; 183 | } 184 | 185 | /* Base Layout */ 186 | .app { 187 | display: flex; 188 | height: 100vh; 189 | margin-left: 225px; 190 | flex-direction: column; 191 | -webkit-transition: all 0.4s ease-in-out; 192 | -moz-transition: all 0.4s ease-in-out; 193 | -o-transition: all 0.4s ease-in-out; 194 | transition: all 0.4s ease-in-out; 195 | } 196 | .base { 197 | overflow: auto; 198 | height: calc(100% - 75px); 199 | } 200 | .app-ui { 201 | height: calc(100% - 95px); 202 | } 203 | 204 | /* Card */ 205 | .card { 206 | width: 100%; 207 | flex-wrap: nowrap !important; 208 | border-radius: 5px !important; 209 | transition: all 0.3s !important; 210 | box-sizing: border-box !important; 211 | background-color: rgb(var(--white) / 20%) !important; 212 | box-shadow: 0px 4px 8px rgb(var(--black) / 5%) !important; 213 | } 214 | .large-card { 215 | width: 100%; 216 | height: 100%; 217 | padding: 15px; 218 | border-radius: 5px !important; 219 | flex-wrap: nowrap !important; 220 | transition: all 0.3s !important; 221 | box-sizing: border-box !important; 222 | background-color: rgb(var(--white) / 20%) !important; 223 | box-shadow: 0px 4px 8px rgb(var(--black) / 5%) !important; 224 | } 225 | .card:hover { 226 | background-color: rgb(var(--white) / 50%) !important; 227 | } 228 | .large-card:hover { 229 | background-color: rgb(var(--white) / 50%) !important; 230 | } 231 | .card img { 232 | border-top-left-radius: 5px; 233 | border-bottom-left-radius: 5px; 234 | box-shadow: 2px 0 10px rgb(var(--black) / 20%); 235 | } 236 | .card p, 237 | .large-card h3 { 238 | font-weight: 600; 239 | } 240 | .card .play-btn, 241 | .large-card .play-btn { 242 | opacity: 0; 243 | } 244 | .card:hover .play-btn, 245 | .large-card:hover .play-btn, 246 | .play-btn.pause { 247 | opacity: 1; 248 | } 249 | #recent-playlist a { 250 | color: inherit; 251 | text-decoration: none; 252 | } 253 | #recent-playlist img { 254 | width: 75px; 255 | height: auto; 256 | display: block; 257 | } 258 | #playlist a { 259 | display: block; 260 | color: inherit; 261 | text-decoration: none; 262 | } 263 | #playlist img, 264 | #playlist .MuiSkeleton-root { 265 | height: auto; 266 | display: block; 267 | max-width: 100%; 268 | border-radius: 20px; 269 | box-sizing: border-box; 270 | } 271 | 272 | /* Properties */ 273 | .pb-0 { 274 | padding-bottom: 0 !important; 275 | } 276 | .no-capitalization { 277 | text-transform: none !important; 278 | } 279 | .no-underlines { 280 | text-decoration: none !important; 281 | } 282 | .MuiAlert-root { 283 | width: 50vh !important; 284 | } 285 | .no-font { 286 | font-size: 1em !important; 287 | } 288 | .font-15x { 289 | font-size: 1.5em !important; 290 | } 291 | .none { 292 | display: none !important; 293 | } 294 | .block { 295 | display: block !important; 296 | } 297 | button:disabled, 298 | input:disabled { 299 | pointer-events: none !important; 300 | } 301 | .font-black { 302 | color: rgb(var(--black)) !important; 303 | } 304 | .flex-nowrap { 305 | display: flex; 306 | flex-wrap: nowrap; 307 | } 308 | .flex { 309 | display: flex; 310 | flex-wrap: wrap; 311 | } 312 | .mrl-5 { 313 | margin: 0 5px; 314 | padding: 0; 315 | } 316 | .mrl-10 { 317 | margin: 0 10px; 318 | padding: 0; 319 | } 320 | .rounded-corner { 321 | border-radius: 5px; 322 | } 323 | .close-btn { 324 | right: 60px; 325 | float: right; 326 | } 327 | .left-align { 328 | text-align: left; 329 | } 330 | .center-align { 331 | text-align: center; 332 | } 333 | .center-vert { 334 | margin: auto 0 !important; 335 | } 336 | .center-flex { 337 | align-items: center; 338 | justify-content: center; 339 | } 340 | .align-right { 341 | margin-left: auto !important; 342 | margin-right: 0 !important; 343 | } 344 | .warning { 345 | color: red; 346 | font-weight: bold; 347 | } 348 | .w-20 { 349 | width: 20% !important; 350 | } 351 | .w-25 { 352 | width: 25% !important; 353 | } 354 | .w-30 { 355 | width: 30% !important; 356 | } 357 | .w-40 { 358 | width: 40% !important; 359 | } 360 | .w-50 { 361 | width: 50% !important; 362 | } 363 | .w-70 { 364 | width: 70% !important; 365 | } 366 | .w-80 { 367 | width: 80% !important; 368 | } 369 | .w-100 { 370 | width: 100% !important; 371 | } 372 | .alert-transparent { 373 | background-color: rgb(255 244 229 / 70%); 374 | } 375 | .p-0 { 376 | padding: 0px !important; 377 | } 378 | .p-5 { 379 | padding: 5px !important; 380 | } 381 | .p-10 { 382 | padding: 10px !important; 383 | } 384 | .p-15 { 385 | padding: 15px !important; 386 | } 387 | .m-5 { 388 | margin: 5px !important; 389 | } 390 | .m-10 { 391 | margin: 10px !important; 392 | } 393 | .mt-5 { 394 | margin-top: 5px !important; 395 | } 396 | .mb-5 { 397 | margin-bottom: 5px !important; 398 | } 399 | .mt-10 { 400 | margin-top: 10px !important; 401 | } 402 | .mt-30 { 403 | margin-top: 30px !important; 404 | } 405 | .mb-10 { 406 | margin-bottom: 10px !important; 407 | } 408 | .ml-10 { 409 | margin-left: 10px !important; 410 | } 411 | .mr-10 { 412 | margin-right: 10px !important; 413 | } 414 | .m-auto { 415 | margin: auto !important; 416 | } 417 | .m-10-auto { 418 | margin: auto 10px !important; 419 | } 420 | svg { 421 | margin: auto; 422 | display: block; 423 | } 424 | #version svg { 425 | margin: auto 0px; 426 | } 427 | #version p { 428 | font-weight: normal; 429 | } 430 | #version code p { 431 | font-size: 15px; 432 | } 433 | #version .MuiIconButton-root { 434 | padding: 10px !important; 435 | } 436 | #version a, 437 | .link { 438 | cursor: pointer; 439 | color: #3090ff; 440 | text-decoration: none; 441 | } 442 | #version a:hover, 443 | .link:hover { 444 | text-decoration: underline; 445 | } 446 | #version svg { 447 | font-size: 1.2em !important; 448 | } 449 | #version .MuiPaper-root:before { 450 | display: none !important; 451 | } 452 | 453 | .small { 454 | font-size: 12px; 455 | font-weight: normal !important; 456 | } 457 | button { 458 | border: none; 459 | cursor: pointer; 460 | background-color: transparent; 461 | } 462 | .col-3 { 463 | display: grid; 464 | grid-template-columns: 33.3% 33.3% 33.3%; 465 | } 466 | .col-4 { 467 | display: grid; 468 | grid-template-columns: 25% 25% 25% 25%; 469 | } 470 | #recent-playlist img, 471 | #footer img { 472 | display: block; 473 | width: 75px; 474 | height: 75px; 475 | } 476 | 477 | /* PlayBack Button */ 478 | .play-btn { 479 | width: 18px; 480 | height: 18px; 481 | cursor: pointer; 482 | border-style: solid; 483 | box-sizing: border-box; 484 | transition: 100ms all ease; 485 | border-width: 9px 0 9px 18px; 486 | border-color: transparent transparent transparent #202020; 487 | } 488 | .play-btn.pause { 489 | border-style: double; 490 | border-width: 0px 0 0px 17px; 491 | } 492 | 493 | /* Controller */ 494 | .footer { 495 | padding: 10px 15px; 496 | overflow: auto; 497 | background: rgb(var(--white) / 60%); 498 | color: rgb(var(--black)); 499 | } 500 | .footer .song-title { 501 | font-size: 18px; 502 | font-weight: 450; 503 | } 504 | .author { 505 | color: rgb(var(--dark-gray)); 506 | } 507 | .footer .author { 508 | font-size: 13px; 509 | } 510 | .playback-bar { 511 | width: 100%; 512 | display: flex; 513 | align-items: center; 514 | } 515 | .progress-time { 516 | font-size: 12px; 517 | min-width: 40px; 518 | } 519 | .audio { 520 | width: 100%; 521 | display: flex; 522 | padding: 10px; 523 | align-items: center; 524 | } 525 | .volume-btn { 526 | min-width: 0px !important; 527 | } 528 | 529 | /* Settings */ 530 | #settings .MuiBox-root { 531 | padding: 0px !important; 532 | } 533 | #settings .active { 534 | background-color: rgb(var(--blue) / 35%); 535 | } 536 | #settings svg { 537 | font-size: 1.5em; 538 | } 539 | #settings .flex .w-50.m-10 { 540 | width: calc(50% - 60px) !important; 541 | } 542 | #settings .flex .w-50.m-10:nth-child(odd) { 543 | margin: 5px 10px 5px 50px !important; 544 | } 545 | #settings .flex .w-50.m-10:nth-child(even) { 546 | margin: 5px 50px 5px 10px !important; 547 | } 548 | #resources p { 549 | font-size: 14.5px; 550 | } 551 | .copy-btn { 552 | position: absolute !important; 553 | right: 10px; 554 | z-index: 100; 555 | } 556 | .MuiTab-root { 557 | min-height: 55px; 558 | display: grid; 559 | grid-template-columns: 20% 80%; 560 | gap: 10px; 561 | } 562 | /* Download */ 563 | #download .large-card svg { 564 | display: block; 565 | margin: 10px auto; 566 | transition: 0.3s all; 567 | color: rgb(var(--black) / 70%); 568 | } 569 | #download .large-card:hover svg { 570 | color: rgb(var(--black)); 571 | } 572 | 573 | /* Search */ 574 | .search { 575 | width: 50%; 576 | } 577 | 578 | /* Scroll Bar */ 579 | ::-webkit-scrollbar { 580 | width: 15px; 581 | background: transparent; 582 | } 583 | ::-webkit-scrollbar-thumb { 584 | background: rgb(var(--black) / 75%); 585 | border-radius: 7px; 586 | /* border-radius: calc(15px / 2); */ 587 | background-clip: content-box; 588 | border: 4px solid transparent; 589 | } 590 | ::-webkit-scrollbar-thumb:hover { 591 | background: rgb(var(--black)); 592 | background-clip: content-box; 593 | } 594 | 595 | @keyframes zoomIn { 596 | 50% { 597 | transform: scale(1.15); 598 | } 599 | 100% { 600 | transform: scale(1); 601 | } 602 | } 603 | 604 | @media only screen and (max-width: 1200px) { 605 | .col-3 { 606 | grid-template-columns: 50% 50%; 607 | } 608 | #version .w-50 { 609 | width: 70% !important; 610 | } 611 | .w-50.audio { 612 | width: 70% !important; 613 | } 614 | .col-4 { 615 | grid-template-columns: 33.3% 33.3% 33.3%; 616 | } 617 | .search { 618 | width: 60%; 619 | } 620 | .MuiTab-root { 621 | grid-template-columns: auto; 622 | gap: 10px; 623 | } 624 | } 625 | @media only screen and (max-width: 800px) { 626 | .col-3 { 627 | grid-template-columns: 100%; 628 | } 629 | .col-4 { 630 | grid-template-columns: 50% 50%; 631 | } 632 | #version .w-50 { 633 | width: 90% !important; 634 | } 635 | .w-50.audio { 636 | width: 100% !important; 637 | } 638 | #settings .flex .w-50.m-10 { 639 | width: 100% !important; 640 | } 641 | #settings .flex .w-50.m-10:nth-child(odd), 642 | #settings .flex .w-50.m-10:nth-child(even) { 643 | margin: 5px 10px !important; 644 | } 645 | .search { 646 | width: 100%; 647 | } 648 | } 649 | @media only screen and (max-width: 600px) { 650 | .search { 651 | width: 100%; 652 | } 653 | .navbar div.mrl-10:last-child { 654 | display: none; 655 | visibility: hidden; 656 | } 657 | .col-4 { 658 | grid-template-columns: 100%; 659 | } 660 | #version .w-50 { 661 | width: 100% !important; 662 | } 663 | .MuiAlert-root { 664 | width: 100% !important; 665 | } 666 | #tabs { 667 | .MuiButtonBase-root { 668 | min-width: auto; 669 | justify-content: left; 670 | } 671 | .w-30 { 672 | width: 10% !important; 673 | } 674 | } 675 | .sidebar-text { 676 | display: none; 677 | visibility: hidden; 678 | } 679 | .sidebar { 680 | width: 60px; 681 | } 682 | .app { 683 | margin-left: 60px; 684 | } 685 | } 686 | -------------------------------------------------------------------------------- /src/lib/icons.component.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export function HomeSolid() { 3 | return ( 4 | 5 | 6 | 10 | 11 | 12 | ); 13 | } 14 | export function HomeOutline() { 15 | return ( 16 | 17 | 18 | 22 | 23 | 24 | ); 25 | } 26 | export function SearchSolid() { 27 | return ( 28 | 29 | 33 | 34 | ); 35 | } 36 | export function SearchOutline() { 37 | return ( 38 | 39 | 43 | 44 | ); 45 | } 46 | export function PreferencesOutline() { 47 | return ( 48 | 49 | 50 | 54 | 55 | 56 | ); 57 | } 58 | export function PreferencesSolid() { 59 | return ( 60 | 61 | 62 | 66 | 70 | 71 | 72 | ); 73 | } 74 | export function AboutOutline() { 75 | return ( 76 | 77 | 81 | 82 | ); 83 | } 84 | export function AboutSolid() { 85 | return ( 86 | 87 | 91 | 92 | ); 93 | } 94 | export function SettingsSolid() { 95 | return ( 96 | 97 | 98 | 102 | 103 | 104 | ); 105 | } 106 | export function SettingsOutline() { 107 | return ( 108 | 109 | 110 | 114 | 118 | 119 | 120 | ); 121 | } 122 | export function ExperimentalOutline() { 123 | return ( 124 | 125 | 129 | 130 | ); 131 | } 132 | export function ExperimentalSolid() { 133 | return ( 134 | 135 | 139 | 140 | ); 141 | } 142 | export function ChevronLeft() { 143 | return ( 144 | 145 | 149 | 150 | ); 151 | } 152 | export function ChevronRight() { 153 | return ( 154 | 155 | 159 | 160 | ); 161 | } 162 | export function SkipNext() { 163 | return ( 164 | 165 | 166 | 170 | 174 | 175 | 176 | ); 177 | } 178 | export function SkipPrevious() { 179 | return ( 180 | 181 | 182 | 186 | 190 | 191 | 192 | ); 193 | } 194 | export function Audio() { 195 | return ( 196 | 197 | 198 | 202 | 203 | 204 | ); 205 | } 206 | export function MutedAudio() { 207 | return ( 208 | 209 | 210 | 214 | 215 | 216 | ); 217 | } 218 | export function Close() { 219 | return ( 220 | 221 | 225 | 226 | ); 227 | } 228 | export function GitHub() { 229 | return ( 230 | 231 | 235 | 236 | ); 237 | } 238 | export function PrivacyPolicy() { 239 | return ( 240 | 241 | 249 | 250 | ); 251 | } 252 | export function Expand() { 253 | return ( 254 | 255 | 259 | 260 | ); 261 | } 262 | export function License() { 263 | return ( 264 | 265 | 271 | 272 | ); 273 | } 274 | export function Themes() { 275 | return ( 276 | 277 | 282 | 283 | ); 284 | } 285 | export function CopyToClipboard() { 286 | return ( 287 | 288 | 289 | 293 | 294 | 295 | ); 296 | } 297 | export function Checkmark() { 298 | return ( 299 | 300 | 301 | 305 | 306 | 307 | ); 308 | } 309 | export function Warning() { 310 | return ( 311 | 312 | 313 | 317 | 318 | 319 | ); 320 | } 321 | export function ThemesApp() { 322 | return ( 323 | 324 | 328 | 329 | ); 330 | } 331 | export function Play() { 332 | return ( 333 | 334 | 338 | 339 | ); 340 | } 341 | export function Pause() { 342 | return ( 343 | 344 | 348 | 349 | ); 350 | } 351 | export function Changelog() { 352 | return ( 353 | 354 | 355 | 359 | 360 | 361 | ); 362 | } 363 | export function Download() { 364 | return ( 365 | 366 | 370 | 371 | ); 372 | } 373 | export function Windows() { 374 | return ( 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | ); 384 | } 385 | export function MacOS() { 386 | return ( 387 | 388 | 393 | 394 | ); 395 | } 396 | export function Linux() { 397 | return ( 398 | 399 | 403 | 404 | ); 405 | } 406 | 407 | export function SaveLocation() { 408 | return ( 409 | 410 | 414 | 415 | ); 416 | } 417 | export function AutoPlay() { 418 | return ( 419 | 420 | 424 | 425 | ); 426 | } 427 | export function Resources() { 428 | return ( 429 | 430 | 431 | 435 | 436 | 437 | ); 438 | } 439 | export function Feedback() { 440 | return ( 441 | 442 | 443 | 447 | 448 | 449 | ); 450 | } 451 | export function Contributors() { 452 | return ( 453 | 454 | 455 | 459 | 463 | 467 | 471 | 475 | 479 | 480 | 481 | ); 482 | } 483 | export function ExternalLink() { 484 | return ( 485 | 492 | 496 | 497 | ); 498 | } 499 | 500 | export function CheckUpdate() { 501 | return ( 502 | 508 | 512 | 513 | ); 514 | } 515 | --------------------------------------------------------------------------------