import 'dart:html';
import 'dart:math';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/painting.dart';
void main() => runApp(SpotifyApp());
class SpotifyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: MainScreen(),
);
}
}
class MainScreen extends StatefulWidget {
@override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
static int audioId = 0;
final _audio = Utils.audio(null, audioId, true);
final _audioKey = GlobalKey<PlayerSectionState>();
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < 650;
return Material(
child: isMobile
? MobileSection(audio: _audio, audioKey: _audioKey)
: HomeSection(audio: _audio, audioKey: _audioKey),
);
}
}
class MobileSection extends StatefulWidget {
const MobileSection({Key key, this.audio, this.audioKey}) : super(key: key);
final Tuple<Widget, AudioElement> audio;
final GlobalKey<PlayerSectionState> audioKey;
@override
_MobileSectionState createState() => _MobileSectionState();
}
class _MobileSectionState extends State<MobileSection> with SingleTickerProviderStateMixin {
Animation<double> _fadeAnimation;
AnimationController _fadeController;
GenericClick<SectionType> _genericClick;
Widget body;
List<Widget> _bodies = [];
final listKey = GlobalKey<ListScreenState>();
@override
void initState() {
super.initState();
_homeClick();
_setupAnimation();
Future.microtask(() {
_bodies.add(HomeScreen(isMobile: true, click: _genericClick));
_bodies.add(ListScreen(isMobile: true, key: listKey, click: _genericClick));
setState(() => body = _bodies[0]);
});
}
void _setupAnimation() {
_fadeController = AnimationController(duration: Duration(milliseconds: 300), vsync: this);
_fadeAnimation = Tween<double>(begin: 0, end: 1).animate(_fadeController);
_fadeController.forward(from: 1);
}
void _homeClick() {
_genericClick = (section, item) {
switch (section) {
case SectionType.BROWSE:
_fadeController.forward(from: 0);
setState(() => body = _bodies[0]);
break;
case SectionType.ALBUM:
_fadeController.forward(from: 0);
setState(() => body = _bodies[1]);
Future.delayed(Duration(milliseconds: 100), () {
listKey.currentState.setPlaylist(item);
});
break;
case SectionType.HOME:
// TODO: Handle this case.
break;
case SectionType.BROWSE_PODCAST:
// TODO: Handle this case.
break;
case SectionType.PODCAST:
// TODO: Handle this case.
break;
case SectionType.GRID:
// TODO: Handle this case.
break;
case SectionType.GRID_CIRCLE:
// TODO: Handle this case.
break;
case SectionType.GRID_SECTION:
// TODO: Handle this case.
break;
case SectionType.LIST:
// TODO: Handle this case.
break;
case SectionType.LIST_IMAGE:
// TODO: Handle this case.
break;
case SectionType.ARTIST:
// TODO: Handle this case.
break;
case SectionType.SONG:
// TODO: Handle this case.
break;
case SectionType.AUDIO:
Utils.currentSong = item;
handlePlayer(item);
break;
case SectionType.FULLSCREEN:
break;
}
};
}
void handlePlayer(Song song) {
if (widget.audio.second.currentSrc == song.url) {
if (widget.audio.second.paused) {
widget.audio.second.play();
window.navigator.mediaSession.setActionHandler('play', () {
window.navigator.mediaSession.playbackState = "playing";
});
} else {
widget.audio.second.pause();
window.navigator.mediaSession.setActionHandler('pause', () {
window.navigator.mediaSession.playbackState = "paused";
});
}
widget.audioKey.currentState.refresh(song);
} else {
widget.audio.second.src = song.url;
widget.audio.second.currentTime = 0.0;
widget.audio.second.play();
window.navigator.mediaSession.metadata = MediaMetadata()
..title = song.name
..artist = song.artist.name
..album = song.album.name
..artwork = [Src(song.image)];
window.navigator.mediaSession.setActionHandler('play', () {
widget.audio.second.play();
window.navigator.mediaSession.playbackState = "playing";
});
window.navigator.mediaSession.setActionHandler('pause', () {
widget.audio.second.pause();
window.navigator.mediaSession.playbackState = "paused";
});
widget.audio.second.onLoadedData.listen((event) {
widget.audioKey.currentState.refresh(song);
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: FadeTransition(opacity: _fadeAnimation, child: body),
bottomNavigationBar: Column(
mainAxisSize: MainAxisSize.min,
children: [
PlayerSection(key: widget.audioKey, audio: widget.audio.second, isMobile: true),
Line(horizontal: true, color: Colors.black),
BottomNavigationBar(
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('Home')),
BottomNavigationBarItem(icon: Icon(Icons.search), title: Text('Search')),
BottomNavigationBarItem(icon: Icon(Icons.library_books), title: Text('Your Library')),
],
elevation: 0,
backgroundColor: Utils.playerSectionBg,
type: BottomNavigationBarType.fixed,
selectedItemColor: Colors.white,
unselectedItemColor: Colors.grey,
onTap: (index) {
switch (index) {
case 0:
_genericClick(SectionType.BROWSE, null);
break;
}
},
),
],
),
);
}
}
class HomeSection extends StatefulWidget {
const HomeSection({Key key, this.audio, this.audioKey}) : super(key: key);
final Tuple<Widget, AudioElement> audio;
final GlobalKey<PlayerSectionState> audioKey;
@override
_HomeSectionState createState() => _HomeSectionState();
}
class _HomeSectionState extends State<HomeSection> {
GenericClick<SectionType> _genericClick;
ItemClick _itemClick;
bool _switchDesktop = true;
int index = 0;
bool _fullScreen = false;
Widget body = Container();
List<Widget> _bodies = [];
final listKey = GlobalKey<ListScreenState>();
@override
void initState() {
super.initState();
Future.microtask(homeClick);
Future.microtask(itemClick);
Future.microtask(() {
_bodies.add(HomeScreen(isMobile: false, click: _genericClick));
_bodies.add(ListScreen(isMobile: false, key: listKey, click: _genericClick));
setState(() => body = _bodies[0]);
});
}
void homeClick() {
_genericClick = (section, item) {
switch (section) {
case SectionType.BROWSE:
setState(() => body = _bodies[0]);
break;
case SectionType.ALBUM:
setState(() => body = _bodies[1]);
Future.delayed(Duration(milliseconds: 100), () {
listKey.currentState.setPlaylist(item);
});
break;
case SectionType.HOME:
// TODO: Handle this case.
break;
case SectionType.BROWSE_PODCAST:
// TODO: Handle this case.
break;
case SectionType.PODCAST:
// TODO: Handle this case.
break;
case SectionType.GRID:
// TODO: Handle this case.
break;
case SectionType.GRID_CIRCLE:
// TODO: Handle this case.
break;
case SectionType.GRID_SECTION:
// TODO: Handle this case.
break;
case SectionType.LIST:
// TODO: Handle this case.
break;
case SectionType.LIST_IMAGE:
// TODO: Handle this case.
break;
case SectionType.ARTIST:
// TODO: Handle this case.
break;
case SectionType.SONG:
// TODO: Handle this case.
break;
case SectionType.AUDIO:
Utils.currentSong = item;
handlePlayer(item);
break;
case SectionType.FULLSCREEN:
setState(() => _fullScreen = item);
if (_fullScreen) {
document.documentElement.requestFullscreen();
} else {
setState(() => body = _bodies[0]);
document.exitFullscreen();
}
break;
}
};
}
void handlePlayer(Song song) {
if (widget.audio.second.currentSrc == song.url) {
if (widget.audio.second.paused) {
widget.audio.second.play();
} else {
widget.audio.second.pause();
}
widget.audioKey.currentState.refresh(song);
} else {
widget.audio.second.pause();
widget.audio.second.src = song.url;
widget.audio.second.load();
window.navigator.mediaSession.metadata = MediaMetadata()
..title = song.name
..artist = song.artist.name
..album = song.album.name
..artwork = [Src(song.image)];
window.navigator.mediaSession.setActionHandler('play', () {
widget.audio.second.play();
window.navigator.mediaSession.playbackState = "playing";
});
window.navigator.mediaSession.setActionHandler('pause', () {
widget.audio.second.pause();
window.navigator.mediaSession.playbackState = "paused";
});
widget.audio.second.onLoadedData.listen((event) {
widget.audioKey.currentState.refresh(song);
widget.audioKey.currentState.updateTime();
});
}
}
void itemClick() {
setState(() => _itemClick = (_index) => index = _index);
selectItem();
}
void setIndex() {
Future.microtask(() {
_switchDesktop = true;
selectItem();
});
}
void selectItem() {
Utils.itemSelection(Utils.leftSectionItems, false);
if (Utils.leftSectionItems[index].key.currentState != null) {
Utils.leftSectionItems[index].key.currentState.selected(true);
}
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final isDesktop = MediaQuery.of(context).size.width > 1000;
final isMobile = MediaQuery.of(context).size.width < 650;
if (isMobile) _switchDesktop = false;
if (!_switchDesktop && !isMobile) setIndex();
return Column(
children: [
Expanded(
child: !_fullScreen
? Row(
children: [
if (!isMobile) LeftSection(itemClick: _itemClick),
Expanded(child: body),
if (isDesktop) RightSection(),
],
)
: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.black,
Color(0xFF626262),
Colors.black,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
width: size.width,
alignment: Alignment.centerRight,
margin: const EdgeInsets.only(right: 60, top: 40),
child: GestureDetector(
onTap: () {
_genericClick(SectionType.FULLSCREEN, false);
document.exitFullscreen();
},
child: Container(
height: 50,
width: 50,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(25),
border: Border.all(color: Colors.white, width: 1),
),
child: Center(
child: Icon(Icons.fullscreen_exit, color: Colors.white, size: 40),
),
),
),
),
Space(size: 60),
Expanded(
child: AspectRatio(
aspectRatio: 1,
child: Card(
elevation: 10,
shape: RoundedRectangleBorder(),
child: Image.network(Utils.currentSong.image, fit: BoxFit.cover),
),
),
),
Space(size: 20),
Text(
Utils.currentSong.name,
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 40),
),
Space(size: 8),
Text(
Utils.currentSong.artist.name,
style: TextStyle(color: Colors.grey, fontWeight: FontWeight.w600, fontSize: 25),
),
Space(size: 60),
],
),
),
),
PlayerSection(click: _genericClick, key: widget.audioKey, audio: widget.audio.second, isMobile: false)
],
);
}
}
class LeftSection extends StatefulWidget {
const LeftSection({Key key, this.itemClick}) : super(key: key);
final ItemClick itemClick;
@override
LeftSectionState createState() => LeftSectionState();
}
class LeftSectionState extends State<LeftSection> {
@override
void initState() {
super.initState();
Future.microtask(() => Utils.leftSectionItems[0].selected = true);
}
@override
Widget build(BuildContext context) {
return Container(
width: 180,
color: Utils.leftPanelBg,
padding: const EdgeInsets.only(top: 12),
child: Column(
children: [
WindowButtons(),
Space(size: 30),
LeftListItem(
key: Utils.leftSectionItems[0].key,
icon: Icons.home,
item: Utils.leftSectionItems[0],
itemClick: widget.itemClick,
index: 0,
),
Space(size: 12),
LeftListItem(
key: Utils.leftSectionItems[1].key,
icon: Icons.open_in_browser,
item: Utils.leftSectionItems[1],
itemClick: widget.itemClick,
index: 1,
),
Space(size: 12),
LeftListItem(
key: Utils.leftSectionItems[2].key,
icon: Icons.radio,
item: Utils.leftSectionItems[2],
itemClick: widget.itemClick,
index: 2,
),
Space(size: 40),
Expanded(
child: ListView.builder(
itemBuilder: (context, i) {
final _index = i + 3;
final item = Utils.leftSectionItems[_index];
return LeftListItem(key: item.key, item: item, itemClick: widget.itemClick, index: _index);
},
itemCount: Utils.leftSectionItems.length - 3,
),
),
Line(horizontal: true),
AddPlayList(),
],
),
);
}
}
class RightSection extends StatefulWidget {
@override
_RightSectionState createState() => _RightSectionState();
}
class _RightSectionState extends State<RightSection> {
@override
Widget build(BuildContext context) {
return Container(
width: 250,
color: Utils.rightPanelBg,
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
children: [
Text(
'Friend Activity',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 20),
),
Space(size: 10),
Container(height: 1, color: Colors.grey[800]),
Space(size: 15),
...Utils.users.map((item) => FriendsItem(user: item)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: OutlineButton(
onPressed: () {},
highlightedBorderColor: Colors.grey[700],
borderSide: BorderSide(color: Colors.white, width: 2.5),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
child: Text('FIND FRIENDS', style: TextStyle(color: Colors.white, fontSize: 12)),
),
),
],
),
);
}
}
class FriendsItem extends StatelessWidget {
const FriendsItem({Key key, this.user}) : super(key: key);
final User user;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.only(bottom: 30),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
height: 40,
width: 40,
child: CircleAvatar(
backgroundColor: Colors.grey[800],
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Image.network(user.image, fit: BoxFit.cover),
),
),
),
Space(size: 15, horizontal: true),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
user.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 15),
),
),
Space(size: 5, horizontal: true),
Text(user.time, style: TextStyle(color: Colors.grey, fontSize: 12)),
],
),
Space(size: 5),
Padding(
padding: const EdgeInsets.only(right: 20),
child: Text(user.song, style: TextStyle(color: Colors.grey, fontSize: 12)),
),
Space(size: 5),
Padding(
padding: const EdgeInsets.only(right: 20),
child: Text(user.playlist, style: TextStyle(color: Colors.grey, fontSize: 11)),
),
],
),
),
],
),
);
}
}
class PlayerSection extends StatefulWidget {
const PlayerSection({Key key, this.click, this.audio, this.isMobile}) : super(key: key);
final GenericClick<SectionType> click;
final AudioElement audio;
final bool isMobile;
@override
PlayerSectionState createState() => PlayerSectionState();
}
class PlayerSectionState extends State<PlayerSection> {
Song _song;
double _current = 0.0;
var _volume = 1.0;
bool _disposed = false;
bool _paused = false;
void refresh(Song song) => setState(() => _song = song);
void updateTime() {
widget.audio.onTimeUpdate.listen((event) {
if (!_disposed) {
setState(() => _current = widget.audio.currentTime);
}
});
}
@override
void initState() {
super.initState();
if (mounted) {
Future.microtask(() {
widget.audio.onPlay.listen((event) {
if (!_disposed) {
setState(() => _paused = false);
}
});
widget.audio.onPause.listen((event) {
if (!_disposed) {
setState(() => _paused = true);
}
});
});
}
}
@override
void dispose() {
_disposed = true;
super.dispose();
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return GestureDetector(
onTap: widget.isMobile
? () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => PlayerScreen(audio: widget.audio, song: _song),
fullscreenDialog: true,
),
);
}
: null,
child: Container(
color: Utils.playerSectionBg,
child: widget.isMobile
? AnimatedContainer(
duration: Duration(milliseconds: 200),
height: _song != null ? 70 : 0,
child: Row(
children: [
Container(
width: 70,
height: 70,
child: Image.network(_song != null ? _song.image : '', fit: BoxFit.cover)),
Space(size: 20, horizontal: true),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_song != null ? '${_song.name} - ${_song.artist.name}' : '',
style: TextStyle(color: Colors.white), maxLines: 1, overflow: TextOverflow.ellipsis),
Space(size: 8),
Row(
children: [
Icon(Icons.devices, color: Colors.white, size: 14),
Space(size: 5, horizontal: true),
Expanded(
child: Text('Available Devices', style: TextStyle(color: Colors.white, fontSize: 12),
maxLines: 1, overflow: TextOverflow.ellipsis),
),
],
)
],
),
),
Space(size: 20, horizontal: true),
Icon(Icons.favorite_border, color: Colors.white),
Space(size: 20, horizontal: true),
GestureDetector(
onTap: handlePlayer,
child: Icon(_paused ? Icons.play_arrow : Icons.pause, color: Colors.white, size: 30),
),
Space(size: 20, horizontal: true),
],
),
)
: Container(
height: 90,
width: size.width,
color: Utils.playerSectionBg,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
Flexible(
flex: 1,
fit: FlexFit.tight,
child: _song != null
? Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(right: 15),
child: SizedBox(
height: 65,
width: 65,
child: Image.network(_song.image, fit: BoxFit.cover),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(_song.name,
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600)),
Space(size: 10, horizontal: true),
Icon(Icons.favorite_border, color: Colors.grey, size: 15),
],
),
Text(_song.artist.name, style: TextStyle(color: Colors.grey)),
],
)
],
)
: SizedBox(),
),
Flexible(
flex: 2,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
flex: 2,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.shuffle, color: Colors.white, size: 15),
Space(size: 20, horizontal: true),
Icon(Icons.skip_previous, color: Colors.white, size: 18),
Space(size: 20, horizontal: true),
GestureDetector(
onTap: _song != null
? () => widget.audio.paused ? widget.audio.play() : widget.audio.pause()
: null,
child: Icon(
widget.audio.paused ? Icons.play_circle_filled : Icons.pause_circle_filled,
color: Colors.white,
size: 40),
),
Space(size: 20, horizontal: true),
Icon(Icons.skip_next, color: Colors.white, size: 18),
Space(size: 20, horizontal: true),
Icon(Icons.repeat, color: Colors.white, size: 15),
],
),
),
Flexible(
flex: 1,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Space(size: 20, horizontal: true),
if (_song != null)
Container(
width: 35,
child: Text(Utils.parseTime(_current),
style: TextStyle(color: Colors.white, fontSize: 12)),
),
Expanded(
child: SliderTheme(
data: SliderThemeData(
trackHeight: 6,
overlayShape: RoundSliderOverlayShape(overlayRadius: 0),
thumbShape:
RoundSliderThumbShape(enabledThumbRadius: 6, disabledThumbRadius: 6),
thumbColor: Colors.white,
activeTrackColor: Colors.green,
),
child: Slider(
value: _song != null ? _current : 0,
activeColor: Colors.white,
inactiveColor: Colors.grey[800],
max: _song != null && !widget.audio.duration.isNaN
? widget.audio.duration
: 1,
onChanged: _song != null
? (value) {
setState(() {
_current = value;
widget.audio.currentTime = _current;
});
}
: null,
),
),
),
if (_song != null)
Text(Utils.parseTime(!widget.audio.duration.isNaN ? widget.audio.duration : 0),
style: TextStyle(color: Colors.white, fontSize: 12)),
Space(size: 20, horizontal: true),
],
),
),
],
),
),
Flexible(
flex: 1,
fit: FlexFit.tight,
child: _song != null
? Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Icon(Icons.playlist_play, color: Colors.white, size: 18),
Space(size: 10, horizontal: true),
Icon(Icons.devices, color: Colors.white, size: 15),
Space(size: 12, horizontal: true),
Icon(Icons.volume_up, color: Colors.white, size: 15),
Space(size: 10, horizontal: true),
Container(
width: 50,
child: SliderTheme(
data: SliderThemeData(
trackHeight: 4,
overlayShape: RoundSliderOverlayShape(overlayRadius: 0),
thumbShape:
RoundSliderThumbShape(enabledThumbRadius: 5, disabledThumbRadius: 0),
thumbColor: Colors.white,
activeTrackColor: Colors.green,
),
child: Slider(
value: _volume,
max: 1,
onChanged: (value) {
setState(() {
widget.audio.volume = value;
_volume = value;
});
},
),
),
),
Space(size: 10, horizontal: true),
GestureDetector(
onTap: () {
widget.click(SectionType.FULLSCREEN, window.innerHeight != window.screen.height);
},
child: Icon(
window.innerHeight == window.screen.height
? Icons.fullscreen_exit
: Icons.fullscreen,
color: Colors.white,
size: 15),
),
],
)
: SizedBox(),
)
],
),
),
),
),
);
}
void handlePlayer() {
setState(() {
if (widget.audio.paused) {
widget.audio.play();
_paused = false;
} else {
widget.audio.pause();
_paused = true;
}
});
}
}
class WindowButtons extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
Space(size: 20, horizontal: true),
ClipOval(child: Container(color: Colors.red, width: 12, height: 12)),
Space(size: 10, horizontal: true),
ClipOval(child: Container(color: Colors.yellow, width: 12, height: 12)),
Space(size: 10, horizontal: true),
ClipOval(child: Container(color: Colors.green, width: 12, height: 12)),
],
);
}
}
class LeftListItem extends StatefulWidget {
const LeftListItem({Key key, this.itemClick, this.icon, this.item, this.index}) : super(key: key);
final ItemClick itemClick;
final IconData icon;
final Item item;
final int index;
@override
LeftListItemState createState() => LeftListItemState();
}
class LeftListItemState extends State<LeftListItem> {
bool _selected = false;
bool _hover = false;
void selected(bool selected) => setState(() => _selected = selected);
@override
Widget build(BuildContext context) {
return widget.item.text != null
? GestureDetector(
onTap: () {
Utils.itemSelection(Utils.leftSectionItems, false);
selected(true);
widget.itemClick(widget.index);
},
child: MouseRegion(
onHover: (_) => setState(() => _hover = true),
onExit: (_) => setState(() => _hover = false),
child: Container(
padding: EdgeInsets.only(
top: when(widget.item.type, {
TextType.HEADER: 0,
TextType.NORMAL: 8,
TextType.TITLE: 4,
TextType.BOLD: 8,
}),
bottom: when(widget.item.type, {
TextType.HEADER: 0,
TextType.NORMAL: 8,
TextType.TITLE: 4,
TextType.BOLD: 8,
}),
right: 10),
child: Row(
children: [
if (_selected)
Container(
width: 4,
color: Utils.selectedItem,
height: when(widget.item.type, {
TextType.HEADER: 20,
TextType.NORMAL: 15,
TextType.TITLE: 0,
TextType.BOLD: 15,
})),
if (widget.item.type == TextType.HEADER) Space(size: _selected ? 16 : 20, horizontal: true),
if (widget.icon != null)
Icon(widget.icon, color: _selected ? Colors.white : _hover ? Colors.white : Colors.grey),
if (widget.icon == null) Space(size: _selected ? 16 : 20, horizontal: true),
if (widget.icon != null) Space(size: 15, horizontal: true),
Text(widget.item.text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: widget.item.type == TextType.TITLE
? Colors.grey
: _selected ? Colors.white : _hover ? Colors.white : Colors.grey,
fontWeight: when(widget.item.type, {
TextType.HEADER: FontWeight.w600,
TextType.NORMAL: FontWeight.w400,
TextType.TITLE: FontWeight.w300,
TextType.BOLD: FontWeight.w600,
}),
fontSize: when(widget.item.type, {
TextType.HEADER: 14,
TextType.NORMAL: 14,
TextType.TITLE: 12,
TextType.BOLD: 14,
})))
],
),
),
),
)
: Space(size: 20);
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({Key key, this.isMobile, this.click}) : super(key: key);
final GenericClick<SectionType> click;
final bool isMobile;
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final _controller = ScrollController();
var _opacity = 1.0;
var _barOpacity = 0.0;
var _height = 0.0;
@override
void initState() {
super.initState();
_controller.addListener(() {
setState(() {
if (_controller.offset < 51) {
_barOpacity = (_controller.offset - 0.0) / (50.0 - 0.0);
_height = (_controller.offset - 0.0) / (50.0 - 0.0);
} else if (_controller.offset > 50) {
_barOpacity = 1.0;
_height = 1.0;
}
});
if (_opacity == 1.0 && _controller.offset > 20) {
setState(() => _opacity = 0.0);
} else if (_opacity == 0.0 && _controller.offset < 20) {
setState(() => _opacity = 1.0);
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return Container(
width: size.width,
height: size.height,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0xFF404040),
Color(0xFF181818),
Color(0xFF181818),
Color(0xFF181818),
Color(0xFF181818),
],
begin: Alignment(-0.4, -1.2),
end: Alignment.bottomCenter,
),
),
child: Stack(
children: [
if (widget.isMobile)
AnimatedOpacity(
duration: Duration(milliseconds: 200),
opacity: _opacity,
child: MobileAppBar(showMenu: true, icon: Icons.settings),
),
ListView(
controller: _controller,
padding: EdgeInsets.only(top: widget.isMobile ? 10 : 140, bottom: 10),
children: [
...Utils.homeList
.asMap()
.entries
.map((item) => PlaylistItem(click: widget.click, playlist: item.value, small: item.key == 0))
],
),
if (!widget.isMobile)
Container(
height: 170 - (_height * 40),
color: Color(0xFF121212).withOpacity(_barOpacity > 0.6 ? 1 : _barOpacity < 0.1 ? 0 : _barOpacity),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Space(size: 5),
Row(
children: [
Icon(Icons.keyboard_arrow_left, color: Colors.grey, size: 35),
Space(size: 5, horizontal: true),
Icon(Icons.keyboard_arrow_right, color: Colors.grey, size: 35),
Space(size: 8, horizontal: true),
Container(
height: 26,
width: 180,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(13),
),
child: Row(
children: [
Space(size: 5, horizontal: true),
Icon(Icons.search, color: Colors.grey[800], size: 18),
Space(size: 5, horizontal: true),
Text('Search', style: TextStyle(color: Colors.grey[800]))
],
),
),
Expanded(child: SizedBox()),
Container(
height: 30,
width: 30,
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Image.network(Utils.mainUser.image, fit: BoxFit.cover),
),
),
Space(size: 8, horizontal: true),
Text(Utils.mainUser.name, style: TextStyle(color: Colors.white)),
Space(size: 15, horizontal: true),
Icon(Icons.keyboard_arrow_down, color: Colors.grey, size: 35),
Space(size: 10, horizontal: true),
],
),
Expanded(child: SizedBox()),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Text('Home',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 30)),
),
Space(size: 15),
],
),
),
)
],
),
);
}
}
class ListScreen extends StatefulWidget {
const ListScreen({Key key, this.isMobile, this.click}) : super(key: key);
final GenericClick<SectionType> click;
final bool isMobile;
@override
ListScreenState createState() => ListScreenState();
}
class ListScreenState extends State<ListScreen> {
final _scrollController = ScrollController();
final _threshold = 306;
bool _playSticky = false;
double percentage = 0.0;
var _barOpacity = 0.0;
var _height = 0.0;
Playlist _playlist;
void setPlaylist(Playlist playlist) => setState(() => _playlist = playlist);
@override
void initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.offset < 51) {
_barOpacity = (_scrollController.offset - 0.0) / (50.0 - 0.0);
} else if (_scrollController.offset > 50) {
_barOpacity = 1.0;
}
if (_scrollController.offset < 180) {
_height = (_scrollController.offset - 0.0) / (180.0 - 0.0);
} else if (_scrollController.offset > 180) {
_height = 1.0;
}
setState(() {
percentage = _scrollController.offset / _threshold >= 1
? 1
: _scrollController.offset / _threshold <= 0 ? 0 : _scrollController.offset / _threshold;
});
if (percentage >= 1 && !_playSticky) {
setState(() => _playSticky = true);
} else {
if (percentage < 1 && _playSticky) {
setState(() => _playSticky = false);
}
}
});
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return widget.isMobile
? Stack(
children: [
Container(
width: size.width,
height: size.height,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF404040), Color(0xFF181818)],
begin: Alignment(0.5, -1.5),
end: Alignment(0.5, 0.2),
),
),
),
if (_playlist != null && _playlist.element == ElementType.ARTIST)
Container(
height: 380 - (percentage * 100),
alignment: Alignment.bottomCenter,
child: Stack(
fit: StackFit.expand,
children: [
Image.network(
_playlist != null && _playlist.image != null ? _playlist.image : '',
fit: BoxFit.cover,
),
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0xFF181818),
Color(0x10181818),
],
begin: Alignment(0.0, 1.0),
end: Alignment(0.0, -1.0),
)),
)
],
),
),
Opacity(
opacity: percentage,
child: Container(
width: size.width,
height: size.height,
color: Color(0xFF181818),
),
),
Builder(
builder: (BuildContext context) {
final scale = _playlist != null && _playlist.element == ElementType.ARTIST
? pow(percentage, 1.5)
: pow(percentage, 10);
final fade = 1.2 * 1 - percentage;
return Transform(
transform: Matrix4.identity()
..scale(1 - (scale) < 0.8 ? 0.8 : 1 - scale, 1 - scale < 0.8 ? 0.8 : 1 - scale),
alignment: Alignment.bottomCenter,
child: Opacity(
opacity: fade < 0 ? 0 : fade > 1 ? 1 : fade,
child: Container(
height: 260,
width: size.width,
margin: const EdgeInsets.only(top: 60),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (!(_playlist != null && _playlist.element == ElementType.ARTIST))
SizedBox(
height: 200,
width: 200,
child: Image.network(
_playlist != null && _playlist.image != null ? _playlist.image : '',
fit: BoxFit.cover,
),
),
Space(size: 20),
Text(
_playlist != null ? _playlist.name : '',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: _playlist != null && _playlist.element == ElementType.ARTIST
? FontWeight.w700
: FontWeight.w600,
fontSize: _playlist != null && _playlist.element == ElementType.ARTIST ? 50 : 25,
color: Colors.white,
),
)
],
),
),
),
);
},
),
Container(
margin: const EdgeInsets.only(top: 50),
child: ListView(
controller: _scrollController,
padding: const EdgeInsets.only(top: 300),
children: [
Container(
width: size.width,
height: 60,
child: Stack(
children: [
Align(
alignment: Alignment.bottomCenter,
child: Container(
color: Color(0xFF181818),
height: 30,
),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
height: 60,
margin: const EdgeInsets.only(bottom: 30),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0xFF181818),
Color(0x10181818),
],
begin: Alignment(0.0, 1.0),
end: Alignment(0.0, -1.0),
)),
),
),
GestureDetector(
onTap: () {
widget.click(SectionType.AUDIO, (_playlist.elements as List<Song>)[0]);
},
child: Center(
child: FittedBox(
child: Container(
height: 45,
padding: const EdgeInsets.symmetric(horizontal: 50),
decoration: BoxDecoration(
color: Color(0xFF1EBA54),
borderRadius: BorderRadius.circular(30),
),
child: Center(
child: Text(
_playlist != null && _playlist.element == ElementType.ARTIST
? 'SHUFFLE PLAY'
: 'PLAY',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600))),
),
),
),
),
],
),
),
Container(
color: Color(0xFF181818),
width: size.width,
height: 40,
child: Row(
children: [
Space(size: 20, horizontal: true),
Text('Download', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600)),
Expanded(child: SizedBox()),
Switch(
value: false,
onChanged: (_) {},
activeColor: Colors.grey,
inactiveTrackColor: Colors.grey),
Space(size: 20, horizontal: true),
],
),
),
Space(size: 10, color: Utils.sectionBg),
if (_playlist != null)
..._playlist.elements.map((song) => SongItem(click: widget.click, song: song))
],
),
),
MobileAppBar(
showMenu: true,
title: _playlist != null ? _playlist.name : '',
opacity: percentage,
click: widget.click),
Visibility(
visible: _playSticky,
child: Container(
margin: const EdgeInsets.only(top: 40),
width: size.width,
height: 60,
child: Stack(
children: [
Align(
alignment: Alignment.topCenter,
child: Container(
width: size.width,
height: 36,
color: Color(0xFF181818),
),
),
Align(
alignment: Alignment.topCenter,
child: FittedBox(
child: GestureDetector(
onTap: () {
widget.click(SectionType.AUDIO, (_playlist.elements as List<Song>)[0]);
},
child: Container(
height: 45,
margin: const EdgeInsets.only(top: 12),
padding: const EdgeInsets.symmetric(horizontal: 50),
decoration: BoxDecoration(
color: Color(0xFF1EBA54),
borderRadius: BorderRadius.circular(30),
),
child: Center(
child: Text(
_playlist != null && _playlist.element == ElementType.ARTIST
? 'SHUFFLE PLAY'
: 'PLAY',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600))),
),
),
),
),
],
),
),
),
],
)
: Stack(
children: [
Container(
width: size.width,
height: size.height,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF404040), Color(0xFF181818), Color(0xFF181818)],
begin: Alignment(0.5, -1.5),
end: Alignment(0.5, 0.2),
),
),
child: ListView(
padding: const EdgeInsets.only(top: 320, bottom: 20, left: 20, right: 20),
controller: _scrollController,
children: [
if (_playlist != null)
..._playlist.elements.map((song) => SingleSongItem(click: widget.click, song: song))
],
),
),
Container(
height: 300 - (_height * 180),
color: Color(0xFF121212).withOpacity(_barOpacity > 0.6 ? 1 : _barOpacity < 0.1 ? 0 : _barOpacity),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Space(size: 5),
Row(
children: [
GestureDetector(
onTap: () => widget.click(SectionType.BROWSE, null),
child: Icon(Icons.keyboard_arrow_left, color: Colors.white, size: 35),
),
Space(size: 5, horizontal: true),
Icon(Icons.keyboard_arrow_right, color: Colors.grey, size: 35),
Space(size: 8, horizontal: true),
Container(
height: 26,
width: 180,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(13),
),
child: Row(
children: [
Space(size: 5, horizontal: true),
Icon(Icons.search, color: Colors.grey[800], size: 18),
Space(size: 5, horizontal: true),
Text('Search', style: TextStyle(color: Colors.grey[800]))
],
),
),
Expanded(child: SizedBox()),
Container(
height: 30,
width: 30,
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Image.network(Utils.mainUser.image, fit: BoxFit.cover),
),
),
Space(size: 8, horizontal: true),
Text(Utils.mainUser.name, style: TextStyle(color: Colors.white)),
Space(size: 15, horizontal: true),
Icon(Icons.keyboard_arrow_down, color: Colors.grey, size: 35),
Space(size: 10, horizontal: true),
],
),
Expanded(child: SizedBox()),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Stack(
alignment: Alignment.bottomLeft,
children: [
AnimatedContainer(
duration: Duration(milliseconds: 250),
height: _barOpacity > 0.15 ? 55 : 140,
margin: EdgeInsets.only(bottom: _barOpacity > 0.15 ? 0 : 60),
child: _playlist != null
? Row(
children: [
AspectRatio(
aspectRatio: 1,
child: Image.network(
_playlist.image,
fit: BoxFit.cover,
),
),
Space(size: 20, horizontal: true),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_barOpacity < 0.06)
Text(
when(_playlist.element, {
ElementType.ARTIST: 'ENJOY THIS ARTIST',
ElementType.PLAYLIST: 'CUSTOM PLAYLIST FOR YOU'
}),
style: TextStyle(color: Colors.grey[400])),
Text(_playlist.name,
style: TextStyle(
color: Colors.white, fontWeight: FontWeight.w600, fontSize: 30)),
if (_barOpacity < 0.06)
Text(
'Made for Mariano Zorrilla - ${(_playlist.elements as List).length} song',
style: TextStyle(color: Colors.grey[400])),
],
)
],
)
: SizedBox(),
),
AnimatedContainer(
duration: Duration(milliseconds: 200),
curve: Curves.decelerate,
alignment: _barOpacity > 0.1 ? Alignment.bottomRight : Alignment.bottomLeft,
margin: EdgeInsets.only(bottom: _barOpacity > 0.2 ? 10 : 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
GestureDetector(
onTap: () {
widget.click(SectionType.AUDIO, (_playlist.elements as List<Song>)[0]);
},
child: Container(
height: 30,
padding: const EdgeInsets.symmetric(horizontal: 45),
decoration: BoxDecoration(
color: Color(0xFF1EBA54),
borderRadius: BorderRadius.circular(30),
),
child: Center(
child: Text(
_playlist != null && _playlist.element == ElementType.ARTIST
? 'SHUFFLE PLAY'
: 'PLAY',
style: TextStyle(
color: Colors.white, fontWeight: FontWeight.w600, fontSize: 12))),
),
),
Space(size: 10, horizontal: true),
Container(
height: 30,
width: 30,
decoration: BoxDecoration(
color: Colors.black,
border: Border.all(color: Colors.white, width: 1),
borderRadius: BorderRadius.circular(15)),
child: Icon(Icons.favorite_border, color: Colors.white, size: 14),
),
Space(size: 10, horizontal: true),
Container(
height: 30,
width: 30,
decoration: BoxDecoration(
color: Colors.black,
border: Border.all(color: Colors.white, width: 1),
borderRadius: BorderRadius.circular(15)),
child: Icon(Icons.more_vert, color: Colors.white, size: 14),
),
],
),
),
],
),
),
Space(size: 15),
],
),
),
),
],
);
}
}
class PlayerScreen extends StatefulWidget {
const PlayerScreen({Key key, this.song, this.audio}) : super(key: key);
final Song song;
final AudioElement audio;
@override
_PlayerScreenState createState() => _PlayerScreenState();
}
class _PlayerScreenState extends State<PlayerScreen> {
var _isGif = false;
var _current = 0.0;
bool _disposed = false;
bool _switchDesktop = true;
@override
void initState() {
super.initState();
if (mounted) {
Future.microtask(() => setState(() => _isGif = widget.song.imgGif != null));
Future.microtask(() {
widget.audio.addEventListener('timeupdate', (event) {
if (!_disposed) setState(() => _current = widget.audio.currentTime);
}, true);
});
}
}
@override
void dispose() {
_disposed = true;
super.dispose();
}
void handleSwitchDesktop() {
Future.microtask(() {
_switchDesktop = true;
Navigator.of(context).pop();
});
}
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < 650;
if (isMobile) _switchDesktop = false;
if (!_switchDesktop && !isMobile) handleSwitchDesktop();
return Material(
child: Container(
decoration: BoxDecoration(
image: _isGif ? DecorationImage(fit: BoxFit.cover, image: NetworkImage(widget.song.imgGif)) : null,
),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Utils.playerTop.withAlpha(_isGif ? 0xAA : 0xFF),
Utils.playerBottom.withAlpha(_isGif ? 0xAA : 0xFF)
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Column(
children: [
MobileAppBar(
title: widget.song.name,
back: Icons.keyboard_arrow_down,
onTap: () {
Navigator.of(context).pop();
},
),
if (!_isGif)
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 65, vertical: 30),
child: FittedBox(
child: SizedBox(
width: 400,
height: 400,
child: Card(
elevation: 5,
shape: RoundedRectangleBorder(),
child: Image.network(widget.song.image, fit: BoxFit.cover),
),
),
),
),
),
if (_isGif) Expanded(child: SizedBox()),
Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Row(
children: [
Space(size: 20, horizontal: true),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.song.name, style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600)),
Text(widget.song.artist.name, style: TextStyle(color: Colors.grey)),
],
),
Expanded(child: SizedBox()),
Icon(Icons.favorite_border, color: Colors.white),
Space(size: 20, horizontal: true),
],
),
Padding(
padding: const EdgeInsets.only(left: 20, right: 8, top: 15, bottom: 8),
child: SliderTheme(
data: SliderThemeData(
trackHeight: 4,
overlayShape: RoundSliderOverlayShape(overlayRadius: 0),
thumbShape: RoundSliderThumbShape(enabledThumbRadius: 6, disabledThumbRadius: 6),
thumbColor: Colors.white,
activeTrackColor: Colors.green,
),
child: Slider(
value: _current <= widget.audio.duration.floor().toDouble()
? _current
: widget.audio.duration.floor().toDouble(),
activeColor: Colors.white,
inactiveColor: Colors.grey[800],
max: widget.audio.duration.floor().toDouble(),
onChanged: (value) {
setState(() {
_current = value;
widget.audio.currentTime = _current;
});
},
),
),
),
Row(
children: [
Space(size: 20, horizontal: true),
Text(Utils.parseTime(_current), style: TextStyle(color: Colors.white)),
Expanded(child: SizedBox()),
Text(Utils.parseTime(widget.audio.duration), style: TextStyle(color: Colors.white)),
Space(size: 20, horizontal: true),
],
),
Row(
children: [
Space(size: 20, horizontal: true),
Icon(Icons.shuffle, color: Colors.white),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.skip_previous, color: Colors.white, size: 30),
Space(size: 20, horizontal: true),
GestureDetector(
onTap: () => widget.audio.paused ? widget.audio.play() : widget.audio.pause(),
child: Icon(widget.audio.paused ? Icons.play_circle_filled : Icons.pause_circle_filled,
color: Colors.white, size: 60),
),
Space(size: 20, horizontal: true),
Icon(Icons.skip_next, color: Colors.white, size: 30),
],
)),
Icon(Icons.repeat, color: Colors.white),
Space(size: 20, horizontal: true),
],
),
Space(size: 20),
Row(
children: [
Space(size: 20, horizontal: true),
Icon(Icons.devices, color: Colors.white, size: 20),
Expanded(child: SizedBox()),
Icon(Icons.playlist_play, color: Colors.white),
Space(size: 20, horizontal: true),
],
),
Space(size: 20),
],
)
],
),
),
),
);
}
}
class MobileAppBar extends StatelessWidget {
final String title;
final IconData back;
final IconData icon;
final bool showMenu;
final double opacity;
final GenericClick<SectionType> click;
final GestureDragCancelCallback onTap;
const MobileAppBar(
{Key key,
this.title,
this.showMenu = true,
this.opacity = 1.0,
this.click,
this.onTap,
this.back = Icons.keyboard_backspace,
this.icon = Icons.more_vert})
: super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: 40,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Space(size: 10, horizontal: true),
if (click != null)
GestureDetector(
onTap: () => click(SectionType.BROWSE, null),
child: Icon(back, color: Colors.white),
),
if (onTap != null)
GestureDetector(
onTap: onTap,
child: Icon(back, color: Colors.white),
),
Expanded(
child: Align(
alignment: Alignment.center,
child: Opacity(
opacity: opacity,
child: Text(title ?? '', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600))),
),
),
if (showMenu) Icon(icon, color: Colors.white),
Space(size: 10, horizontal: true)
],
),
);
}
}
class PlaylistItem extends StatefulWidget {
const PlaylistItem({Key key, this.click, this.playlist, this.small}) : super(key: key);
final GenericClick<SectionType> click;
final Playlist playlist;
final bool small;
@override
_PlaylistItemState createState() => _PlaylistItemState();
}
class _PlaylistItemState extends State<PlaylistItem> {
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final isMobile = MediaQuery.of(context).size.width < 600;
return GestureDetector(
onTap: () => widget.click(SectionType.ALBUM, widget.playlist),
child: Container(
width: size.width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Space(size: 40),
Padding(
padding: const EdgeInsets.only(left: 17),
child: widget.playlist.element == ElementType.ARTIST
? Row(
children: [
SizedBox(
width: 40,
height: 40,
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Image.network(widget.playlist.image, fit: BoxFit.cover),
),
),
Space(size: 10, horizontal: true),
Text(widget.playlist.name,
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 20))
],
)
: Text(widget.playlist.name,
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 20)),
),
Space(size: 15),
Container(
height: widget.small ? isMobile ? 130 : 180 : isMobile ? 200 : 250,
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 8),
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
final item = (widget.playlist.elements as List<Song>)[index];
if (item is Song) {
return GestureDetector(
onTap: () => widget.click(SectionType.AUDIO, item),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CoverImage(small: widget.small, item: item),
if (widget.small)
Padding(
padding: const EdgeInsets.only(left: 10, top: 10),
child:
Text(item.name, style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600)),
),
if (!widget.small)
Padding(
padding: const EdgeInsets.only(left: 10, top: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item.name, style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600)),
Space(size: 5),
Text('${item.album.name} - ${item.artist.name}',
style: TextStyle(color: Colors.grey, fontSize: 12)),
],
),
),
],
),
);
} else {
return SizedBox();
}
},
itemCount: (widget.playlist.elements as List).length,
),
),
],
),
),
);
}
}
class CoverImage extends StatefulWidget {
const CoverImage({Key key, this.small, this.item}) : super(key: key);
final bool small;
final Song item;
@override
_CoverImageState createState() => _CoverImageState();
}
class _CoverImageState extends State<CoverImage> {
var _hover = false;
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < 650;
return MouseRegion(
onHover: (_) => setState(() => _hover = true),
onEnter: (_) => setState(() => _hover = true),
onExit: (_) => setState(() => _hover = false),
child: Container(
height: widget.small ? isMobile ? 100 : 150 : isMobile ? 150 : 200,
width: widget.small ? isMobile ? 100 : 150 : isMobile ? 150 : 200,
margin: const EdgeInsets.symmetric(horizontal: 10),
child: Stack(
fit: StackFit.expand,
children: [
Image.network(widget.item.image, fit: BoxFit.cover),
if (!isMobile)
Visibility(
visible: _hover,
child: Container(
color: Colors.black.withAlpha(0xAA),
child: Center(
child: Row(
children: [
Icon(Icons.favorite_border, color: Colors.white, size: widget.small ? 18 : 22),
Icon(Icons.play_circle_outline, color: Colors.white, size: widget.small ? 40 : 60),
Icon(Icons.more_horiz, color: Colors.white, size: widget.small ? 18 : 22),
],
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
),
),
),
)
],
),
),
);
}
}
class SingleSongItem extends StatefulWidget {
const SingleSongItem({Key key, this.click, this.song}) : super(key: key);
final GenericClick<SectionType> click;
final Song song;
@override
_SingleSongItemState createState() => _SingleSongItemState();
}
class _SingleSongItemState extends State<SingleSongItem> {
var _hover = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => widget.click(SectionType.AUDIO, widget.song),
child: MouseRegion(
onHover: (_) => setState(() => _hover = true),
onEnter: (_) => setState(() => _hover = true),
onExit: (_) => setState(() => _hover = false),
child: Container(
color: _hover ? Colors.grey[700] : Utils.sectionBg,
padding: const EdgeInsets.symmetric(vertical: 5),
child: Row(
children: [
Space(size: 10, horizontal: true),
Container(
height: 30,
width: 30,
child: Visibility(
visible: _hover,
child: Icon(Icons.play_circle_outline, color: Colors.white, size: 30),
),
),
Space(size: 10, horizontal: true),
Icon(Icons.favorite_border, color: Colors.white, size: 18),
Space(size: 15, horizontal: true),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
child: Text(widget.song.name,
style: TextStyle(color: Colors.white, fontSize: 14),
maxLines: 1,
overflow: TextOverflow.ellipsis),
constraints: BoxConstraints(minWidth: 150, maxWidth: 250),
),
Space(size: 20, horizontal: true),
Expanded(
child: Text(
'${widget.song.artist.name} - ${widget.song.album.name}',
style: TextStyle(fontSize: 12, color: Colors.grey),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
],
),
),
Space(size: 10, horizontal: true),
Visibility(visible: _hover, child: Icon(Icons.more_horiz, color: Colors.grey)),
Space(size: 10, horizontal: true),
],
),
),
),
);
}
}
class SongItem extends StatelessWidget {
const SongItem({Key key, this.click, this.song}) : super(key: key);
final GenericClick<SectionType> click;
final Song song;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => click(SectionType.AUDIO, song),
child: Container(
color: Utils.sectionBg,
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Space(size: 10, horizontal: true),
Container(height: 50, width: 50, child: Image.network(song.image, fit: BoxFit.cover)),
Space(size: 10, horizontal: true),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(song.name, style: TextStyle(color: Colors.white, fontSize: 14)),
Space(size: 4),
Text(
'${song.artist.name} - ${song.album.name}',
style: TextStyle(fontSize: 12, color: Colors.grey),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
],
),
),
Space(size: 10, horizontal: true),
Icon(Icons.more_vert, color: Colors.grey),
Space(size: 10, horizontal: true),
],
),
),
);
}
}
class AddPlayList extends StatefulWidget {
@override
_AddPlayListState createState() => _AddPlayListState();
}
class _AddPlayListState extends State<AddPlayList> {
bool _hover = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onHover: (_) => setState(() => _hover = true),
onExit: (_) => setState(() => _hover = false),
child: Container(
height: 60,
padding: const EdgeInsets.only(left: 20),
child: Row(
children: [
Icon(Icons.add_circle_outline, color: _hover ? Colors.white : Colors.grey),
Space(size: 10, horizontal: true),
Text('New Playlist', style: TextStyle(color: _hover ? Colors.white : Colors.grey)),
],
),
),
);
}
}
class Space extends StatelessWidget {
const Space({Key key, this.size, this.horizontal = false, this.color = Colors.transparent}) : super(key: key);
final double size;
final Color color;
final bool horizontal;
@override
Widget build(BuildContext context) {
return Container(width: horizontal ? size : 0, height: !horizontal ? size : 0, color: color);
}
}
class Line extends StatelessWidget {
const Line({Key key, this.horizontal = false, this.color}) : super(key: key);
final bool horizontal;
final Color color;
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return Container(
height: horizontal ? 1 : size.height, width: horizontal ? size.width : 0.5, color: color ?? Colors.grey[800]);
}
}
enum ElementType { PLAYLIST, ARTIST, ALBUM, SONG }
enum TextType { HEADER, NORMAL, TITLE, BOLD, SPACE }
enum SectionType {
HOME,
BROWSE,
BROWSE_PODCAST,
PODCAST,
GRID,
GRID_CIRCLE,
GRID_SECTION,
LIST,
LIST_IMAGE,
ARTIST,
ALBUM,
SONG,
AUDIO,
FULLSCREEN
}
class Utils {
static const Color leftPanelBg = Color(0xFF121212);
static const Color rightPanelBg = Color(0xFF121212);
static const Color sectionBg = Color(0xFF181818);
static const Color playerSectionBg = Color(0xFF282828);
static const Color selectedItem = Color(0xFF20D760);
static const Color playerTop = Color(0xFF964847);
static const Color playerBottom = Color(0xFF191317);
static List<User> users = [
User(
'Martin Aguinis',
'https://pbs.twimg.com/profile_images/1139005628846878721/lSg5Loq4_400x400.jpg',
'Summer Nights',
'LiQWYD',
'2h',
),
User(
'Tim Sneath',
'https://pbs.twimg.com/profile_images/653618067084218368/XlQA-oRl.jpg',
'Atch',
'Your Daily Mix 1',
'10h',
),
User(
'Simon',
'https://pbs.twimg.com/profile_images/1017532253394624513/LgFqlJ4U_400x400.jpg',
'Dawn',
'MusicbyAden',
'21h',
),
];
static User mainUser = User('Mariano Zorrilla',
'https://pbs.twimg.com/profile_images/1222274976415281153/TVSI4DIx_400x400.jpg', null, null, null);
static List<Item> leftSectionItems = [
Item('Home', true, TextType.HEADER, SectionType.HOME, GlobalKey<LeftListItemState>()),
Item('Browse', false, TextType.HEADER, SectionType.BROWSE, GlobalKey<LeftListItemState>()),
Item('Radio', false, TextType.HEADER, SectionType.GRID, GlobalKey<LeftListItemState>()),
Item('YOUR LIBRARY', false, TextType.TITLE, null, GlobalKey<LeftListItemState>()),
Item('Made for you', false, TextType.BOLD, SectionType.GRID_SECTION, GlobalKey<LeftListItemState>()),
Item('Recently Played', false, TextType.BOLD, SectionType.GRID, GlobalKey<LeftListItemState>()),
Item('Liked Song', false, TextType.BOLD, SectionType.LIST, GlobalKey<LeftListItemState>()),
Item('Albums', false, TextType.BOLD, SectionType.GRID, GlobalKey<LeftListItemState>()),
Item('Artists', false, TextType.BOLD, SectionType.GRID_CIRCLE, GlobalKey<LeftListItemState>()),
Item('Podcasts', false, TextType.BOLD, SectionType.PODCAST, GlobalKey<LeftListItemState>()),
Item(null, false, TextType.SPACE, null, null),
Item('PLAYLISTS', false, TextType.TITLE, SectionType.LIST, GlobalKey<LeftListItemState>()),
Item('Discover Weekly', false, TextType.NORMAL, SectionType.LIST, GlobalKey<LeftListItemState>()),
Item('Starred', false, TextType.NORMAL, SectionType.LIST, GlobalKey<LeftListItemState>()),
];
static String parseTime(num time) {
var seconds = time % 60;
var foo = time - seconds;
var minutes = foo / 60;
var formatSeconds = seconds.toInt().toString();
if (seconds < 10) {
formatSeconds = '0' + formatSeconds;
}
return '$minutes:$formatSeconds';
}
static void itemSelection<T extends List>(T list, dynamic selected) {
list.asMap().forEach((key, value) {
if (list[key].key != null && list[key].key.currentState != null) {
list[key].key.currentState.selected(selected is bool ? selected : key == selected);
}
});
}
static List<Playlist> homeList = [
Playlist('Recently Played', 'https://assets.codepen.io/2399829/daily.png', ElementType.PLAYLIST, [
atchVoyage,
memoriesMusicByAden,
coffee,
virtualJoySilentCrafter,
summerNights,
boraBora,
youngLove,
intense,
overSoon,
sakura,
soul,
followMe,
callMe,
]),
Playlist('LiQWYD', 'https://assets.codepen.io/2399829/liqwyd.jpg', ElementType.ARTIST, [
overSoon,
summerNights,
callMe,
coffee,
soul,
youngLove,
fadeOut,
eyesClosed,
flow,
feelIt,
breathe,
leap,
]),
Playlist('Dance & Electronic', 'https://assets.codepen.io/2399829/edm.jpg', ElementType.PLAYLIST, [
soul,
laser,
youngLove,
boraBora,
memoriesMusicByAden,
takeAwayThePain,
intense,
eyesClosed,
callMe,
]),
Playlist('Vendredi', 'https://assets.codepen.io/2399829/vendredi.jpg', ElementType.ARTIST, [
takeAwayThePain,
followMe,
tropicalLove,
ai,
cocktail,
rooftop,
humAndThrill,
silhouette,
trueStory,
mistyFantasy,
firework,
divingIn,
]),
];
static Artist get atch => Artist('Atch', 'https://i1.sndcdn.com/avatars-000724049929-uwg00o-t500x500.jpg', []);
static Album get atchAlbum =>
Album('Voyage', 'https://i1.sndcdn.com/artworks-04xcPMCHF5woz843-CwdjVQ-t500x500.jpg', atch, []);
static Song get atchVoyage => Song(
'Voyage',
'https://assets.codepen.io/2399829/arch_voyage.jpg',
null,
'https://assets.codepen.io/2399829/voyage_atch.mp3',
atch,
atchAlbum,
);
static Song get memoriesMusicByAden => Song(
'Memories',
'https://i1.sndcdn.com/artworks-OSGuqLsjDoSnpGZN-ihwdsA-t500x500.jpg',
null,
'https://assets.codepen.io/2399829/memories_musicbyaden.mp3',
musicByAden,
memoriesAlbum,
);
static Album get memoriesAlbum => Album('MusicByAden', getThumbnail('4fX1VmXg-gU'), musicByAden, []);
static Artist get musicByAden => Artist('MusicByAden', getThumbnail('4fX1VmXg-gU'), []);
static Song get virtualJoySilentCrafter => Song(
'Virtual Joy',
'https://assets.codepen.io/2399829/virtualjoy.jpg',
'https://assets.codepen.io/2399829/virtualjoy.gif',
'https://assets.codepen.io/2399829/virtual_joy_silentcrafter.mp3',
silentCrafter,
silentCrafterHappy,
);
static Song get laser => Song(
'Laser',
'https://assets.codepen.io/2399829/laser_silentcrafter.jpg',
null,
'https://assets.codepen.io/2399829/laser_silentcrafter.mp3',
silentCrafter,
silentCrafterHappy,
);
static Album get silentCrafterHappy => Album('Happy', getThumbnail('eNgusegQBGM'), silentCrafter, []);
static Artist get silentCrafter => Artist('SilentCrafter ', getThumbnail('eNgusegQBGM'), []);
static Song get boraBora => Song(
'Bora Bora',
'https://assets.codepen.io/2399829/bora.jpg',
null,
'https://assets.codepen.io/2399829/bora_bora_mbb.mp3',
mbb,
calmAlbum,
);
static Album get calmAlbum => Album('Calm', getThumbnail('Qvw-s27xp_g'), mbb, []);
static Artist get mbb => Artist('MBB ', getThumbnail('Qvw-s27xp_g'), []);
static Song get intense => Song(
'Intense',
'https://assets.codepen.io/2399829/intense.jpg',
null,
'https://assets.codepen.io/2399829/intense_peyruis.mp3',
peyruis,
bright,
);
static Album get bright => Album('Bright', getThumbnail('AOjdw8XX-vE'), peyruis, []);
static Artist get peyruis => Artist('Peyruis ', getThumbnail('AOjdw8XX-vE'), []);
static Song get overSoon => Song(
'Over Soon',
getThumbnail('FfQ5aULtfTY'),
null,
'https://assets.codepen.io/2399829/over_soon_liqwyd.mp3',
liQWYD,
chill,
);
static Song get summerNights => Song(
'Summer Nights',
'https://assets.codepen.io/2399829/summer_nights.jpg',
'https://assets.codepen.io/2399829/summer_nights.gif',
'https://assets.codepen.io/2399829/summer_nights_liqwyd.mp3',
liQWYD,
chill,
);
static Song get callMe => Song(
'Call Me',
'https://assets.codepen.io/2399829/callme.jpg',
null,
'https://assets.codepen.io/2399829/call_me_liqwyd.mp3',
liQWYD,
chill,
);
static Song get coffee => Song(
'Coffee',
'https://assets.codepen.io/2399829/coffee_liqwyd.jpg',
null,
'https://assets.codepen.io/2399829/coffee_liqwyd.mp3',
liQWYD,
chill,
);
static Song get soul => Song(
'Soul',
getThumbnail('n6soxrKiCos'),
null,
'https://assets.codepen.io/2399829/soul_liqwyd.mp3',
liQWYD,
chill,
);
static Song get youngLove => Song(
'Young Love',
'https://assets.codepen.io/2399829/young_love_liqwyd.jpg',
null,
'https://assets.codepen.io/2399829/young_love_liqwyd.mp3',
liQWYD,
chill,
);
static Song get fadeOut => Song(
'Fade Out',
getThumbnail('64AGaW9GuWM'),
null,
'https://assets.codepen.io/2399829/fade_out_liqwyd.mp3',
liQWYD,
chill,
);
static Song get eyesClosed => Song(
'Eyes Closed',
getThumbnail('O4jNueFacQM'),
null,
'https://assets.codepen.io/2399829/eyes_closed_liqwyd.mp3',
liQWYD,
chill,
);
static Song get flow => Song(
'Flow',
getThumbnail('A52eKafrihU'),
null,
'https://assets.codepen.io/2399829/flow_liqwyd.mp3',
liQWYD,
chill,
);
static Song get feelIt => Song(
'Feel It',
getThumbnail('mTczsFoP2Cc'),
null,
'https://assets.codepen.io/2399829/feel_it_liqwyd.mp3',
liQWYD,
chill,
);
static Song get breathe => Song(
'Breathe',
getThumbnail('UzOXNwtkcYs'),
null,
'https://assets.codepen.io/2399829/breathe_liqwyd.mp3',
liQWYD,
chill,
);
static Song get leap => Song(
'Leap',
getThumbnail('9DhAPuKtqV8'),
null,
'https://assets.codepen.io/2399829/leap_liqwyd.mp3',
liQWYD,
chill,
);
static Album get chill => Album('Chill', 'https://assets.codepen.io/2399829/album_liqwyd.jpg', liQWYD, []);
static Artist get liQWYD => Artist('LiQWYD', 'https://assets.codepen.io/2399829/liqwyd.jpg', []);
static Song get sakura => Song(
'Sakura 2020',
'https://assets.codepen.io/2399829/sakura.png',
'https://assets.codepen.io/2399829/sakura.gif',
'https://assets.codepen.io/2399829/sakura_2020_roa.mp3',
roa,
roaAlbum,
);
static Album get roaAlbum => Album('Roa', 'https://assets.codepen.io/2399829/roa_music.jpg', roa, []);
static Artist get roa => Artist('Roa', 'https://assets.codepen.io/2399829/roa_music.jpg', []);
static Song get followMe => Song(
'Follow Me',
'https://assets.codepen.io/2399829/follow_me_vendredi.jpg',
null,
'https://assets.codepen.io/2399829/follow_me_vendredi.mp3',
vendredi,
vendrediAlbum,
);
static Song get tropicalLove => Song(
'Tropical Love',
getThumbnail('8C-9VIKe-VQ'),
'https://assets.codepen.io/2399829/tropical_love.gif',
'https://assets.codepen.io/2399829/tropical_love_vendredi.mp3',
vendredi,
vendrediAlbum,
);
static Song get ai => Song(
'AI',
getThumbnail('-TRUcjFes4M'),
null,
'https://assets.codepen.io/2399829/ai_vendredi.mp3',
vendredi,
vendrediAlbum,
);
static Song get cocktail => Song(
'Cocktail',
getThumbnail('0GVbYKhVaxI'),
null,
'https://assets.codepen.io/2399829/cocktail_vendredi.mp3',
vendredi,
vendrediAlbum,
);
static Song get rooftop => Song(
'Rooftop Marrakech',
getThumbnail('A4GNhPf-YYE'),
null,
'https://assets.codepen.io/2399829/rooftop_marrakech_vendredi.mp3',
vendredi,
vendrediAlbum,
);
static Song get humAndThrill => Song(
'Hum & Thrill',
getThumbnail('7tZyeBLrAJ4'),
null,
'https://assets.codepen.io/2399829/hum_and_thrill_vendredi.mp3',
vendredi,
vendrediAlbum,
);
static Song get silhouette => Song(
'Silhouette',
getThumbnail('WhqnxycuUXQ'),
null,
'https://assets.codepen.io/2399829/silhouette_vendredi.mp3',
vendredi,
vendrediAlbum,
);
static Song get trueStory => Song(
'True Story',
getThumbnail('sgKMJQWKkk4'),
null,
'https://assets.codepen.io/2399829/true_story_vendredi.mp3',
vendredi,
vendrediAlbum,
);
static Song get mistyFantasy => Song(
'Misty Fantasy',
getThumbnail('fazCGsYP938'),
null,
'https://assets.codepen.io/2399829/misty_fantasy_vendredi.mp3',
vendredi,
vendrediAlbum,
);
static Song get firework => Song(
'Firework',
getThumbnail('r5W-txT2GWQ'),
null,
'https://assets.codepen.io/2399829/firework_vendredi.mp3',
vendredi,
vendrediAlbum,
);
static Song get divingIn => Song(
'Diving In',
getThumbnail('6hHkS95U65E'),
null,
'https://assets.codepen.io/2399829/diving_in_vendredi.mp3',
vendredi,
vendrediAlbum,
);
static Song get takeAwayThePain => Song(
'Take The Pain Away',
getThumbnail('1p0KYaAGMC4'),
null,
'https://assets.codepen.io/2399829/take_the_pain_away_vendredi.mp3',
vendredi,
vendrediAlbum,
);
static Album get vendrediAlbum => Album('Vendredi', 'https://assets.codepen.io/2399829/vendredi.jpg', vendredi, []);
static Artist get vendredi => Artist('Vendredi', 'https://assets.codepen.io/2399829/vendredi.jpg', []);
static Song currentSong;
static String getThumbnail(String url) => 'http://i3.ytimg.com/vi/$url/maxresdefault.jpg';
static Tuple<Widget, AudioElement> audio(String url, id, bool autoPlay) {
final audio = AudioElement();
audio.autoplay = autoPlay;
audio.src = url;
// ignore:undefined_prefixed_name
ui.platformViewRegistry.registerViewFactory('audio_$url$id', (int viewId) => audio);
return Tuple(HtmlElementView(key: UniqueKey(), viewType: 'audio_$url$id'), audio);
}
}
class Item {
String text;
bool selected;
TextType type;
SectionType section;
GlobalKey<LeftListItemState> key;
Item(this.text, this.selected, this.type, this.section, this.key);
}
class Playlist {
String name;
String image;
ElementType element;
dynamic elements;
Playlist(this.name, this.image, this.element, this.elements);
}
class Artist extends Element {
String name;
String image;
List<Album> albums;
Artist(this.name, this.image, this.albums);
}
class Album extends Element {
String name;
String image;
Artist artist;
List<Song> songs;
Album(this.name, this.image, this.artist, this.songs);
}
class Song extends Element {
String name;
String image;
String imgGif;
String url;
Artist artist;
Album album;
Song(this.name, this.image, this.imgGif, this.url, this.artist, this.album);
}
class User {
String name;
String image;
String song;
String playlist;
String time;
User(this.name, this.image, this.song, this.playlist, this.time);
}
class Src {
String src;
Src(this.src);
}
class Element {}
typedef ItemClick = Function(int index);
typedef GenericClick<T> = Function(T section, dynamic item);
class Tuple<F, S> {
F first;
S second;
Tuple(this.first, this.second);
}
Type when<Input, Type>(Input selectedOption, Map<Input, Type> branches, [Type defaultValue]) {
if (!branches.containsKey(selectedOption)) {
return defaultValue;
}
return branches[selectedOption];
}
View Compiled
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.