Merge pull request #1 from KRTirtho/flutify

Flutify
This commit is contained in:
Kingkor Roy Tirtho 2022-01-08 19:26:02 +06:00 committed by GitHub
commit 99948dbb6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
188 changed files with 4952 additions and 33076 deletions

View File

@ -1,6 +0,0 @@
node_modules
.git
.gitignore
.env
Dockerfile
docker-compose*

View File

@ -1,45 +0,0 @@
/**
* @type {import("eslint").Linter.Config}
*/
const config = {
parser: "@typescript-eslint/parser",
extends: [
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
],
plugins: ["react", "@typescript-eslint"],
env: {
browser: true,
es6: true,
},
settings: {
react: {
version: "16.14.0",
},
},
globals: {
Atomics: "readonly",
SharedArrayBuffer: "readonly",
},
ignorePatterns: [".eslintrc.js", "./src/reportWebVitals.ts"],
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2018,
sourceType: "module",
project: "./tsconfig.json",
tsconfigRootDir: __dirname,
},
rules: {
"@typescript-eslint/no-var-requires": "off",
"prettier/prettier": "warn",
"react/prop-types": "off",
"linebreak-style": "off",
"react/react-in-jsx-scope": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
},
};
module.exports = config;

64
.gitignore vendored
View File

@ -1,22 +1,46 @@
node_modules
dist/
# Miscellaneous
*.class
*.log
# user specific
cache/
local/
# debian build specific
deb-struct/usr/lib/
deb-struct/usr/bin/
deb-struct/usr/share/applications
# deply build binaries
deploy/linux/build
deploy/win32/build
deploy/darwin/build
# aur build specific
aur-struct/src
aur-struct/pkg
aur-struct/*.zip*
aur-struct/*.zst
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
*.tsbuildinfo
tsconfig.tsbuildinfo
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

10
.metadata Normal file
View File

@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b
channel: stable
project_type: app

View File

@ -1,11 +0,0 @@
/**
* @type {import("prettier").Options}
*/
const config = {
tabWidth: 4,
printWidth: 90,
semi: true,
endOfLine: "auto",
trailingComma: "all",
};
module.exports = config;

13
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,13 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Flutter",
"type": "dart",
"request": "launch",
"program": "lib/main.dart"
}
],
"compounds": []
}

View File

@ -1,3 +1 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}
{}

10
.vscode/tasks.json vendored
View File

@ -1,12 +1,4 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"problemMatcher": [],
"label": "npm: start",
"detail": "qode ./dist/index.js"
}
]
"tasks": []
}

108
README.md
View File

