mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 16:05:18 +00:00
Merge branch 'master' into build
This commit is contained in:
commit
d7d2b31e8e
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@ -1,5 +1,4 @@
|
|||||||
open_collective: spotube
|
open_collective: spotube
|
||||||
ko_fi: krtirtho
|
|
||||||
patreon: krtirtho
|
patreon: krtirtho
|
||||||
custom:
|
custom:
|
||||||
- "https://www.buymeacoffee.com/krtirtho"
|
- "https://www.buymeacoffee.com/krtirtho"
|
||||||
|
2
.github/workflows/flutter-build.yml
vendored
2
.github/workflows/flutter-build.yml
vendored
@ -35,6 +35,8 @@ jobs:
|
|||||||
build/Spotube-linux-x86_64.tar.xz
|
build/Spotube-linux-x86_64.tar.xz
|
||||||
build/Spotube-*-x86_64.AppImage
|
build/Spotube-*-x86_64.AppImage
|
||||||
# Building Android Application
|
# Building Android Application
|
||||||
|
- run: echo ${{ secrets.KEYSTORE }} | base64 --decode > upload-keystore.jks
|
||||||
|
- run: echo ${{ secrets.KEY_PROPERTIES }} > android/key.properties
|
||||||
- run: flutter build apk
|
- run: flutter build apk
|
||||||
- run: make apk
|
- run: make apk
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v2
|
||||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -45,7 +45,6 @@ app.*.map.json
|
|||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
|
||||||
|
|
||||||
*.pkg.tar.zst
|
*.pkg.tar.zst
|
||||||
/aur-struct/*.tar
|
/aur-struct/*.tar
|
||||||
/aur-struct/src
|
/aur-struct/src
|
||||||
@ -73,4 +72,6 @@ help.txt
|
|||||||
secrets.json
|
secrets.json
|
||||||
|
|
||||||
dist
|
dist
|
||||||
appimage-build
|
appimage-build
|
||||||
|
|
||||||
|
android/key.properties
|
||||||
|
@ -25,6 +25,12 @@ apply plugin: 'com.android.application'
|
|||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||||
|
|
||||||
|
def keystoreProperties = new Properties()
|
||||||
|
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 31
|
compileSdkVersion 31
|
||||||
|
|
||||||
@ -51,11 +57,17 @@ android {
|
|||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
release {
|
||||||
|
keyAlias keystoreProperties['keyAlias']
|
||||||
|
keyPassword keystoreProperties['keyPassword']
|
||||||
|
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
|
||||||
|
storePassword keystoreProperties['storePassword']
|
||||||
|
}
|
||||||
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// TODO: Add your own signing config for the release build.
|
signingConfig signingConfigs.release
|
||||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
|
||||||
signingConfig signingConfigs.debug
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/Shared/HeartButton.dart';
|
import 'package:spotube/components/Shared/HeartButton.dart';
|
||||||
@ -59,6 +60,7 @@ class AlbumView extends HookConsumerWidget {
|
|||||||
titleImage: albumArt,
|
titleImage: albumArt,
|
||||||
tracksSnapshot: tracksSnapshot,
|
tracksSnapshot: tracksSnapshot,
|
||||||
album: album,
|
album: album,
|
||||||
|
routePath: "/album/${album.id}",
|
||||||
onPlay: ([track]) {
|
onPlay: ([track]) {
|
||||||
if (tracksSnapshot.asData?.value != null) {
|
if (tracksSnapshot.asData?.value != null) {
|
||||||
playPlaylist(
|
playPlaylist(
|
||||||
|
@ -23,6 +23,7 @@ import 'package:spotube/hooks/usePaginatedFutureProvider.dart';
|
|||||||
import 'package:spotube/hooks/useUpdateChecker.dart';
|
import 'package:spotube/hooks/useUpdateChecker.dart';
|
||||||
import 'package:spotube/models/Logger.dart';
|
import 'package:spotube/models/Logger.dart';
|
||||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||||
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
List<String> spotifyScopes = [
|
List<String> spotifyScopes = [
|
||||||
"playlist-modify-public",
|
"playlist-modify-public",
|
||||||
@ -73,7 +74,7 @@ class Home extends HookConsumerWidget {
|
|||||||
child: MoveWindow(),
|
child: MoveWindow(),
|
||||||
),
|
),
|
||||||
Expanded(child: MoveWindow()),
|
Expanded(child: MoveWindow()),
|
||||||
if (!Platform.isMacOS && !Platform.isAndroid && !Platform.isIOS)
|
if (!Platform.isMacOS && !kIsMobile)
|
||||||
const TitleBarActionButtons(),
|
const TitleBarActionButtons(),
|
||||||
],
|
],
|
||||||
))
|
))
|
||||||
@ -98,7 +99,7 @@ class Home extends HookConsumerWidget {
|
|||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
Platform.isAndroid || Platform.isIOS
|
kIsMobile
|
||||||
? titleBarContents
|
? titleBarContents
|
||||||
: WindowTitleBarBox(child: titleBarContents),
|
: WindowTitleBarBox(child: titleBarContents),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
@ -44,7 +44,7 @@ class ShimmerArtistProfile extends HookWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
const Flexible(child: ShimmerTrackTile()),
|
const Flexible(child: ShimmerTrackTile(noSliver: true)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,12 @@ import 'package:skeleton_text/skeleton_text.dart';
|
|||||||
import 'package:spotube/extensions/ShimmerColorTheme.dart';
|
import 'package:spotube/extensions/ShimmerColorTheme.dart';
|
||||||
|
|
||||||
class ShimmerTrackTile extends StatelessWidget {
|
class ShimmerTrackTile extends StatelessWidget {
|
||||||
const ShimmerTrackTile({Key? key}) : super(key: key);
|
final bool noSliver;
|
||||||
|
|
||||||
|
const ShimmerTrackTile({
|
||||||
|
Key? key,
|
||||||
|
this.noSliver = false,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -13,25 +18,35 @@ class ShimmerTrackTile extends StatelessWidget {
|
|||||||
.extension<ShimmerColorTheme>()!
|
.extension<ShimmerColorTheme>()!
|
||||||
.shimmerBackgroundColor!;
|
.shimmerBackgroundColor!;
|
||||||
|
|
||||||
return Padding(
|
final single = Container(
|
||||||
padding: const EdgeInsets.only(top: 30),
|
margin: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: ListView.builder(
|
child: Row(
|
||||||
scrollDirection: Axis.vertical,
|
children: [
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
SkeletonAnimation(
|
||||||
itemCount: 5,
|
shimmerColor: shimmerColor,
|
||||||
shrinkWrap: true,
|
borderRadius: BorderRadius.circular(20),
|
||||||
itemBuilder: (BuildContext context, int index) {
|
shimmerDuration: 1000,
|
||||||
return Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 20),
|
width: 50,
|
||||||
child: Row(
|
height: 50,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: shimmerBackgroundColor,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.only(top: 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SkeletonAnimation(
|
SkeletonAnimation(
|
||||||
shimmerColor: shimmerColor,
|
shimmerColor: shimmerColor,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
shimmerDuration: 1000,
|
shimmerDuration: 1000,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 50,
|
height: 15,
|
||||||
height: 50,
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: shimmerBackgroundColor,
|
color: shimmerBackgroundColor,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
@ -39,46 +54,41 @@ class ShimmerTrackTile extends StatelessWidget {
|
|||||||
margin: const EdgeInsets.only(top: 10),
|
margin: const EdgeInsets.only(top: 10),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
SkeletonAnimation(
|
||||||
Expanded(
|
shimmerColor: shimmerColor,
|
||||||
child: Column(
|
borderRadius: BorderRadius.circular(20),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
shimmerDuration: 1000,
|
||||||
children: [
|
child: Container(
|
||||||
SkeletonAnimation(
|
constraints: BoxConstraints(
|
||||||
shimmerColor: shimmerColor,
|
maxWidth: MediaQuery.of(context).size.width * .8),
|
||||||
borderRadius: BorderRadius.circular(20),
|
height: 10,
|
||||||
shimmerDuration: 1000,
|
decoration: BoxDecoration(
|
||||||
child: Container(
|
color: shimmerBackgroundColor,
|
||||||
height: 15,
|
borderRadius: BorderRadius.circular(10),
|
||||||
decoration: BoxDecoration(
|
),
|
||||||
color: shimmerBackgroundColor,
|
margin: const EdgeInsets.only(top: 10),
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
margin: const EdgeInsets.only(top: 10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SkeletonAnimation(
|
|
||||||
shimmerColor: shimmerColor,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
shimmerDuration: 1000,
|
|
||||||
child: Container(
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxWidth: MediaQuery.of(context).size.width * .8),
|
|
||||||
height: 10,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: shimmerBackgroundColor,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
margin: const EdgeInsets.only(top: 10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (noSliver) {
|
||||||
|
return ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: 5,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemBuilder: (context, _) => single,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(BuildContext context, int index) => single,
|
||||||
|
childCount: 5,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@ -12,6 +11,7 @@ import 'package:spotube/components/Shared/SpotubeMarqueeText.dart';
|
|||||||
import 'package:spotube/helpers/artists-to-clickable-artists.dart';
|
import 'package:spotube/helpers/artists-to-clickable-artists.dart';
|
||||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||||
|
import 'package:spotube/hooks/useCustomStatusBarColor.dart';
|
||||||
import 'package:spotube/hooks/usePaletteColor.dart';
|
import 'package:spotube/hooks/usePaletteColor.dart';
|
||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
|
|
||||||
@ -46,29 +46,10 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
|
|
||||||
final PaletteColor paletteColor = usePaletteColor(context, albumArt, ref);
|
final PaletteColor paletteColor = usePaletteColor(context, albumArt, ref);
|
||||||
|
|
||||||
final backgroundColor = Theme.of(context).backgroundColor;
|
useCustomStatusBarColor(
|
||||||
|
paletteColor.color,
|
||||||
useEffect(() {
|
GoRouter.of(context).location == "/player",
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
);
|
||||||
SystemUiOverlayStyle(
|
|
||||||
statusBarColor: paletteColor.color, // status bar color
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}, [paletteColor.color]);
|
|
||||||
|
|
||||||
useEffect(() {
|
|
||||||
return () {
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
|
||||||
SystemUiOverlayStyle(
|
|
||||||
statusBarColor: backgroundColor, // status bar color
|
|
||||||
statusBarIconBrightness: backgroundColor.computeLuminance() > 0.179
|
|
||||||
? Brightness.dark
|
|
||||||
: Brightness.light,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/components/Shared/HeartButton.dart';
|
import 'package:spotube/components/Shared/HeartButton.dart';
|
||||||
import 'package:spotube/components/Shared/TrackCollectionView.dart';
|
import 'package:spotube/components/Shared/TrackCollectionView.dart';
|
||||||
@ -80,6 +81,7 @@ class PlaylistView extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
showShare: playlist.id != "user-liked-tracks",
|
showShare: playlist.id != "user-liked-tracks",
|
||||||
|
routePath: "/playlist/${playlist.id}",
|
||||||
onShare: () {
|
onShare: () {
|
||||||
final data = "https://open.spotify.com/playlist/${playlist.id}";
|
final data = "https://open.spotify.com/playlist/${playlist.id}";
|
||||||
Clipboard.setData(
|
Clipboard.setData(
|
||||||
|
@ -13,6 +13,7 @@ import 'package:spotube/models/SpotifyMarkets.dart';
|
|||||||
import 'package:spotube/models/SpotubeTrack.dart';
|
import 'package:spotube/models/SpotubeTrack.dart';
|
||||||
import 'package:spotube/provider/Auth.dart';
|
import 'package:spotube/provider/Auth.dart';
|
||||||
import 'package:spotube/provider/UserPreferences.dart';
|
import 'package:spotube/provider/UserPreferences.dart';
|
||||||
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class Settings extends HookConsumerWidget {
|
class Settings extends HookConsumerWidget {
|
||||||
@ -56,7 +57,7 @@ class Settings extends HookConsumerWidget {
|
|||||||
constraints: const BoxConstraints(maxWidth: 1366),
|
constraints: const BoxConstraints(maxWidth: 1366),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
if (!Platform.isAndroid && !Platform.isIOS) ...[
|
if (!kIsMobile) ...[
|
||||||
SettingsHotKeyTile(
|
SettingsHotKeyTile(
|
||||||
title: "Next track global shortcut",
|
title: "Next track global shortcut",
|
||||||
currentHotKey: preferences.nextTrackHotKey,
|
currentHotKey: preferences.nextTrackHotKey,
|
||||||
|
@ -9,6 +9,7 @@ import 'package:spotube/helpers/getLyrics.dart';
|
|||||||
import 'package:spotube/models/SpotubeTrack.dart';
|
import 'package:spotube/models/SpotubeTrack.dart';
|
||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
import 'package:spotube/provider/UserPreferences.dart';
|
import 'package:spotube/provider/UserPreferences.dart';
|
||||||
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||||
import 'package:path_provider/path_provider.dart' as path_provider;
|
import 'package:path_provider/path_provider.dart' as path_provider;
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
@ -30,7 +31,7 @@ class DownloadTrackButton extends HookConsumerWidget {
|
|||||||
|
|
||||||
final _downloadTrack = useCallback(() async {
|
final _downloadTrack = useCallback(() async {
|
||||||
if (track == null) return;
|
if (track == null) return;
|
||||||
if ((Platform.isAndroid || Platform.isIOS) &&
|
if ((kIsMobile) &&
|
||||||
!await Permission.storage.isGranted &&
|
!await Permission.storage.isGranted &&
|
||||||
!await Permission.storage.isPermanentlyDenied) {
|
!await Permission.storage.isPermanentlyDenied) {
|
||||||
final status = await Permission.storage.request();
|
final status = await Permission.storage.request();
|
||||||
|
@ -2,6 +2,7 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
class TitleBarActionButtons extends StatelessWidget {
|
class TitleBarActionButtons extends StatelessWidget {
|
||||||
final Color? color;
|
final Color? color;
|
||||||
@ -67,19 +68,22 @@ class PageWindowTitleBar extends StatelessWidget
|
|||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
@override
|
@override
|
||||||
Size get preferredSize => Size.fromHeight(
|
Size get preferredSize => Size.fromHeight(
|
||||||
!Platform.isIOS && !Platform.isAndroid ? appWindow.titleBarHeight : 35,
|
(kIsDesktop ? appWindow.titleBarHeight : 35),
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (Platform.isIOS || Platform.isAndroid) {
|
if (kIsMobile) {
|
||||||
return PreferredSize(
|
return PreferredSize(
|
||||||
preferredSize: const Size.fromHeight(300),
|
preferredSize: const Size.fromHeight(300),
|
||||||
child: Row(
|
child: Container(
|
||||||
children: [
|
color: backgroundColor,
|
||||||
if (leading != null) leading!,
|
child: Row(
|
||||||
Expanded(child: Center(child: center)),
|
children: [
|
||||||
],
|
if (leading != null) leading!,
|
||||||
|
Expanded(child: Center(child: center)),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -94,7 +98,7 @@ class PageWindowTitleBar extends StatelessWidget
|
|||||||
),
|
),
|
||||||
if (leading != null) leading!,
|
if (leading != null) leading!,
|
||||||
Expanded(child: MoveWindow(child: Center(child: center))),
|
Expanded(child: MoveWindow(child: Center(child: center))),
|
||||||
if (!Platform.isMacOS && !Platform.isIOS && !Platform.isAndroid)
|
if (!Platform.isMacOS && !kIsMobile)
|
||||||
TitleBarActionButtons(color: foregroundColor)
|
TitleBarActionButtons(color: foregroundColor)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/components/LoaderShimmers/ShimmerTrackTile.dart';
|
import 'package:spotube/components/LoaderShimmers/ShimmerTrackTile.dart';
|
||||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||||
import 'package:spotube/components/Shared/TracksTableView.dart';
|
import 'package:spotube/components/Shared/TracksTableView.dart';
|
||||||
import 'package:spotube/helpers/simple-track-to-track.dart';
|
import 'package:spotube/helpers/simple-track-to-track.dart';
|
||||||
|
import 'package:spotube/hooks/useCustomStatusBarColor.dart';
|
||||||
import 'package:spotube/hooks/usePaletteColor.dart';
|
import 'package:spotube/hooks/usePaletteColor.dart';
|
||||||
import 'package:spotube/models/Logger.dart';
|
import 'package:spotube/models/Logger.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
class TrackCollectionView extends HookConsumerWidget {
|
class TrackCollectionView extends HookConsumerWidget {
|
||||||
final logger = getLogger(TrackCollectionView);
|
final logger = getLogger(TrackCollectionView);
|
||||||
@ -25,6 +28,8 @@ class TrackCollectionView extends HookConsumerWidget {
|
|||||||
|
|
||||||
final bool showShare;
|
final bool showShare;
|
||||||
final bool isOwned;
|
final bool isOwned;
|
||||||
|
|
||||||
|
final String routePath;
|
||||||
TrackCollectionView({
|
TrackCollectionView({
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.id,
|
required this.id,
|
||||||
@ -33,6 +38,7 @@ class TrackCollectionView extends HookConsumerWidget {
|
|||||||
required this.isPlaying,
|
required this.isPlaying,
|
||||||
required this.onPlay,
|
required this.onPlay,
|
||||||
required this.onShare,
|
required this.onShare,
|
||||||
|
required this.routePath,
|
||||||
this.heartBtn,
|
this.heartBtn,
|
||||||
this.album,
|
this.album,
|
||||||
this.description,
|
this.description,
|
||||||
@ -83,6 +89,11 @@ class TrackCollectionView extends HookConsumerWidget {
|
|||||||
|
|
||||||
final collapsed = useState(false);
|
final collapsed = useState(false);
|
||||||
|
|
||||||
|
useCustomStatusBarColor(
|
||||||
|
color?.color ?? Theme.of(context).backgroundColor,
|
||||||
|
GoRouter.of(context).location == routePath,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
listener() {
|
listener() {
|
||||||
if (controller.position.pixels >= 400 && !collapsed.value) {
|
if (controller.position.pixels >= 400 && !collapsed.value) {
|
||||||
@ -99,142 +110,135 @@ class TrackCollectionView extends HookConsumerWidget {
|
|||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: PageWindowTitleBar(
|
appBar: (kIsDesktop)
|
||||||
backgroundColor:
|
? PageWindowTitleBar(
|
||||||
tracksSnapshot.asData?.value != null ? color?.color : null,
|
backgroundColor: color?.color,
|
||||||
foregroundColor: tracksSnapshot.asData?.value != null
|
foregroundColor: color?.titleTextColor,
|
||||||
? color?.titleTextColor
|
leading: Row(
|
||||||
|
children: [BackButton(color: color?.titleTextColor)],
|
||||||
|
),
|
||||||
|
)
|
||||||
: null,
|
: null,
|
||||||
leading: Row(
|
body: CustomScrollView(
|
||||||
children: [
|
controller: controller,
|
||||||
BackButton(
|
slivers: [
|
||||||
color: tracksSnapshot.asData?.value != null
|
SliverAppBar(
|
||||||
? color?.titleTextColor
|
actions: collapsed.value ? buttons : null,
|
||||||
: null,
|
floating: false,
|
||||||
)
|
pinned: true,
|
||||||
],
|
expandedHeight: 400,
|
||||||
),
|
automaticallyImplyLeading: kIsMobile,
|
||||||
),
|
iconTheme: IconThemeData(color: color?.titleTextColor),
|
||||||
body: tracksSnapshot.when(
|
primary: true,
|
||||||
data: (tracks) {
|
backgroundColor: color?.color,
|
||||||
return CustomScrollView(
|
title: collapsed.value
|
||||||
controller: controller,
|
? Text(
|
||||||
slivers: [
|
title,
|
||||||
SliverAppBar(
|
style: Theme.of(context).textTheme.headline4?.copyWith(
|
||||||
actions: collapsed.value ? buttons : null,
|
color: color?.titleTextColor,
|
||||||
floating: false,
|
fontWeight: FontWeight.w600,
|
||||||
pinned: true,
|
|
||||||
expandedHeight: 400,
|
|
||||||
automaticallyImplyLeading: false,
|
|
||||||
primary: true,
|
|
||||||
title: collapsed.value
|
|
||||||
? Text(
|
|
||||||
title,
|
|
||||||
style:
|
|
||||||
Theme.of(context).textTheme.headline4?.copyWith(
|
|
||||||
color: color?.titleTextColor,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
backgroundColor: color?.color.withOpacity(0.8),
|
|
||||||
flexibleSpace: LayoutBuilder(builder: (context, constrains) {
|
|
||||||
return FlexibleSpaceBar(
|
|
||||||
background: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [
|
|
||||||
color?.color ?? Colors.transparent,
|
|
||||||
Theme.of(context).canvasColor,
|
|
||||||
],
|
|
||||||
begin: const FractionalOffset(0, 0),
|
|
||||||
end: const FractionalOffset(0, 1),
|
|
||||||
tileMode: TileMode.clamp,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Material(
|
|
||||||
type: MaterialType.transparency,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 20,
|
|
||||||
vertical: 20,
|
|
||||||
),
|
),
|
||||||
child: Wrap(
|
)
|
||||||
spacing: 20,
|
: null,
|
||||||
runSpacing: 20,
|
flexibleSpace: LayoutBuilder(builder: (context, constrains) {
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
return FlexibleSpaceBar(
|
||||||
alignment: WrapAlignment.center,
|
background: Container(
|
||||||
runAlignment: WrapAlignment.center,
|
decoration: BoxDecoration(
|
||||||
children: [
|
gradient: LinearGradient(
|
||||||
Container(
|
colors: [
|
||||||
constraints:
|
color?.color ?? Colors.transparent,
|
||||||
const BoxConstraints(maxHeight: 200),
|
Theme.of(context).canvasColor,
|
||||||
child: ClipRRect(
|
],
|
||||||
borderRadius: BorderRadius.circular(10),
|
begin: const FractionalOffset(0, 0),
|
||||||
child: CachedNetworkImage(
|
end: const FractionalOffset(0, 1),
|
||||||
imageUrl: titleImage,
|
tileMode: TileMode.clamp,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 20,
|
||||||
|
vertical: 20,
|
||||||
|
),
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 20,
|
||||||
|
runSpacing: 20,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
runAlignment: WrapAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
constraints:
|
||||||
|
const BoxConstraints(maxHeight: 200),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: titleImage,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Column(
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Column(
|
||||||
mainAxisAlignment:
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
MainAxisAlignment.spaceBetween,
|
mainAxisAlignment:
|
||||||
children: [
|
MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.headline4
|
||||||
|
?.copyWith(
|
||||||
|
color: color?.titleTextColor,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (description != null)
|
||||||
Text(
|
Text(
|
||||||
title,
|
description!,
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.headline4
|
.bodyLarge
|
||||||
?.copyWith(
|
?.copyWith(
|
||||||
color: color?.titleTextColor,
|
color: color?.bodyTextColor,
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
),
|
),
|
||||||
if (description != null)
|
const SizedBox(height: 10),
|
||||||
Text(
|
Row(
|
||||||
description!,
|
mainAxisSize: MainAxisSize.min,
|
||||||
style: Theme.of(context)
|
children: buttons,
|
||||||
.textTheme
|
),
|
||||||
.bodyLarge
|
],
|
||||||
?.copyWith(
|
)
|
||||||
color: color?.bodyTextColor,
|
],
|
||||||
),
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: buttons,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}),
|
);
|
||||||
),
|
}),
|
||||||
TracksTableView(
|
),
|
||||||
tracks is! List<Track>
|
tracksSnapshot.when(
|
||||||
? tracks
|
data: (tracks) {
|
||||||
.map((track) => simpleTrackToTrack(track, album!))
|
return TracksTableView(
|
||||||
.toList()
|
tracks is! List<Track>
|
||||||
: tracks,
|
? tracks
|
||||||
onTrackPlayButtonPressed: onPlay,
|
.map((track) => simpleTrackToTrack(track, album!))
|
||||||
playlistId: id,
|
.toList()
|
||||||
userPlaylist: isOwned,
|
: tracks,
|
||||||
),
|
onTrackPlayButtonPressed: onPlay,
|
||||||
],
|
playlistId: id,
|
||||||
);
|
userPlaylist: isOwned,
|
||||||
},
|
);
|
||||||
error: (error, _) => Text("Error $error"),
|
},
|
||||||
loading: () => const ShimmerTrackTile(),
|
error: (error, _) =>
|
||||||
),
|
SliverToBoxAdapter(child: Text("Error $error")),
|
||||||
),
|
loading: () => const ShimmerTrackTile(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,77 +97,5 @@ class TracksTableView extends HookConsumerWidget {
|
|||||||
}).toList()
|
}).toList()
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
return Container(
|
|
||||||
color: Theme.of(context).backgroundColor,
|
|
||||||
child: Scrollbar(
|
|
||||||
child: ListView(
|
|
||||||
children: [
|
|
||||||
if (heading != null) heading!,
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Text(
|
|
||||||
"#",
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: tableHeadStyle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Title",
|
|
||||||
style: tableHeadStyle,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// used alignment of this table-head
|
|
||||||
if (breakpoint.isMoreThan(Breakpoints.md)) ...[
|
|
||||||
const SizedBox(width: 100),
|
|
||||||
Expanded(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Album",
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: tableHeadStyle,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
if (!breakpoint.isSm) ...[
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Text("Time", style: tableHeadStyle),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
],
|
|
||||||
const SizedBox(width: 40),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
...tracks.asMap().entries.map((track) {
|
|
||||||
String? thumbnailUrl = imageToUrlString(
|
|
||||||
track.value.album?.images,
|
|
||||||
index: (track.value.album?.images?.length ?? 1) - 1,
|
|
||||||
);
|
|
||||||
String duration =
|
|
||||||
"${track.value.duration?.inMinutes.remainder(60)}:${zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
|
|
||||||
return TrackTile(
|
|
||||||
playback,
|
|
||||||
playlistId: playlistId,
|
|
||||||
track: track,
|
|
||||||
duration: duration,
|
|
||||||
thumbnailUrl: thumbnailUrl,
|
|
||||||
userPlaylist: userPlaylist,
|
|
||||||
onTrackPlayButtonPressed: onTrackPlayButtonPressed,
|
|
||||||
);
|
|
||||||
}).toList()
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
38
lib/hooks/useCustomStatusBarColor.dart
Normal file
38
lib/hooks/useCustomStatusBarColor.dart
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
|
||||||
|
void useCustomStatusBarColor(Color color, bool isCurrentRoute) {
|
||||||
|
final context = useContext();
|
||||||
|
final backgroundColor = Theme.of(context).backgroundColor;
|
||||||
|
resetStatusbar() => SystemChrome.setSystemUIOverlayStyle(
|
||||||
|
SystemUiOverlayStyle(
|
||||||
|
statusBarColor: backgroundColor, // status bar color
|
||||||
|
statusBarIconBrightness: backgroundColor.computeLuminance() > 0.179
|
||||||
|
? Brightness.dark
|
||||||
|
: Brightness.light,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final statusBarColor = SystemChrome.latestStyle?.statusBarColor;
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
if (isCurrentRoute && statusBarColor != color) {
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
|
SystemUiOverlayStyle(
|
||||||
|
statusBarColor: color, // status bar color
|
||||||
|
statusBarIconBrightness: color.computeLuminance() > 0.179
|
||||||
|
? Brightness.dark
|
||||||
|
: Brightness.light,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (!isCurrentRoute && statusBarColor == color) {
|
||||||
|
resetStatusbar();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}, [color, isCurrentRoute, statusBarColor]);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
return resetStatusbar;
|
||||||
|
}, []);
|
||||||
|
}
|
@ -7,6 +7,7 @@ import 'package:spotube/hooks/playback.dart';
|
|||||||
import 'package:spotube/models/GlobalKeyActions.dart';
|
import 'package:spotube/models/GlobalKeyActions.dart';
|
||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
import 'package:spotube/provider/UserPreferences.dart';
|
import 'package:spotube/provider/UserPreferences.dart';
|
||||||
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
useHotKeys(WidgetRef ref) {
|
useHotKeys(WidgetRef ref) {
|
||||||
final playback = ref.watch(playbackProvider);
|
final playback = ref.watch(playbackProvider);
|
||||||
@ -20,7 +21,7 @@ useHotKeys(WidgetRef ref) {
|
|||||||
final _playOrPause = useTogglePlayPause(playback);
|
final _playOrPause = useTogglePlayPause(playback);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (Platform.isIOS || Platform.isAndroid) return null;
|
if (kIsMobile) return null;
|
||||||
_hotKeys = [
|
_hotKeys = [
|
||||||
GlobalKeyActions(
|
GlobalKeyActions(
|
||||||
HotKey(KeyCode.space, scope: HotKeyScope.inapp),
|
HotKey(KeyCode.space, scope: HotKeyScope.inapp),
|
||||||
|
@ -16,13 +16,9 @@ import 'package:spotube/provider/YouTube.dart';
|
|||||||
import 'package:spotube/themes/dark-theme.dart';
|
import 'package:spotube/themes/dark-theme.dart';
|
||||||
import 'package:spotube/themes/light-theme.dart';
|
import 'package:spotube/themes/light-theme.dart';
|
||||||
import 'package:spotube/utils/AudioPlayerHandler.dart';
|
import 'package:spotube/utils/AudioPlayerHandler.dart';
|
||||||
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
// await JustAudioBackground.init(
|
|
||||||
// androidNotificationChannelId: 'oss.krtirtho.Spotube',
|
|
||||||
// androidNotificationChannelName: 'Spotube',
|
|
||||||
// androidNotificationOngoing: true,
|
|
||||||
// );
|
|
||||||
AudioPlayerHandler audioPlayerHandler = await AudioService.init(
|
AudioPlayerHandler audioPlayerHandler = await AudioService.init(
|
||||||
builder: () => AudioPlayerHandler(),
|
builder: () => AudioPlayerHandler(),
|
||||||
config: const AudioServiceConfig(
|
config: const AudioServiceConfig(
|
||||||
@ -31,12 +27,11 @@ void main() async {
|
|||||||
androidNotificationOngoing: true,
|
androidNotificationOngoing: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (!Platform.isAndroid && !Platform.isIOS) {
|
if (kIsDesktop) {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await hotKeyManager.unregisterAll();
|
await hotKeyManager.unregisterAll();
|
||||||
doWhenWindowReady(() {
|
doWhenWindowReady(() {
|
||||||
appWindow.minSize =
|
appWindow.minSize = const Size(359, 700);
|
||||||
Size(Platform.isAndroid || Platform.isIOS ? 280 : 359, 700);
|
|
||||||
appWindow.alignment = Alignment.center;
|
appWindow.alignment = Alignment.center;
|
||||||
appWindow.title = "Spotube";
|
appWindow.title = "Spotube";
|
||||||
appWindow.maximize();
|
appWindow.maximize();
|
||||||
@ -75,6 +70,7 @@ class Spotube extends HookConsumerWidget {
|
|||||||
.watch(userPreferencesProvider.select((s) => s.backgroundColorScheme));
|
.watch(userPreferencesProvider.select((s) => s.backgroundColorScheme));
|
||||||
final player = ref.watch(audioPlayerProvider);
|
final player = ref.watch(audioPlayerProvider);
|
||||||
final youtube = ref.watch(youtubeProvider);
|
final youtube = ref.watch(youtubeProvider);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
return () {
|
return () {
|
||||||
player.dispose();
|
player.dispose();
|
||||||
|
@ -67,8 +67,8 @@ GoRouter createGoRouter() => GoRouter(
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: "/player",
|
path: "/player",
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
return SpotubePage(
|
return const SpotubePage(
|
||||||
child: const PlayerView(),
|
child: PlayerView(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -72,7 +72,9 @@ class Auth extends PersistedChangeNotifier {
|
|||||||
_clientSecret = map["clientSecret"];
|
_clientSecret = map["clientSecret"];
|
||||||
_accessToken = map["accessToken"];
|
_accessToken = map["accessToken"];
|
||||||
_refreshToken = map["refreshToken"];
|
_refreshToken = map["refreshToken"];
|
||||||
_expiration = DateTime.tryParse(map["expiration"]);
|
_expiration = map["expiration"] != null
|
||||||
|
? DateTime.tryParse(map["expiration"])
|
||||||
|
: _expiration;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
5
lib/utils/platform.dart
Normal file
5
lib/utils/platform.dart
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
final kIsDesktop = Platform.isLinux || Platform.isWindows || Platform.isMacOS;
|
||||||
|
|
||||||
|
final kIsMobile = Platform.isAndroid || Platform.isIOS;
|
Loading…
Reference in New Issue
Block a user