├── .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 | 
4 |
5 |
6 |
7 | [](https://github.com/stanleyowen/loofi/releases)
8 | [](https://github.com/stanleyowen/loofi/network)
9 | [](https://github.com/stanleyowen/loofi/stargazers)
10 | [](https://github.com/stanleyowen/loofi/blob/master/LICENSE)
11 |
12 | [](https://github.com/stanleyowen/loofi/actions/workflows/codeql-analysis.yml)
13 | [](https://github.com/stanleyowen/loofi/actions/workflows/semgrep.yml)
14 | [](https://github.com/stanleyowen/loofi/actions/workflows/tauri-test-build.yml)
15 | [](https://app.netlify.com/sites/loofi/deploys)
16 | [](https://github.com/stanleyowen/loofi/actions/workflows/tauri-publish.yml)
17 |
18 | [](https://github.com/stanleyowen/loofi/releases)
19 | [](https://github.com/stanleyowen/loofi/releases)
20 | [](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 | [](https://github.com/stanleyowen/loofi/stargazers)
32 | [](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 |
31 | {value === index &&
{children}
}
32 |
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 |
22 |
37 |
38 |
39 |
40 |
51 |
52 |
56 |
60 |
64 |
65 |
66 |
67 |
JavaScript Required
68 |
69 |
70 | We're sorry, but Loofi doesn't work appropriately without
71 | JavaScript enabled.
72 |
73 |
74 |
75 |
76 |
77 |
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 |
58 |
59 |
60 | {Theme.map((theme) => {
61 | return (
62 |
67 | setTheme(theme.type, theme.image)
68 | }
69 | >
70 |
71 |
72 |
73 |
74 | {theme.type}
75 |
76 |
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 |
55 | Download
56 |
57 |
58 |
59 |
60 |
61 |
62 |
macOS
63 |
64 | {properties?.downloadURL['darwin-x86_64']?.dmg
65 | ?.split('/')
66 | ?.pop()}
67 |
68 |
79 | Download
80 |
81 |
82 |
83 |
84 |
85 |
86 |
Linux
87 |
88 | {properties?.downloadURL['linux-x86_64']?.appImage
89 | ?.split('/')
90 | ?.pop()}
91 |
92 |
103 | Download
104 |
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 |
71 |
72 | {tab.toLowerCase() === 'download' ? (
73 |
74 | ) : properties.activeTab ===
75 | tab.toLowerCase() ? (
76 |
77 | ) : (
78 |
79 | )}
80 |
81 |
82 | {tab.toLowerCase()}
83 |
84 |
85 |
86 | );
87 | })}
88 | {process.env.REACT_APP_ALLOW_BETA === 'true' &&
89 | !window.__TAURI_METADATA__ ? (
90 | process.env.REACT_APP_CONTEXT === 'production' ? (
91 |
setDialog(true)}
95 | >
96 |
97 |
98 |
99 | Beta
100 |
101 | ) : (
102 |
106 | (window.location.href = String(
107 | process.env.REACT_APP_STABLE
108 | ))
109 | }
110 | >
111 |
112 |
113 |
114 | Stable
115 |
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 | setDialog(false)}>Cancel
135 |
138 | (window.location.href = String(
139 | process.env.REACT_APP_BETA
140 | ))
141 | }
142 | >
143 | Continue
144 |
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 |
triggerAudio(e, music)}
91 | id={(music.title + music.author).replace(
92 | /\s/g,
93 | '-'
94 | )}
95 | >
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 |
triggerAudio(e, music)}
115 | id={(music.title + music.author).replace(
116 | /\s/g,
117 | '-'
118 | )}
119 | >
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 | {
231 | await installUpdate();
232 | await relaunch();
233 | }}
234 | >
235 | Update Now
236 |
237 | setUpdateDialog(false)}
240 | >
241 | Later
242 |
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 |
106 | triggerAudio(e, music)
107 | }
108 | id={(
109 | music.title + music.author
110 | ).replace(/\s/g, '-')}
111 | >
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 |
138 | triggerAudio(e, music)
139 | }
140 | id={(
141 | music.title + music.author
142 | ).replace(/\s/g, '-')}
143 | >
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 |
165 | triggerAudio(e, music)
166 | }
167 | id={(
168 | music.title + music.author
169 | ).replace(/\s/g, '-')}
170 | >
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 |
197 | triggerAudio(e, music)
198 | }
199 | id={(
200 | music.title + music.author
201 | ).replace(/\s/g, '-')}
202 | >
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 | CopyToClipboard(e)}
74 | >
75 | {copiedToClipboard === true ? (
76 |
77 | ) : copiedToClipboard === 'error' ? (
78 |
79 | ) : (
80 |
81 | )}
82 |
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 |
135 |
136 | Submit Feedback
137 |
138 |
139 |
140 |
141 |
146 |
147 | Change Log
148 |
149 |
150 |
151 |
152 |
157 |
158 | Contributors
159 |
160 |
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 |
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 |
--------------------------------------------------------------------------------