@ -1,6 +1,6 @@
![Spotube](assets/spotube_banner.svg)
![Spotube](assets/spotube_banner.png)
Spotube is a [qt](https://qt.io) based lightweight spotify client which uses [nodegui/react-nodegui](https://github.com/nodegui/react-nodegui) as frontend & nodejs as backend. It utilizes the power of Spotify & Youtube's public API & creates a hazardless, performant & resource friendly User Experience
Spotube is a [Flutter](https://flutter.dev) based lightweight spotify client. It utilizes the power of Spotify & Youtube's public API & creates a hazardless, performant & resource friendly User Experience
![Application Screenshot](assets/spotube-screenshot.png)
## Features
@ -10,36 +10,23 @@ Following are the features that currently spotube offers:
- Open Source
- No telementry, diagnostics or user data collection
- Lightweight & resource friendly
- Near native performance & seemless with default desktop themes (Win10, Win7, OSX, QT-default)
- Near native performance (Thanks to Flutter+Skia)
- Playback control is on user's machine instead of server based
- Small size & less data hungry
- No spotify or youtube ads since it uses all public & free APIs (But it's recommended to support the creators by watching/liking/subscribing to the artists youtube channel or add as favourite track in spotify. Mostly buying spotify premium is the best way to support their valuable creations)
- Lyrics
- Downloadable track
- Downloadable track (WIP)
## Requirements (Linux🐧 only)
Don't worry **spotify premium isn't required**😱. But some extra packages are required.
- [MPV](https://mpv.io/installation/) player for playing the actual audio
- [youtube-dl](https://github.com/ytdl-org/youtube-dl) for streaming the audio from youtube. It already comes pre bundled with mpv
> **Important for [Ubuntu/Debian]():** If you're using any **ubuntu/debian** based linux distro then **youtube-dl** installed from the typical **apt-get** repositories will most likely not work as that version is older than current release. So remove it & install from the repository manually
Remove the **youtube-dl** installed with **mpv** player or from **apt package manger**
```bash
$ sudo apt-get remove youtube-dl
```
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) for streaming the audio from youtube
Now, Install youtube-dl from
- official github repo: https://github.com/ytdl-org/youtube-dl#installation (recommended)
**or**
- snap installation
```bash
$ snap install youtube-dl
```
- official github repo: https://github.com/yt-dlp/yt-dlp#installation (recommended)
## Installation
@ -69,7 +56,7 @@ Extract the **`Spotube-winx64-v<version>.zip`** & double click on **`install.bat
There are some configurations that needs to be done to start using this software
You need a spotify account & a web app for
You need a spotify account & a developer app for
- clientId
- clientSecret
@ -88,94 +75,25 @@ You need a spotify account & a web app for
- Click on **SHOW CLIENT SECRET** to reveal the **clientSecret**. Then copy the **clientID**, **clientSecret** & paste in the **Spotube's** respective fields
![step-4](https://user-images.githubusercontent.com/61944859/111769501-7fe31e80-88d3-11eb-8fc1-f3655dbd4711.png)
Also, you need a [genius](https://genius.com) account for **lyrics** & a API Client for
- accessToken
> **Note!**: No personal data or any kind of sensitive information won't be collected from spotify. Don't believe? See the code for yourself
### Building from source
**nodegui/react-nodegui** requires following packages to run
- [CMake](https://cmake.org/install/) 3.1 & up
- GCC v7
- Nodejs 12.x & up
**Windows Specific:**
- Visual Studio 2019
**MacOS & Linux specific:**
- Make
**Ubuntu/Debian based linux specific:**
Having `pkg-config build-essential mesa-common-dev libglu1-mesa-dev` is important
```bash
$ sudo apt-get install pkg-config build-essential mesa-common-dev libglu1-mesa-dev
```
After having this dependencies set up run following commands:
```bash
$ git clone https://github.com/KRTirtho/spotube.git
$ cd spotube
$ npm install
```
Now start building:
```bash
$ npm run build
$ npm run pack
```
Go to built package directory replace `os-name` with `linux`|`win32`|`darwin`:
```bash
$ cd deploy/<os-name>/build/spotube
```
If everything went smoothly then double clicking on the
- `AppRun` or Spotube-x86_64.AppImage for **linux**
- qode.exe for **Windows**
- Spotube-x86_64.dmg for **MacOS**
should work just fine without any problem
### Development
Follow the **Build from Source** guideline till `npm install`
Now, to start the dev server run the command in one terminal:
```bash
$ npm run dev
```
To start the application in development environment run following command in another terminal keeping the dev server running:
```bash
$ npm start
```
## Known Issues
There will be some glitches, lags & stuck motions because of the library Spotube is currently using under the hood. It has some issues with layouts thus sometimes some contents aren't shown or overflows out of the window. But resizing the window would fix this issue. Soon there will be some updates fixing this sort of layout related problems
## TODO:
- [ ] Compile, Debug & Build for **MacOS**
- [x] Add support for show Lyric of currently playing track
- [x] Track download
- [x] Cached playback
- [ ] Track download
- [ ] Support for playing/streaming podcasts/shows
- [x] Easy installation procedure/mechanism for simplicity in Windows
- [ ] Artist, User & Album pages
## Things that don't work
- Shows & Podcasts aren't supported as it'd require premium anyway
- Beautiful UI (you missed it, see the title😂👆)
- OS Media Controls
- Global Media Shortcuts/Keyboard Media Buttons
#### Social handlers

30
analysis_options.yaml Normal file
View File

@ -0,0 +1,30 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
file_names: false
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

13
android/.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks

68
android/app/build.gradle Normal file
View File

@ -0,0 +1,68 @@
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion flutter.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.spotube"
minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.spotube">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,34 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.spotube">
<application
android:label="spotube"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>

View File

@ -0,0 +1,6 @@
package com.example.spotube
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.spotube">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

31
android/build.gradle Normal file
View File

@ -0,0 +1,31 @@
buildscript {
ext.kotlin_version = '1.3.50'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true

View File

@ -0,0 +1,6 @@
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip

11
android/settings.gradle Normal file
View File

@ -0,0 +1,11 @@
include ':app'
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"

6
assets.d.ts vendored
View File

@ -1,6 +0,0 @@
declare module "*.svg";
declare module "*.png";
declare module "*.jpg";
declare module "*.jpeg";
declare module "*.gif";
declare module "*.bmp";

View File

@ -1 +0,0 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="angle-left" class="svg-inline--fa fa-angle-left fa-w-8" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 512"><path fill="currentColor" d="M31.7 239l136-136c9.4-9.4 24.6-9.4 33.9 0l22.6 22.6c9.4 9.4 9.4 24.6 0 33.9L127.9 256l96.4 96.4c9.4 9.4 9.4 24.6 0 33.9L201.7 409c-9.4 9.4-24.6 9.4-33.9 0l-136-136c-9.5-9.4-9.5-24.6-.1-34z"></path></svg>

Before

Width:  |  Height:  |  Size: 427 B

View File

@ -1 +0,0 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="backward" class="svg-inline--fa fa-backward fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M11.5 280.6l192 160c20.6 17.2 52.5 2.8 52.5-24.6V96c0-27.4-31.9-41.8-52.5-24.6l-192 160c-15.3 12.8-15.3 36.4 0 49.2zm256 0l192 160c20.6 17.2 52.5 2.8 52.5-24.6V96c0-27.4-31.9-41.8-52.5-24.6l-192 160c-15.3 12.8-15.3 36.4 0 49.2z"></path></svg>

Before

Width:  |  Height:  |  Size: 463 B

View File

@ -1 +0,0 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="download" class="svg-inline--fa fa-download fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M216 0h80c13.3 0 24 10.7 24 24v168h87.7c17.8 0 26.7 21.5 14.1 34.1L269.7 378.3c-7.5 7.5-19.8 7.5-27.3 0L90.1 226.1c-12.6-12.6-3.7-34.1 14.1-34.1H192V24c0-13.3 10.7-24 24-24zm296 376v112c0 13.3-10.7 24-24 24H24c-13.3 0-24-10.7-24-24V376c0-13.3 10.7-24 24-24h146.7l49 49c20.1 20.1 52.5 20.1 72.6 0l49-49H488c13.3 0 24 10.7 24 24zm-124 88c0-11-9-20-20-20s-20 9-20 20 9 20 20 20 20-9 20-20zm64 0c0-11-9-20-20-20s-20 9-20 20 9 20 20 20 20-9 20-20z"></path></svg>

Before

Width:  |  Height:  |  Size: 678 B

View File

@ -1 +0,0 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="forward" class="svg-inline--fa fa-forward fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M500.5 231.4l-192-160C287.9 54.3 256 68.6 256 96v320c0 27.4 31.9 41.8 52.5 24.6l192-160c15.3-12.8 15.3-36.4 0-49.2zm-256 0l-192-160C31.9 54.3 0 68.6 0 96v320c0 27.4 31.9 41.8 52.5 24.6l192-160c15.3-12.8 15.3-36.4 0-49.2z"></path></svg>

Before

Width:  |  Height:  |  Size: 454 B

View File

@ -1 +0,0 @@
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="heart" class="svg-inline--fa fa-heart fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M458.4 64.3C400.6 15.7 311.3 23 256 79.3 200.7 23 111.4 15.6 53.6 64.3-21.6 127.6-10.6 230.8 43 285.5l175.4 178.7c10 10.2 23.4 15.9 37.6 15.9 14.3 0 27.6-5.6 37.6-15.8L469 285.6c53.5-54.7 64.7-157.9-10.6-221.3zm-23.6 187.5L259.4 430.5c-2.4 2.4-4.4 2.4-6.8 0L77.2 251.8c-36.5-37.2-43.9-107.6 7.3-150.7 38.9-32.7 98.9-27.8 136.5 10.5l35 35.7 35-35.7c37.8-38.5 97.8-43.2 136.5-10.6 51.1 43.1 43.5 113.9 7.3 150.8z"></path></svg>

Before

Width:  |  Height:  |  Size: 640 B

View File

@ -1 +0,0 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="heart" class="svg-inline--fa fa-heart fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M462.3 62.6C407.5 15.9 326 24.3 275.7 76.2L256 96.5l-19.7-20.3C186.1 24.3 104.5 15.9 49.7 62.6c-62.8 53.6-66.1 149.8-9.9 207.9l193.5 199.8c12.5 12.9 32.8 12.9 45.3 0l193.5-199.8c56.3-58.1 53-154.3-9.8-207.9z"></path></svg>

Before

Width:  |  Height:  |  Size: 437 B

View File

@ -1,217 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 1024 1024"
width="1024"
height="1024"
id="svg164"
sodipodi:docname="Spotube Icon.svg"
inkscape:export-filename="/home/krtirtho/development/desktop-dev/spotube/deploy/linux/spotube/Spotube Icon.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
<metadata
id="metadata168">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1280"
inkscape:window-height="957"
id="namedview166"
showgrid="false"
inkscape:zoom="0.71289062"
inkscape:cx="223.73699"
inkscape:cy="427.38956"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg164" />
<defs
id="defs116">
<path
d="M864.5 488.49C864.5 680.95 708.5 837.21 516.36 837.21C324.22 837.21 168.22 680.95 168.22 488.49C168.22 296.03 324.22 139.78 516.36 139.78C708.5 139.78 864.5 296.03 864.5 488.49Z"
id="amHaSVpu" />
<path
d=""
id="a2BJMy7Jb" />
<path
d="M311.12 310.07C313.34 313.32 304.24 320.57 310.85 329.83C318.01 339.85 343.74 336.24 360.41 316.59C372.91 301.85 475.1 266.53 511.85 269.93C588.19 276.99 575.39 271.83 590.61 281.1C596.7 284.81 639.52 293.78 662.13 310.85C684.73 327.91 711.65 343.39 715.92 338.17C725.54 326.39 743.34 305.37 658.01 254.46C616.69 229.81 620.32 239.57 587.05 230.59C533.9 216.24 472.16 220.95 401.83 244.73C336.72 281.52 306.48 303.3 311.12 310.07Z"
id="a1tDWZImgU" />
<path
d="M411.75 413.54C413 415.03 367.9 428.18 360.73 442.12C349.52 463.92 375.04 487.76 385.62 472.66C392.39 463 485.79 428.29 508.37 428.23C555.27 428.11 596.35 438.36 606.16 443.1C614 446.9 672.19 473.83 676.79 467C681.97 459.31 702.29 441.03 647.4 415.1C607.69 396.35 600.7 394.71 570 390.75C547.31 387.83 524.26 387.05 500.84 388.41C438.23 401.33 408.53 409.7 411.75 413.54Z"
id="d5X4Y3iXxi" />
<path
d="M401.51 326.48C402.99 328.06 352.9 339.29 345.48 353.13C333.86 374.78 363.61 400.24 374.82 385.38C381.99 375.88 485.36 345.01 510.73 346.03C563.41 348.13 610.02 360.52 621.25 365.81C630.24 370.04 696.82 400.2 701.68 393.48C707.16 385.9 729.17 368.27 666.33 339.28C620.88 318.31 612.95 316.31 578.28 310.83C552.66 306.77 526.72 304.88 500.48 305.15C430.72 315.32 397.73 322.42 401.51 326.48Z"
id="caQrsW984" />
<path
d="M538.91 551.22C529.19 537.59 505.67 536.18 497.12 555.44C488.61 574.6 420.53 727.92 412.02 747.09C404.35 764.36 418.47 783.09 436.92 780.92C457.39 778.53 621.12 759.48 641.58 757.1C659.83 755 669.2 734.02 658.48 719.06C634.56 685.49 550.87 568 538.91 551.22Z"
id="gcFKZeS95" />
<path
d="M520.79 470.72C521.19 474.45 518.48 477.8 514.75 478.2C512.05 478.49 510.72 478.63 508.02 478.92C504.28 479.32 500.93 476.61 500.53 472.88C500.01 467.97 499.16 460.01 498.64 455.1C498.24 451.36 500.94 448.01 504.68 447.62C507.38 447.33 508.71 447.19 511.41 446.9C515.14 446.5 518.49 449.2 518.89 452.94C519.42 457.85 520.27 465.81 520.79 470.72Z"
id="aFNm6T4Ou" />
<path
d="M525.36 513.59C525.76 517.32 523.06 520.67 519.32 521.07C516.62 521.36 515.29 521.5 512.59 521.79C508.86 522.19 505.51 519.48 505.11 515.75C504.58 510.84 503.73 502.88 503.21 497.97C502.81 494.23 505.52 490.88 509.25 490.48C511.95 490.2 513.28 490.05 515.98 489.77C519.72 489.37 523.07 492.07 523.47 495.81C523.99 500.71 524.84 508.68 525.36 513.59Z"
id="f44lxYrdu7" />
</defs>
<g
id="g162"
transform="matrix(1.3645108,0,0,1.3645108,-191.6942,-155.16566)">
<g
id="g160">
<g
id="g120">
<use
xlink:href="#amHaSVpu"
opacity="1"
fill="#1db954"
fill-opacity="1"
id="use118"
x="0"
y="0"
width="100%"
height="100%" />
</g>
<g
id="g126">
<g
id="g124">
<use
xlink:href="#a2BJMy7Jb"
opacity="1"
fill-opacity="0"
stroke="#000000"
stroke-width="1"
stroke-opacity="1"
id="use122"
x="0"
y="0"
width="100%"
height="100%" />
</g>
</g>
<g
id="g140">
<g
id="g130">
<use
xlink:href="#a1tDWZImgU"
opacity="1"
fill="#ffffff"
fill-opacity="1"
id="use128"
x="0"
y="0"
width="100%"
height="100%" />
</g>
<g
id="g134">
<use
xlink:href="#d5X4Y3iXxi"
opacity="1"
fill="#ffffff"
fill-opacity="1"
id="use132"
x="0"
y="0"
width="100%"
height="100%" />
</g>
<g
id="g138">
<use
xlink:href="#caQrsW984"
opacity="1"
fill="#ffffff"
fill-opacity="1"
id="use136"
x="0"
y="0"
width="100%"
height="100%" />
</g>
</g>
<g
id="g148">
<use
xlink:href="#gcFKZeS95"
opacity="1"
fill="#ffffff"
fill-opacity="1"
id="use142"
x="0"
y="0"
width="100%"
height="100%" />
<g
id="g146">
<use
xlink:href="#gcFKZeS95"
opacity="1"
fill-opacity="0"
stroke="#000000"
stroke-width="1"
stroke-opacity="0"
id="use144"
x="0"
y="0"
width="100%"
height="100%" />
</g>
</g>
<g
id="g158">
<g
id="g152">
<use
xlink:href="#aFNm6T4Ou"
opacity="1"
fill="#ffffff"
fill-opacity="1"
id="use150"
x="0"
y="0"
width="100%"
height="100%" />
</g>
<g
id="g156">
<use
xlink:href="#f44lxYrdu7"
opacity="1"
fill="#ffffff"
fill-opacity="1"
id="use154"
x="0"
y="0"
width="100%"
height="100%" />
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1 +0,0 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="music" class="svg-inline--fa fa-music fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M470.38 1.51L150.41 96A32 32 0 0 0 128 126.51v261.41A139 139 0 0 0 96 384c-53 0-96 28.66-96 64s43 64 96 64 96-28.66 96-64V214.32l256-75v184.61a138.4 138.4 0 0 0-32-3.93c-53 0-96 28.66-96 64s43 64 96 64 96-28.65 96-64V32a32 32 0 0 0-41.62-30.49z"></path></svg>

Before

Width:  |  Height:  |  Size: 474 B

View File

@ -1 +0,0 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="pause" class="svg-inline--fa fa-pause fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M144 479H48c-26.5 0-48-21.5-48-48V79c0-26.5 21.5-48 48-48h96c26.5 0 48 21.5 48 48v352c0 26.5-21.5 48-48 48zm304-48V79c0-26.5-21.5-48-48-48h-96c-26.5 0-48 21.5-48 48v352c0 26.5 21.5 48 48 48h96c26.5 0 48-21.5 48-48z"></path></svg>

Before

Width:  |  Height:  |  Size: 444 B

View File

@ -1 +0,0 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="play" class="svg-inline--fa fa-play fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M424.4 214.7L72.4 6.6C43.8-10.3 0 6.1 0 47.9V464c0 37.5 40.7 60.1 72.4 41.3l352-208c31.4-18.5 31.5-64.1 0-82.6z"></path></svg>

Before

Width:  |  Height:  |  Size: 339 B

View File

@ -1 +0,0 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="random" class="svg-inline--fa fa-random fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M504.971 359.029c9.373 9.373 9.373 24.569 0 33.941l-80 79.984c-15.01 15.01-40.971 4.49-40.971-16.971V416h-58.785a12.004 12.004 0 0 1-8.773-3.812l-70.556-75.596 53.333-57.143L352 336h32v-39.981c0-21.438 25.943-31.998 40.971-16.971l80 79.981zM12 176h84l52.781 56.551 53.333-57.143-70.556-75.596A11.999 11.999 0 0 0 122.785 96H12c-6.627 0-12 5.373-12 12v56c0 6.627 5.373 12 12 12zm372 0v39.984c0 21.46 25.961 31.98 40.971 16.971l80-79.984c9.373-9.373 9.373-24.569 0-33.941l-80-79.981C409.943 24.021 384 34.582 384 56.019V96h-58.785a12.004 12.004 0 0 0-8.773 3.812L96 336H12c-6.627 0-12 5.373-12 12v56c0 6.627 5.373 12 12 12h110.785c3.326 0 6.503-1.381 8.773-3.812L352 176h32z"></path></svg>

Before

Width:  |  Height:  |  Size: 904 B

View File

@ -1 +0,0 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="search" class="svg-inline--fa fa-search fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z"></path></svg>

Before

Width:  |  Height:  |  Size: 577 B

View File

@ -1 +0,0 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="cog" class="svg-inline--fa fa-cog fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M487.4 315.7l-42.6-24.6c4.3-23.2 4.3-47 0-70.2l42.6-24.6c4.9-2.8 7.1-8.6 5.5-14-11.1-35.6-30-67.8-54.7-94.6-3.8-4.1-10-5.1-14.8-2.3L380.8 110c-17.9-15.4-38.5-27.3-60.8-35.1V25.8c0-5.6-3.9-10.5-9.4-11.7-36.7-8.2-74.3-7.8-109.2 0-5.5 1.2-9.4 6.1-9.4 11.7V75c-22.2 7.9-42.8 19.8-60.8 35.1L88.7 85.5c-4.9-2.8-11-1.9-14.8 2.3-24.7 26.7-43.6 58.9-54.7 94.6-1.7 5.4.6 11.2 5.5 14L67.3 221c-4.3 23.2-4.3 47 0 70.2l-42.6 24.6c-4.9 2.8-7.1 8.6-5.5 14 11.1 35.6 30 67.8 54.7 94.6 3.8 4.1 10 5.1 14.8 2.3l42.6-24.6c17.9 15.4 38.5 27.3 60.8 35.1v49.2c0 5.6 3.9 10.5 9.4 11.7 36.7 8.2 74.3 7.8 109.2 0 5.5-1.2 9.4-6.1 9.4-11.7v-49.2c22.2-7.9 42.8-19.8 60.8-35.1l42.6 24.6c4.9 2.8 11 1.9 14.8-2.3 24.7-26.7 43.6-58.9 54.7-94.6 1.5-5.5-.7-11.3-5.6-14.1zM256 336c-44.1 0-80-35.9-80-80s35.9-80 80-80 80 35.9 80 80-35.9 80-80 80z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

BIN
assets/spotube-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

71
assets/spotube-logo.svg Normal file
View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:bx="https://boxy-svg.com">
<defs>
<radialGradient id="gradient-2-0" gradientUnits="userSpaceOnUse" cx="251.179" cy="248.821" r="241.45" gradientTransform="matrix(1, 0, 0, 1, -1.768285, 0.589104)" xlink:href="#gradient-2"/>
<linearGradient id="gradient-2">
<stop offset="0.841" style="stop-color: rgb(255, 255, 255);"/>
<stop offset="1" style="stop-color: rgb(201, 201, 201);"/>
</linearGradient>
<filter id="drop-shadow-filter-0" x="-500%" y="-500%" width="1000%" height="1000%" bx:preset="drop-shadow 1 0 0 10 0.42 rgba(201,201,201,1)">
<feGaussianBlur in="SourceAlpha" stdDeviation="10"/>
<feOffset dx="0" dy="0"/>
<feComponentTransfer result="offsetblur">
<feFuncA id="spread-ctrl" type="linear" slope="0.84"/>
</feComponentTransfer>
<feFlood flood-color="rgba(201,201,201,1)"/>
<feComposite in2="offsetblur" operator="in"/>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<linearGradient id="gradient-4-3" gradientUnits="userSpaceOnUse" x1="47.146" y1="18.044" x2="47.146" y2="75.354" xlink:href="#gradient-4"/>
<linearGradient id="gradient-4">
<stop offset="0.113" style="stop-color: rgb(83, 240, 111);"/>
<stop offset="0.608" style="stop-color: rgb(0, 177, 86);"/>
<stop offset="0.944" style="stop-color: rgb(2, 167, 156);"/>
</linearGradient>
<filter id="inner-shadow-filter-0" x="-500%" y="-500%" width="1000%" height="1000%" bx:preset="inner-shadow 1 0 0 4 0.5 rgba(0,0,0,0.7)">
<feOffset dx="0" dy="0"/>
<feGaussianBlur stdDeviation="4"/>
<feComposite operator="out" in="SourceGraphic"/>
<feComponentTransfer result="choke">
<feFuncA type="linear" slope="1"/>
</feComponentTransfer>
<feFlood flood-color="rgba(0,0,0,0.7)" result="color"/>
<feComposite operator="in" in="color" in2="choke" result="shadow"/>
<feComposite operator="over" in="shadow" in2="SourceGraphic"/>
</filter>
<linearGradient id="gradient-4-1" gradientUnits="userSpaceOnUse" x1="82.026" y1="144.832" x2="82.026" y2="264.462" xlink:href="#gradient-4"/>
<linearGradient id="gradient-4-2" gradientUnits="userSpaceOnUse" x1="143.693" y1="22.804" x2="143.693" y2="264.582" xlink:href="#gradient-4"/>
<linearGradient id="gradient-4-0" gradientUnits="userSpaceOnUse" x1="205.862" y1="146.28" x2="205.862" y2="265.91" xlink:href="#gradient-4"/>
</defs>
<ellipse style="paint-order: fill; filter: url(#drop-shadow-filter-0); fill: url(#gradient-2-0);" cx="249.41" cy="249.41" rx="241.45" ry="241.45"/>
<g transform="matrix(0.319972, 0, 0, 0.323174, 248.635162, 304.74234)" style="">
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(-175.05 -175.05000000000004) scale(3.89 3.89)">
<path d="M 91.835 18.32 C 91.637 18.132 91.374 18.036 91.096 18.046 C 84.617 18.367 77.578 19.444 68.948 21.435 C 68.677 21.498 68.444 21.67 68.305 21.911 C 68.166 22.152 68.135 22.44 68.217 22.705 L 69.055 25.409 C 62.692 22.996 53.742 21.607 45.995 21.912 C 43.155 21.912 39.913 23.321 36.95 25.412 L 25.235 25.412 L 26.074 22.704 C 26.157 22.438 26.124 22.151 25.986 21.91 C 25.848 21.669 25.615 21.496 25.344 21.434 C 16.714 19.443 9.676 18.366 3.196 18.045 C 2.927 18.033 2.656 18.13 2.457 18.319 C 2.258 18.509 2.146 18.771 2.146 19.045 L 2.146 53.387 C 2.146 53.94 2.594 54.387 3.146 54.387 L 15.524 54.387 C 15.962 54.387 16.35 54.102 16.479 53.683 L 16.951 52.16 C 27.138 64.032 37.497 74.935 45.585 74.935 C 47.142 74.935 48.614 74.524 49.986 73.645 L 50.197 73.859 C 51.141 74.815 52.406 75.346 53.758 75.354 C 53.769 75.354 53.779 75.354 53.79 75.354 C 55.129 75.354 56.387 74.839 57.336 73.903 C 58.17 73.078 58.635 72.03 58.772 70.947 C 59.702 71.718 60.833 72.127 61.978 72.127 C 63.259 72.126 64.542 71.643 65.525 70.673 C 66.634 69.577 67.105 68.092 66.981 66.648 C 67.427 66.758 67.878 66.833 68.331 66.833 C 69.614 66.833 70.869 66.39 71.779 65.491 C 72.735 64.547 73.266 63.282 73.274 61.93 C 73.282 60.578 72.767 59.308 71.887 58.423 C 71.519 57.97 71.139 57.535 70.766 57.089 L 77.582 52.94 L 77.812 53.682 C 77.942 54.101 78.329 54.386 78.767 54.386 L 91.146 54.386 C 91.699 54.386 92.146 53.939 92.146 53.386 L 92.146 19.045 C 92.146 18.771 92.034 18.509 91.835 18.32 Z M 14.787 52.387 L 4.146 52.387 L 4.146 20.102 C 9.952 20.461 16.268 21.437 23.845 23.145 L 14.787 52.387 Z M 70.373 64.067 C 69.234 65.193 67.063 65.072 65.817 63.809 C 65.8 63.792 65.778 63.786 65.76 63.771 C 65.693 63.694 65.642 63.608 65.569 63.534 L 54.619 52.448 C 54.229 52.056 53.598 52.052 53.204 52.439 C 52.811 52.828 52.808 53.46 53.195 53.854 L 64.145 64.94 C 64.714 65.515 65.025 66.283 65.02 67.1 C 65.015 67.916 64.695 68.68 64.119 69.248 C 62.924 70.431 60.991 70.416 59.809 69.222 L 57.384 66.767 C 57.382 66.765 57.381 66.763 57.38 66.762 L 46.43 55.677 C 46.041 55.286 45.408 55.281 45.016 55.668 C 44.623 56.057 44.619 56.689 45.007 57.083 L 55.956 68.169 C 57.138 69.364 57.126 71.298 55.93 72.479 C 54.734 73.661 52.8 73.647 51.62 72.453 L 38.24 58.908 C 37.851 58.516 37.218 58.51 36.826 58.9 C 36.433 59.288 36.429 59.921 36.817 60.314 L 48.528 72.169 C 41.093 76.143 28.778 62.93 17.651 49.901 L 24.616 27.414 L 34.431 27.414 C 31.69 29.846 29.43 32.75 28.339 35.397 C 26.943 38.783 27.852 40.687 28.86 41.688 C 28.886 41.714 28.914 41.739 28.943 41.762 C 32.786 44.809 36.571 45.577 42.466 39.479 C 44.467 39.601 46.171 39.254 47.65 38.415 C 55.956 44.222 63.587 51.376 70.399 59.758 C 71.581 60.953 71.569 62.887 70.373 64.067 Z M 69.464 55.541 C 63.058 48.131 55.937 41.698 48.248 36.395 C 47.907 36.159 47.455 36.159 47.114 36.394 C 45.792 37.301 44.175 37.645 42.17 37.449 C 41.859 37.415 41.556 37.533 41.343 37.759 C 35.758 43.702 32.999 42.415 30.234 40.232 C 29.238 39.193 29.657 37.448 30.188 36.159 C 32.412 30.761 40.301 23.913 46.034 23.912 C 54.206 23.599 63.683 25.188 69.82 27.879 L 76.972 50.97 L 69.464 55.541 Z M 90.146 52.387 L 79.504 52.387 L 70.446 23.145 C 78.023 21.437 84.34 20.461 90.145 20.102 L 90.145 52.387 Z" style="stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1; fill: rgb(28, 28, 29); stroke-width: 3.33887px; paint-order: stroke; stroke: url(#gradient-4-3);" stroke-linecap="round"/>
</g>
</g>
<g transform="matrix(1.107829, 0, 0, 1.106267, 91.508957, 66.745735)" style="filter: url(#inner-shadow-filter-0);">
<g>
<path d="M 71.421 155.437 L 71.421 253.857 C 71.421 259.724 76.162 264.462 82.026 264.462 C 87.88 264.462 92.631 259.724 92.631 253.857 L 92.631 155.437 C 92.631 149.581 87.88 144.832 82.026 144.832 C 76.162 144.832 71.421 149.576 71.421 155.437 Z" style="stroke-width: 9.80924px; stroke: url(#gradient-4-1); fill: rgb(29, 29, 29); stroke-linecap: round; stroke-linejoin: round;"/>
<path d="M29.456,264.582h23.351v-116.85c0.064-0.56,0.166-1.119,0.166-1.693c0-50.412,40.69-91.42,90.698-91.42 c50.002,0,90.692,41.008,90.692,91.42c0,0.771,0.113,1.518,0.228,2.263v116.28h23.354c16.254,0,29.442-13.64,29.442-30.469 v-60.936c0-13.878-8.989-25.57-21.261-29.249c-1.129-66.971-55.608-121.124-122.45-121.124 c-66.86,0-121.347,54.158-122.465,121.15C8.956,147.638,0,159.32,0,173.187v60.926C0,250.932,13.187,264.582,29.456,264.582z" style="stroke-width: 11.3184px; stroke: url(#gradient-4-2); fill: rgb(29, 29, 29); stroke-linecap: round; stroke-linejoin: round;"/>
<path d="M 195.258 156.885 L 195.258 255.305 C 195.258 261.172 200.006 265.91 205.862 265.91 C 211.718 265.91 216.466 261.172 216.466 255.305 L 216.466 156.885 C 216.466 151.029 211.718 146.28 205.862 146.28 C 199.995 146.28 195.258 151.024 195.258 156.885 Z" style="stroke-width: 9.80924px; stroke: url(#gradient-4-0); fill: rgb(29, 29, 29); stroke-linecap: round; stroke-linejoin: round;"/>
</g>
</g>
<g transform="matrix(0.972684, 0, 0, 0.972684, 62.617451, 9.850071)" style=""/>
<g transform="matrix(0.972684, 0, 0, 0.972684, 62.617451, 9.850071)" style=""/>
<g transform="matrix(0.972684, 0, 0, 0.972684, 62.617451, 9.850071)" style=""/>
<g transform="matrix(0.972684, 0, 0, 0.972684, 62.617451, 9.850071)" style=""/>
<g transform="matrix(0.972684, 0, 0, 0.972684, 62.617451, 9.850071)" style=""/>
<g transform="matrix(0.972684, 0, 0, 0.972684, 62.617451, 9.850071)" style=""/>
<g transform="matrix(0.972684, 0, 0, 0.972684, 62.617451, 9.850071)" style=""/>
<g transform="matrix(0.972684, 0, 0, 0.972684, 62.617451, 9.850071)" style=""/>
<g transform="matrix(0.972684, 0, 0, 0.972684, 62.617451, 9.850071)" style=""/>
<g transform="matrix(0.972684, 0, 0, 0.972684, 62.617451, 9.850071)" style=""/>
<g transform="matrix(0.972684, 0, 0, 0.972684, 62.617451, 9.850071)" style=""/>
<g transform="matrix(0.972684, 0, 0, 0.972684, 62.617451, 9.850071)" style=""/>
<g transform="matrix(0.972684, 0, 0, 0.972684, 62.617451, 9.850071)" style=""/>
<g transform="matrix(0.972684, 0, 0, 0.972684, 62.617451, 9.850071)" style=""/>
<g transform="matrix(0.972684, 0, 0, 0.972684, 62.617451, 9.850071)" style=""/>
</svg>

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

After

Width:  |  Height:  |  Size: 732 KiB

BIN
assets/spotube_banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 736 KiB

View File

@ -1 +0,0 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="stop" class="svg-inline--fa fa-stop fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48z"></path></svg>

Before

Width:  |  Height:  |  Size: 333 B

View File

@ -1 +0,0 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="times" class="svg-inline--fa fa-times fa-w-11" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"><path fill="currentColor" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"></path></svg>

Before

Width:  |  Height:  |  Size: 645 B

View File

@ -1,42 +0,0 @@
import { build } from "esbuild";
import { mkdir, copyFile } from "fs/promises";
import { join } from "path";
build({
bundle: true,
outdir: "./dist",
minify: false,
platform: 'node',
target: "es6",
color: true,
jsx: "transform",
sourcemap: true,
tsconfig: "./tsconfig.json",
entryPoints: ["src/index.tsx"],
target: ['node12'],
loader: {
".png": "file",
".jpg": "file",
".gif": "file",
".svg": "file",
".node": "file"
},
plugins: [
{
name: "Native Addon",
setup(build) {
build.onResolve({ filter: /\.node$/ }, async (arg) => {
const pluginName = arg.path.split("/").reverse()[0]
mkdir(join(process.cwd(), "dist"), {recursive: true})
await copyFile(join(arg.resolveDir, arg.path), join(process.cwd(), "dist", pluginName))
return {
external: true,
path: "./"+pluginName,
namespace: "js",
pluginName
}
})
}
},
]
}).catch(() => process.exit(1));

34
ios/.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>9.0</string>
</dict>
</plist>

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1,481 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1300;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.spotube;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.spotube;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.spotube;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,13 @@
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

47
ios/Runner/Info.plist Normal file
View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Sptube</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>spotube</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@ -0,0 +1,87 @@
import 'package:flutter/material.dart' hide Page;
import 'package:provider/provider.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/PlaylistCard.dart';
import 'package:spotube/components/PlaylistGenreView.dart';
import 'package:spotube/provider/SpotifyDI.dart';
class CategoryCard extends StatefulWidget {
final Category category;
final Iterable<PlaylistSimple>? playlists;
const CategoryCard(
this.category, {
Key? key,
this.playlists,
}) : super(key: key);
@override
_CategoryCardState createState() => _CategoryCardState();
}
class _CategoryCardState extends State<CategoryCard> {
@override
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.category.name ?? "Unknown",
style: Theme.of(context).textTheme.headline5,
),
TextButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return PlaylistGenreView(
widget.category.id!,
widget.category.name!,
playlists: widget.playlists,
);
},
),
);
},
child: const Text("See all"),
)
],
),
),
Consumer<SpotifyDI>(
builder: (context, data, child) {
return FutureBuilder<Iterable<PlaylistSimple>>(
future: widget.playlists == null
? (widget.category.id != "user-featured-playlists"
? data.spotifyApi.playlists
.getByCategoryId(widget.category.id!)
: data.spotifyApi.playlists.featured)
.getPage(4, 0)
.then((value) => value.items ?? [])
: Future.value(widget.playlists),
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Center(child: Text("Error occurred"));
}
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator.adaptive(),
);
}
return Wrap(
spacing: 20,
runSpacing: 20,
children: snapshot.data!
.map((playlist) => PlaylistCard(playlist))
.toList(),
);
});
},
)
],
);
}
}

253
lib/components/Home.dart Normal file
View File

@ -0,0 +1,253 @@
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart' hide Page;
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotify/spotify.dart' hide Image;
import 'package:spotube/components/CategoryCard.dart';
import 'package:spotube/components/Login.dart';
import 'package:spotube/components/Lyrics.dart';
import 'package:spotube/components/PageWindowTitleBar.dart';
import 'package:spotube/components/Player.dart' as player;
import 'package:spotube/components/Settings.dart';
import 'package:spotube/components/UserLibrary.dart';
import 'package:spotube/models/LocalStorageKeys.dart';
import 'package:spotube/models/sideBarTiles.dart';
import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/SpotifyDI.dart';
List<String> spotifyScopes = [
"user-library-read",
"user-library-modify",
"user-read-private",
"user-read-email",
"playlist-read-collaborative"
];
class Home extends StatefulWidget {
const Home({Key? key}) : super(key: key);
@override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
final PagingController<int, Category> _pagingController =
PagingController(firstPageKey: 0);
int _selectedIndex = 0;
@override
void initState() {
super.initState();
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) async {
try {
Auth authProvider = context.read<Auth>();
SharedPreferences localStorage = await SharedPreferences.getInstance();
var clientId = localStorage.getString(LocalStorageKeys.clientId);
var clientSecret =
localStorage.getString(LocalStorageKeys.clientSecret);
var accessToken = localStorage.getString(LocalStorageKeys.accessToken);
var refreshToken =
localStorage.getString(LocalStorageKeys.refreshToken);
var expirationStr = localStorage.getString(LocalStorageKeys.expiration);
var expiration =
expirationStr != null ? DateTime.parse(expirationStr) : null;
if (clientId != null && clientSecret != null) {
SpotifyApi spotifyApi = SpotifyApi(
SpotifyApiCredentials(
clientId,
clientSecret,
accessToken: accessToken,
refreshToken: refreshToken,
expiration: expiration,
scopes: spotifyScopes,
),
);
SpotifyApiCredentials credentials = await spotifyApi.getCredentials();
if (credentials.accessToken?.isNotEmpty ?? false) {
authProvider.setAuthState(
clientId: clientId,
clientSecret: clientSecret,
accessToken:
credentials.accessToken, // accessToken can be new/refreshed
refreshToken: refreshToken,
expiration: credentials.expiration,
isLoggedIn: true,
);
}
}
_pagingController.addPageRequestListener((pageKey) async {
try {
SpotifyDI data = context.read<SpotifyDI>();
Page<Category> categories = await data.spotifyApi.categories
.list(country: "US")
.getPage(15, pageKey);
var items = categories.items!.toList();
if (pageKey == 0) {
Category category = Category();
category.id = "user-featured-playlists";
category.name = "Featured";
items.insert(0, category);
}
if (categories.isLast && categories.items != null) {
_pagingController.appendLastPage(items);
} else if (categories.items != null) {
_pagingController.appendPage(items, categories.nextOffset);
}
} catch (e) {
_pagingController.error = e;
}
});
} catch (e) {
print("[login state error]: $e");
}
});
}
@override
void dispose() {
_pagingController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
Auth authProvider = Provider.of<Auth>(context);
if (!authProvider.isLoggedIn) {
return Login();
}
return Scaffold(
body: Column(
children: [
WindowTitleBarBox(
child: Row(
children: [
Expanded(
child: Row(
children: [
Container(
constraints: const BoxConstraints(maxWidth: 256),
color:
Theme.of(context).navigationRailTheme.backgroundColor,
child: MoveWindow(),
),
Expanded(child: MoveWindow()),
const TitleBarActionButtons(),
],
)),
],
),
),
Expanded(
child: Row(
children: [
NavigationRail(
destinations: sidebarTileList
.map((e) => NavigationRailDestination(
icon: Icon(e.icon),
label: Text(
e.title,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
))
.toList(),
selectedIndex: _selectedIndex,
onDestinationSelected: (value) => setState(() {
_selectedIndex = value;
}),
extended: true,
leading: Padding(
padding: const EdgeInsets.only(left: 15),
child: Row(children: [
Image.asset(
"assets/spotube-logo.png",
height: 50,
width: 50,
),
const SizedBox(
width: 10,
),
Text("Spotube",
style: Theme.of(context).textTheme.headline4),
]),
),
trailing:
Consumer<SpotifyDI>(builder: (context, data, widget) {
return FutureBuilder<User>(
future: data.spotifyApi.me.get(),
builder: (context, snapshot) {
var avatarImg = snapshot.data?.images?.last.url;
return Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
if (avatarImg != null)
CircleAvatar(
backgroundImage:
CachedNetworkImageProvider(avatarImg),
),
const SizedBox(width: 10),
Text(
snapshot.data?.displayName ?? "User's name",
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
],
),
IconButton(
icon: const Icon(Icons.settings_outlined),
onPressed: () {
Navigator.of(context)
.push(MaterialPageRoute(
builder: (context) {
return const Settings();
},
));
}),
],
),
);
},
);
}),
),
// contents of the spotify
if (_selectedIndex == 0)
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: PagedListView(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<Category>(
itemBuilder: (context, item, index) {
return CategoryCard(item);
},
),
),
),
),
if (_selectedIndex == 2) const UserLibrary(),
if (_selectedIndex == 3) const Lyrics(),
],
),
),
// player itself
const player.Player()
],
),
);
}
}

159
lib/components/Login.dart Normal file
View File

@ -0,0 +1,159 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotify/spotify.dart' hide Image;
import 'package:spotube/components/Home.dart';
import 'package:spotube/components/PageWindowTitleBar.dart';
import 'package:spotube/helpers/server_ipc.dart';
import 'package:spotube/models/LocalStorageKeys.dart';
import 'package:spotube/provider/Auth.dart';
class Login extends StatefulWidget {
@override
_LoginState createState() => _LoginState();
}
class _LoginState extends State<Login> {
String clientId = "";
String clientSecret = "";
bool _fieldError = false;
String? accessToken;
String? refreshToken;
DateTime? expiration;
handleLogin(Auth authState) async {
try {
if (clientId == "" || clientSecret == "") {
return setState(() {
_fieldError = true;
});
}
final credentials = SpotifyApiCredentials(clientId, clientSecret);
final grant = SpotifyApi.authorizationCodeGrant(credentials);
const redirectUri = "http://localhost:4304/auth/spotify/callback";
final authUri = grant.getAuthorizationUrl(Uri.parse(redirectUri),
scopes: spotifyScopes);
final responseUri = await connectIpc(authUri.toString(), redirectUri);
SharedPreferences localStorage = await SharedPreferences.getInstance();
if (responseUri != null) {
final SpotifyApi spotify =
SpotifyApi.fromAuthCodeGrant(grant, responseUri);
var credentials = await spotify.getCredentials();
if (credentials.accessToken != null) {
accessToken = credentials.accessToken;
await localStorage.setString(
LocalStorageKeys.accessToken, credentials.accessToken!);
}
if (credentials.refreshToken != null) {
refreshToken = credentials.refreshToken;
await localStorage.setString(
LocalStorageKeys.refreshToken, credentials.refreshToken!);
}
if (credentials.expiration != null) {
expiration = credentials.expiration;
await localStorage.setString(LocalStorageKeys.expiration,
credentials.expiration?.toString() ?? "");
}
}
await localStorage.setString(LocalStorageKeys.clientId, clientId);
await localStorage.setString(
LocalStorageKeys.clientSecret,
clientSecret,
);
authState.setAuthState(
clientId: clientId,
clientSecret: clientSecret,
accessToken: accessToken,
refreshToken: refreshToken,
expiration: expiration,
isLoggedIn: true,
);
} catch (e) {
print("[Login.handleLogin] $e");
}
}
@override
Widget build(BuildContext context) {
return Consumer<Auth>(
builder: (context, authState, child) {
return Scaffold(
body: Column(
children: [
const PageWindowTitleBar(),
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.asset(
"assets/spotube-logo.png",
width: 400,
height: 400,
),
Text("Add your spotify credentials to get started",
style: Theme.of(context).textTheme.headline4),
const Text(
"Don't worry, any of your credentials won't be collected or shared with anyone"),
const SizedBox(
height: 10,
),
Container(
constraints: const BoxConstraints(
maxWidth: 400,
),
child: Column(
children: [
TextField(
decoration: const InputDecoration(
hintText: "Spotify Client ID",
label: Text("ClientID"),
),
onChanged: (value) {
setState(() {
clientId = value;
});
},
),
const SizedBox(
height: 10,
),
TextField(
decoration: const InputDecoration(
hintText: "Spotify Client Secret",
label: Text("Client Secret"),
),
onChanged: (value) {
setState(() {
clientSecret = value;
});
},
),
const SizedBox(
height: 10,
),
ElevatedButton(
onPressed: () {
handleLogin(authState);
},
child: const Text("Submit"),
)
],
),
),
],
),
),
),
],
),
);
},
);
}
}

View File

@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:spotube/helpers/artist-to-string.dart';
import 'package:spotube/helpers/getLyrics.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/UserPreferences.dart';
class Lyrics extends StatefulWidget {
const Lyrics({Key? key}) : super(key: key);
@override
State<Lyrics> createState() => _LyricsState();
}
class _LyricsState extends State<Lyrics> {
Map<String, String> _lyrics = {};
@override
Widget build(BuildContext context) {
Playback playback = context.watch<Playback>();
UserPreferences userPreferences = context.watch<UserPreferences>();
if (playback.currentTrack != null &&
userPreferences.geniusAccessToken != null &&
playback.currentTrack!.id != _lyrics["id"]) {
getLyrics(
playback.currentTrack!.name!,
artistsToString(playback.currentTrack!.artists ?? []),
apiKey: userPreferences.geniusAccessToken,
optimizeQuery: true,
).then((lyrics) {
if (lyrics != null) {
setState(() {
_lyrics = {"lyrics": lyrics, "id": playback.currentTrack!.id!};
});
}
});
}
if (_lyrics["lyrics"] != null && playback.currentTrack == null) {
setState(() {
_lyrics = {};
});
}
if (_lyrics["lyrics"] == null && playback.currentTrack != null) {
return const Expanded(
child: Center(
child: CircularProgressIndicator.adaptive(),
),
);
}
return Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Text(
playback.currentTrack?.name ?? "",
style: Theme.of(context).textTheme.headline3,
),
),
Center(
child: Text(
artistsToString(playback.currentTrack?.artists ?? []),
style: Theme.of(context).textTheme.headline5,
),
),
Expanded(
child: SingleChildScrollView(
child: Center(
child: Text(
_lyrics["lyrics"] == null && playback.currentTrack == null
? "No Track being played currently"
: _lyrics["lyrics"]!,
style: Theme.of(context).textTheme.headline6,
),
),
),
),
const Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Text("Powered by genius.com"),
),
)
],
),
);
}
}

View File

@ -0,0 +1,68 @@
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:flutter/material.dart';
import 'package:mpv_dart/mpv_dart.dart';
import 'package:provider/provider.dart';
import 'package:spotube/provider/PlayerDI.dart';
class TitleBarActionButtons extends StatelessWidget {
const TitleBarActionButtons({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
MPVPlayer player = context.watch<PlayerDI>().player;
return Row(
children: [
TextButton(
onPressed: () {
appWindow.minimize();
},
style: ButtonStyle(
foregroundColor:
MaterialStateProperty.all(Theme.of(context).iconTheme.color),
),
child: const Icon(Icons.minimize_rounded)),
TextButton(
onPressed: () {
appWindow.maximizeOrRestore();
},
style: ButtonStyle(
foregroundColor:
MaterialStateProperty.all(Theme.of(context).iconTheme.color),
),
child: const Icon(Icons.crop_square_rounded)),
TextButton(
onPressed: () {
player.stop();
appWindow.close();
},
style: ButtonStyle(
foregroundColor:
MaterialStateProperty.all(Theme.of(context).iconTheme.color),
overlayColor: MaterialStateProperty.all(Colors.redAccent),
),
child: const Icon(
Icons.close_rounded,
)),
],
);
}
}
class PageWindowTitleBar extends StatelessWidget {
final Widget? leading;
final Widget? center;
const PageWindowTitleBar({Key? key, this.leading, this.center})
: super(key: key);
@override
Widget build(BuildContext context) {
return WindowTitleBarBox(
child: Row(
children: [
if (leading != null) leading!,
Expanded(child: MoveWindow(child: Center(child: center))),
const TitleBarActionButtons()
],
),
);
}
}

312
lib/components/Player.dart Normal file
View File

@ -0,0 +1,312 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/PlayerControls.dart';
import 'package:spotube/helpers/artist-to-string.dart';
import 'package:spotube/models/GlobalKeyActions.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:flutter/material.dart';
import 'package:mpv_dart/mpv_dart.dart';
import 'package:provider/provider.dart';
import 'package:spotube/provider/PlayerDI.dart';
import 'package:spotube/provider/SpotifyDI.dart';
class Player extends StatefulWidget {
const Player({Key? key}) : super(key: key);
@override
_PlayerState createState() => _PlayerState();
}
class _PlayerState extends State<Player> {
bool _isPlaying = false;
bool _shuffled = false;
double _duration = 0;
String? _currentPlaylistId;
double _volume = 0;
List<HotKey> _hotKeys = [];
@override
void initState() {
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) async {
try {
MPVPlayer player = context.read<PlayerDI>().player;
if (!player.isRunning()) {
await player.start();
}
double volume = await player.getProperty<double>("volume");
setState(() {
_volume = volume / 100;
});
player.on(MPVEvents.paused, null, (ev, context) {
setState(() {
_isPlaying = false;
});
});
player.on(MPVEvents.resumed, null, (ev, context) {
setState(() {
_isPlaying = true;
});
});
player.on(MPVEvents.status, null, (ev, _) async {
Map data = ev.eventData as Map;
Playback playback = context.read<Playback>();
if (data["property"] == "media-title" && data["value"] != null) {
var track = playback.currentPlaylist?.tracks.where((track) {
String title = data["value"];
return title.contains(track.name!) &&
track.artists!
.every((artist) => title.contains(artist.name!));
});
if (track != null && track.isNotEmpty) {
setState(() {
_isPlaying = true;
});
playback.setCurrentTrack = track.first;
}
}
if (data["property"] == "duration" && data["value"] != null) {
setState(() {
_duration = data["value"];
});
}
playOrPause(key) async {
_isPlaying ? await player.pause() : await player.play();
}
List<GlobalKeyActions> keyWithActions = [
GlobalKeyActions(
HotKey(KeyCode.space, scope: HotKeyScope.inapp),
playOrPause,
),
GlobalKeyActions(
HotKey(KeyCode.mediaPlayPause),
playOrPause,
),
GlobalKeyActions(HotKey(KeyCode.mediaTrackNext), (key) async {
await player.next();
}),
GlobalKeyActions(HotKey(KeyCode.mediaTrackPrevious), (key) async {
await player.prev();
}),
GlobalKeyActions(HotKey(KeyCode.mediaStop), (key) async {
await player.stop();
setState(() {
_isPlaying = false;
_currentPlaylistId = null;
_duration = 0;
_shuffled = false;
});
playback.reset();
})
];
await Future.wait(
keyWithActions.map((e) {
return hotKeyManager.register(
e.hotKey,
keyDownHandler: e.onKeyDown,
);
}),
);
});
} catch (e) {
if (kDebugMode) {
print("[PLAYER]: $e");
}
}
});
super.initState();
}
@override
void dispose() async {
MPVPlayer player = context.read<PlayerDI>().player;
player.removeAllByEvent(MPVEvents.paused);
player.removeAllByEvent(MPVEvents.resumed);
player.removeAllByEvent(MPVEvents.status);
await Future.wait(_hotKeys.map((e) => hotKeyManager.unregister(e)));
super.dispose();
}
String playlistToStr(CurrentPlaylist playlist) {
var tracks = playlist.tracks.map((track) {
var artists = artistsToString(track.artists ?? []);
var title = track.name?.replaceAll("-", " ");
return "ytdl://ytsearch:$artists - $title";
}).toList();
return tracks.join("\n");
}
Future playPlaylist(MPVPlayer player, CurrentPlaylist playlist) async {
try {
if (player.isRunning() && playlist.id != _currentPlaylistId) {
var playlistPath = "/tmp/playlist-${playlist.id}.json";
File file = File(playlistPath);
var newPlaylist = playlistToStr(playlist);
if (!await file.exists()) {
await file.create();
}
await file.writeAsString(newPlaylist);
await player.loadPlaylist(playlistPath);
setState(() {
_currentPlaylistId = playlist.id;
_shuffled = false;
});
}
} catch (e, stackTrace) {
print("[Player]: $e");
print(stackTrace);
}
}
@override
Widget build(BuildContext context) {
MPVPlayer player = context.watch<PlayerDI>().player;
return Container(
color: Theme.of(context).backgroundColor,
child: Consumer<Playback>(
builder: (context, playback, widget) {
if (playback.currentPlaylist != null) {
playPlaylist(player, playback.currentPlaylist!);
}
String? albumArt = playback.currentTrack?.album?.images?.last.url;
return Material(
type: MaterialType.transparency,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
if (albumArt != null)
CachedNetworkImage(
imageUrl: albumArt,
maxHeightDiskCache: 50,
maxWidthDiskCache: 50,
placeholder: (context, url) {
return Container(
height: 50,
width: 50,
color: Colors.green[400],
);
},
),
// title of the currently playing track
Flexible(
flex: 1,
child: Column(
children: [
Text(
playback.currentTrack?.name ?? "Not playing",
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(
artistsToString(playback.currentTrack?.artists ?? []))
],
),
),
// controls
Flexible(
flex: 3,
child: PlayerControls(
player: player,
isPlaying: _isPlaying,
duration: _duration,
shuffled: _shuffled,
onShuffle: () {
if (!_shuffled) {
player.shuffle().then(
(value) => setState(() {
_shuffled = true;
}),
);
} else {
player.unshuffle().then(
(value) => setState(() {
_shuffled = false;
}),
);
}
},
onStop: () {
setState(() {
_isPlaying = false;
_currentPlaylistId = null;
_duration = 0;
_shuffled = false;
});
playback.reset();
},
),
),
// add to saved tracks
Expanded(
flex: 1,
child: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Consumer<SpotifyDI>(builder: (context, data, widget) {
return FutureBuilder<bool>(
future: playback.currentTrack?.id != null
? data.spotifyApi.tracks.me
.containsOne(playback.currentTrack!.id!)
: Future.value(false),
initialData: false,
builder: (context, snapshot) {
bool isLiked = snapshot.data ?? false;
return IconButton(
icon: Icon(
!isLiked
? Icons.favorite_outline_rounded
: Icons.favorite_rounded,
color: isLiked ? Colors.green : null,
),
onPressed: () {
if (!isLiked &&
playback.currentTrack?.id != null) {
data.spotifyApi.tracks.me
.saveOne(playback.currentTrack!.id!)
.then((value) => setState(() {}));
}
});
});
}),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 200),
child: Slider.adaptive(
value: _volume,
onChanged: (value) {
player.volume(value * 100).then((_) {
setState(() {
_volume = value;
});
});
},
),
),
],
),
)
],
),
);
},
),
);
}
}

View File

@ -0,0 +1,143 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mpv_dart/mpv_dart.dart';
import 'package:spotube/helpers/zero-pad-num-str.dart';
class PlayerControls extends StatefulWidget {
final MPVPlayer player;
final bool isPlaying;
final double duration;
final bool shuffled;
final Function? onStop;
final Function? onShuffle;
const PlayerControls({
required this.player,
required this.isPlaying,
required this.duration,
required this.shuffled,
this.onShuffle,
this.onStop,
Key? key,
}) : super(key: key);
@override
_PlayerControlsState createState() => _PlayerControlsState();
}
class _PlayerControlsState extends State<PlayerControls> {
double currentPos = 0;
@override
void initState() {
super.initState();
widget.player.on(MPVEvents.timeposition, null, (ev, context) {
widget.player.getPercentPosition().then((value) {
setState(() {
currentPos = value / 100;
});
});
});
}
@override
void dispose() {
widget.player.removeAllByEvent(MPVEvents.timeposition);
super.dispose();
}
@override
Widget build(BuildContext context) {
var totalDuration = Duration(seconds: widget.duration.toInt());
var totalMinutes = zeroPadNumStr(totalDuration.inMinutes.remainder(60));
var totalSeconds = zeroPadNumStr(totalDuration.inSeconds.remainder(60));
var currentDuration =
Duration(seconds: (widget.duration * currentPos).toInt());
var currentMinutes = zeroPadNumStr(currentDuration.inMinutes.remainder(60));
var currentSeconds = zeroPadNumStr(currentDuration.inSeconds.remainder(60));
return Container(
constraints: const BoxConstraints(maxWidth: 700),
child: Column(
children: [
Row(
children: [
Expanded(
child: Slider.adaptive(
value: currentPos,
onChanged: (value) async {
try {
setState(() {
currentPos = value;
});
await widget.player.goToPosition(value * widget.duration);
} catch (e) {
print("[PlayerControls]: $e");
}
},
),
),
Text(
"$currentMinutes:$currentSeconds/$totalMinutes:$totalSeconds",
)
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
icon: const Icon(Icons.shuffle_rounded),
color:
widget.shuffled ? Theme.of(context).primaryColor : null,
onPressed: () {
widget.onShuffle?.call();
}),
IconButton(
icon: const Icon(Icons.skip_previous_rounded),
onPressed: () async {
bool moved = await widget.player.prev();
if (moved) {
setState(() {
currentPos = 0;
});
}
}),
IconButton(
icon: Icon(
widget.isPlaying
? Icons.pause_rounded
: Icons.play_arrow_rounded,
),
onPressed: () async {
widget.isPlaying
? await widget.player.pause()
: await widget.player.play();
}),
IconButton(
icon: const Icon(Icons.skip_next_rounded),
onPressed: () async {
bool moved = await widget.player.next();
if (moved) {
setState(() {
currentPos = 0;
});
}
}),
IconButton(
icon: const Icon(Icons.stop_rounded),
onPressed: () async {
await widget.player.stop();
widget.onStop?.call();
setState(() {
currentPos = 0;
});
},
)
],
)
],
),
);
}
}

View File

@ -0,0 +1,128 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/PlaylistView.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart';
class PlaylistCard extends StatefulWidget {
final PlaylistSimple playlist;
const PlaylistCard(this.playlist, {Key? key}) : super(key: key);
@override
_PlaylistCardState createState() => _PlaylistCardState();
}
class _PlaylistCardState extends State<PlaylistCard> {
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) {
return PlaylistView(widget.playlist);
},
));
},
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 200),
child: Ink(
decoration: BoxDecoration(
color: Theme.of(context).backgroundColor,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
blurRadius: 10,
offset: const Offset(0, 3),
spreadRadius: 5,
color: Theme.of(context).shadowColor)
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// thumbnail of the playlist
Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage(
imageUrl: widget.playlist.images![0].url!,
progressIndicatorBuilder: (context, url, progress) {
return CircularProgressIndicator.adaptive(
value: progress.progress,
);
},
),
),
Positioned.directional(
textDirection: TextDirection.ltr,
bottom: 10,
end: 5,
child: Builder(builder: (context) {
Playback playback = context.watch<Playback>();
SpotifyDI data = context.watch<SpotifyDI>();
bool isPlaylistPlaying = playback.currentPlaylist !=
null &&
playback.currentPlaylist!.id == widget.playlist.id;
return ElevatedButton(
onPressed: () async {
if (isPlaylistPlaying) return;
List<Track> tracks =
(widget.playlist.id != "user-liked-tracks"
? await data.spotifyApi.playlists
.getTracksByPlaylistId(
widget.playlist.id!)
.all()
: await data.spotifyApi.tracks.me.saved
.all()
.then((tracks) =>
tracks.map((e) => e.track!)))
.toList();
playback.setCurrentPlaylist = CurrentPlaylist(
tracks: tracks,
id: widget.playlist.id!,
name: widget.playlist.name!,
thumbnail: widget.playlist.images!.first.url!,
);
},
child: Icon(
isPlaylistPlaying
? Icons.pause_rounded
: Icons.play_arrow_rounded,
),
style: ButtonStyle(
shape: MaterialStateProperty.all(
const CircleBorder(),
),
padding: MaterialStateProperty.all(
const EdgeInsets.all(16),
),
),
);
}),
)
],
),
const SizedBox(height: 5),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
child: Column(
children: [
Text(
widget.playlist.name!,
style: const TextStyle(fontWeight: FontWeight.bold),
),
],
),
)
],
),
),
),
);
}
}

View File

@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/PageWindowTitleBar.dart';
import 'package:spotube/components/PlaylistCard.dart';
import 'package:spotube/provider/SpotifyDI.dart';
class PlaylistGenreView extends StatefulWidget {
final String genreId;
final String genreName;
final Iterable<PlaylistSimple>? playlists;
const PlaylistGenreView(
this.genreId,
this.genreName, {
this.playlists,
Key? key,
}) : super(key: key);
@override
_PlaylistGenreViewState createState() => _PlaylistGenreViewState();
}
class _PlaylistGenreViewState extends State<PlaylistGenreView> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
const PageWindowTitleBar(
leading: BackButton(),
),
Text(
widget.genreName,
style: Theme.of(context).textTheme.headline4,
textAlign: TextAlign.center,
),
Consumer<SpotifyDI>(
builder: (context, data, child) => Expanded(
child: SingleChildScrollView(
child: FutureBuilder<Iterable<PlaylistSimple>>(
future: widget.playlists == null
? (widget.genreId != "user-featured-playlists"
? data.spotifyApi.playlists
.getByCategoryId(widget.genreId)
.all()
: data.spotifyApi.playlists.featured.all())
: Future.value(widget.playlists),
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Center(child: Text("Error occurred"));
}
if (!snapshot.hasData) {
return const CircularProgressIndicator.adaptive();
}
return Wrap(
children: snapshot.data!
.map(
(playlist) => Padding(
padding: const EdgeInsets.all(8.0),
child: PlaylistCard(playlist),
),
)
.toList(),
);
}),
),
),
)
],
),
);
}
}

View File

@ -0,0 +1,230 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:spotube/components/PageWindowTitleBar.dart';
import 'package:spotube/helpers/zero-pad-num-str.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/provider/SpotifyDI.dart';
class PlaylistView extends StatefulWidget {
final PlaylistSimple playlist;
const PlaylistView(this.playlist, {Key? key}) : super(key: key);
@override
_PlaylistViewState createState() => _PlaylistViewState();
}
class _PlaylistViewState extends State<PlaylistView> {
List<TableRow> trackToTableRow(List<Track> tracks) {
return tracks.asMap().entries.map((track) {
var thumbnailUrl = track.value.album?.images?.last.url;
var duration =
"${track.value.duration?.inMinutes.remainder(60)}:${zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
return (TableRow(
children: [
TableCell(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
(track.key + 1).toString(),
textAlign: TextAlign.center,
),
)),
TableCell(
child: Row(
children: [
if (thumbnailUrl != null)
Padding(
padding: const EdgeInsets.all(8.0),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(5)),
child: CachedNetworkImage(
placeholder: (context, url) {
return Container(
height: 40,
width: 40,
color: Colors.green[300],
);
},
imageUrl: thumbnailUrl,
maxHeightDiskCache: 40,
maxWidthDiskCache: 40,
),
),
),
const SizedBox(width: 10),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
track.value.name ?? "",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 17,
),
overflow: TextOverflow.ellipsis,
),
Text(
(track.value.artists ?? [])
.map((e) => e.name)
.join(", "),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
TableCell(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
track.value.album?.name ?? "",
overflow: TextOverflow.ellipsis,
),
),
),
TableCell(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
duration,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
),
)
],
));
}).toList();
}
@override
Widget build(BuildContext context) {
return Consumer<SpotifyDI>(builder: (_, data, __) {
return Scaffold(
body: FutureBuilder<Iterable<Track>>(
future: widget.playlist.id != "user-liked-tracks"
? data.spotifyApi.playlists
.getTracksByPlaylistId(widget.playlist.id)
.all()
: data.spotifyApi.tracks.me.saved
.all()
.then((tracks) => tracks.map((e) => e.track!)),
builder: (context, snapshot) {
List<Track> tracks = snapshot.data?.toList() ?? [];
TextStyle tableHeadStyle =
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16);
return Column(
children: [
PageWindowTitleBar(
leading: Row(
children: [
// nav back
const BackButton(),
// heart playlist
IconButton(
icon: const Icon(Icons.favorite_outline_rounded),
onPressed: () {},
),
// play playlist
Consumer<Playback>(
builder: (context, playback, widget) {
var isPlaylistPlaying =
playback.currentPlaylist?.id ==
this.widget.playlist.id;
return IconButton(
icon: Icon(
isPlaylistPlaying
? Icons.stop_rounded
: Icons.play_arrow_rounded,
),
onPressed: snapshot.hasData
? () {
if (!isPlaylistPlaying) {
playback.setCurrentPlaylist =
CurrentPlaylist(
tracks: tracks,
id: this.widget.playlist.id!,
name: this.widget.playlist.name!,
thumbnail: this
.widget
.playlist
.images![0]
.url!,
);
}
}
: null,
);
}),
],
),
),
Center(
child: Text(widget.playlist.name!,
style: Theme.of(context).textTheme.headline4),
),
snapshot.hasError
? const Center(child: Text("Error occurred"))
: !snapshot.hasData
? const Expanded(
child: Center(
child: CircularProgressIndicator.adaptive()),
)
: Expanded(
child: Scrollbar(
child: ListView(
children: [
SingleChildScrollView(
child: Table(
columnWidths: const {
0: FixedColumnWidth(40),
1: FlexColumnWidth(),
2: FlexColumnWidth(),
3: FixedColumnWidth(45),
},
children: [
TableRow(
children: [
TableCell(
child: Text(
"#",
textAlign: TextAlign.center,
style: tableHeadStyle,
)),
TableCell(
child: Text(
"Title",
style: tableHeadStyle,
)),
TableCell(
child: Text(
"Album",
style: tableHeadStyle,
)),
TableCell(
child: Text(
"Time",
textAlign: TextAlign.center,
style: tableHeadStyle,
)),
],
),
...trackToTableRow(tracks),
],
),
),
],
),
),
),
],
);
}),
);
});
}
}

Some files were not shown because too many files have changed in this diff Show More