Merge branch 'dev' into android-audio-session-broadcast-clean
1
Makefile
@ -28,6 +28,7 @@ publishaur:
|
||||
|
||||
innoinstall:
|
||||
powershell curl -o build\installer.exe http://files.jrsoftware.org/is/6/innosetup-${INNO_VERSION}.exe
|
||||
powershell git clone https://github.com/DomGries/InnoDependencyInstaller.git build\inno-depend
|
||||
powershell build\installer.exe /verysilent /allusers /dir=build\iscc
|
||||
|
||||
inno:
|
||||
|
||||
@ -31,4 +31,6 @@ linter:
|
||||
analyzer:
|
||||
enable-experiment:
|
||||
- records
|
||||
- patterns
|
||||
- patterns
|
||||
errors:
|
||||
invalid_annotation_target: ignore
|
||||
|
||||
@ -21,6 +21,6 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>11.0</string>
|
||||
<string>12.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '11.0'
|
||||
# platform :ios, '12.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
@ -196,7 +196,7 @@ SPEC CHECKSUMS:
|
||||
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
||||
file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de
|
||||
file_selector_ios: 8c25d700d625e1dcdd6599f2d927072f2254647b
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_inappwebview: acd4fc0f012cefd09015000c241137d82f01ba62
|
||||
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
||||
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
|
||||
@ -221,6 +221,6 @@ SPEC CHECKSUMS:
|
||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
|
||||
|
||||
PODFILE CHECKSUM: e36c7ad9836dfd8d22934c7680185432a658e28f
|
||||
PODFILE CHECKSUM: 5129d2e80ab0dfc533f262cedf032011b1dfe4fd
|
||||
|
||||
COCOAPODS: 1.14.3
|
||||
COCOAPODS: 1.15.2
|
||||
|
||||
@ -406,7 +406,7 @@
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1430;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
@ -1056,6 +1056,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "stable-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1078,6 +1079,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "stable-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1099,6 +1101,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "stable-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1198,6 +1201,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "stable-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1294,6 +1298,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "stable-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1387,6 +1392,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "stable-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1408,6 +1414,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "dev-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1430,6 +1437,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "dev-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1452,6 +1460,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "dev-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1473,6 +1482,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "dev-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1494,6 +1504,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "dev-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1515,6 +1526,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "dev-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1614,6 +1626,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "stable-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1636,6 +1649,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "dev-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1732,6 +1746,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "stable-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1753,6 +1768,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "dev-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1846,6 +1862,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "stable-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1867,6 +1884,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "dev-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1888,6 +1906,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "nightly-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1910,6 +1929,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "nightly-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1932,6 +1952,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "nightly-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1954,6 +1975,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "nightly-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1975,6 +1997,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "nightly-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -1996,6 +2019,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "nightly-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -2017,6 +2041,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "nightly-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -2038,6 +2063,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "nightly-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -2059,6 +2085,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "nightly-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -2158,6 +2185,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "stable-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -2180,6 +2208,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "dev-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -2202,6 +2231,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "nightly-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -2298,6 +2328,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "stable-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -2319,6 +2350,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "dev-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -2340,6 +2372,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "nightly-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -2433,6 +2466,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "stable-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -2454,6 +2488,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "dev-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -2475,6 +2510,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = "nightly-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@ -111,4 +111,5 @@ abstract class SpotubeIcons {
|
||||
static const wikipedia = SimpleIcons.wikipedia;
|
||||
static const discord = SimpleIcons.discord;
|
||||
static const youtube = SimpleIcons.youtube;
|
||||
static const radio = FeatherIcons.radio;
|
||||
}
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import 'dart:ffi';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
@ -69,22 +72,27 @@ class HomePageFriendsSection extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final group in friendGroup)
|
||||
Row(
|
||||
children: [
|
||||
for (final friend in group)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: FriendItem(friend: friend),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
child: ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(
|
||||
dragDevices: PointerDeviceKind.values.toSet(),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final group in friendGroup)
|
||||
Row(
|
||||
children: [
|
||||
for (final friend in group)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: FriendItem(friend: friend),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -50,10 +50,11 @@ enum SortBy {
|
||||
none,
|
||||
ascending,
|
||||
descending,
|
||||
artist,
|
||||
album,
|
||||
newest,
|
||||
oldest,
|
||||
duration,
|
||||
artist,
|
||||
album,
|
||||
}
|
||||
|
||||
final localTracksProvider = FutureProvider<List<LocalTrack>>((ref) async {
|
||||
|
||||
@ -94,10 +94,10 @@ class PlayerView extends HookConsumerWidget {
|
||||
|
||||
final topPadding = MediaQueryData.fromView(View.of(context)).padding.top;
|
||||
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvoked: (didPop) async {
|
||||
panelController.close();
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
await panelController.close();
|
||||
return false;
|
||||
},
|
||||
child: IconTheme(
|
||||
data: theme.iconTheme.copyWith(color: bodyTextColor),
|
||||
|
||||
@ -48,6 +48,11 @@ class SortTracksDropdown extends StatelessWidget {
|
||||
enabled: value != SortBy.oldest,
|
||||
title: Text(context.l10n.sort_oldest),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: SortBy.duration,
|
||||
enabled: value != SortBy.duration,
|
||||
title: Text(context.l10n.sort_duration),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: SortBy.artist,
|
||||
enabled: value != SortBy.artist,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:flutter/material.dart' hide Page;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@ -10,6 +11,7 @@ import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/library/user_local_tracks.dart';
|
||||
import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart';
|
||||
import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart';
|
||||
import 'package:spotube/components/shared/dialogs/prompt_dialog.dart';
|
||||
import 'package:spotube/components/shared/dialogs/track_details_dialog.dart';
|
||||
import 'package:spotube/components/shared/heart_button.dart';
|
||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||
@ -20,7 +22,9 @@ import 'package:spotube/provider/authentication_provider.dart';
|
||||
import 'package:spotube/provider/blacklist_provider.dart';
|
||||
import 'package:spotube/provider/download_manager_provider.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/provider/spotify_provider.dart';
|
||||
import 'package:spotube/services/mutations/mutations.dart';
|
||||
import 'package:spotube/services/queries/search.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
|
||||
enum TrackOptionValue {
|
||||
@ -36,6 +40,7 @@ enum TrackOptionValue {
|
||||
favorite,
|
||||
details,
|
||||
download,
|
||||
startRadio,
|
||||
}
|
||||
|
||||
class TrackOptions extends HookConsumerWidget {
|
||||
@ -82,6 +87,79 @@ class TrackOptions extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void actionStartRadio(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
Track track,
|
||||
) async {
|
||||
final playback = ref.read(ProxyPlaylistNotifier.notifier);
|
||||
final playlist = ref.read(ProxyPlaylistNotifier.provider);
|
||||
final spotify = ref.read(spotifyProvider);
|
||||
final query = "${track.name} Radio";
|
||||
final pages = await QueryClient.of(context)
|
||||
.fetchInfiniteQueryJob<List<Page>, dynamic, int, SearchParams>(
|
||||
job: SearchQueries.queryJob(query),
|
||||
args: (
|
||||
spotify: spotify,
|
||||
searchType: SearchType.playlist,
|
||||
query: query,
|
||||
),
|
||||
) ??
|
||||
[];
|
||||
|
||||
final radios = pages
|
||||
.expand((e) => e.items?.toList() ?? <PlaylistSimple>[])
|
||||
.toList()
|
||||
.cast<PlaylistSimple>();
|
||||
|
||||
final artists = track.artists!.map((e) => e.name);
|
||||
|
||||
final radio = radios.firstWhere(
|
||||
(e) {
|
||||
final validPlaylists =
|
||||
artists.where((a) => e.description!.contains(a!));
|
||||
return e.name == "${track.name} Radio" &&
|
||||
(validPlaylists.length >= 2 ||
|
||||
validPlaylists.length == artists.length) &&
|
||||
e.owner?.displayName == "Spotify";
|
||||
},
|
||||
orElse: () => radios.first,
|
||||
);
|
||||
|
||||
bool replaceQueue = false;
|
||||
|
||||
if (context.mounted && playlist.tracks.isNotEmpty) {
|
||||
replaceQueue = await showPromptDialog(
|
||||
context: context,
|
||||
title: context.l10n.how_to_start_radio,
|
||||
message: context.l10n.replace_queue_question,
|
||||
okText: context.l10n.replace,
|
||||
cancelText: context.l10n.add_to_queue,
|
||||
);
|
||||
}
|
||||
|
||||
if (replaceQueue || playlist.tracks.isEmpty) {
|
||||
await playback.stop();
|
||||
await playback.load([track], autoPlay: true);
|
||||
|
||||
// we don't have to add those tracks as useEndlessPlayback will do it for us
|
||||
return;
|
||||
} else {
|
||||
await playback.addTrack(track);
|
||||
}
|
||||
|
||||
final tracks =
|
||||
await spotify.playlists.getTracksByPlaylistId(radio.id!).all();
|
||||
|
||||
await playback.addTracks(
|
||||
tracks.toList()
|
||||
..removeWhere((e) {
|
||||
final isDuplicate = playlist.tracks.any((t) => t.id == e.id);
|
||||
return e.id == track.id || isDuplicate;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||
@ -207,6 +285,9 @@ class TrackOptions extends HookConsumerWidget {
|
||||
case TrackOptionValue.download:
|
||||
await downloadManager.addToQueue(track);
|
||||
break;
|
||||
case TrackOptionValue.startRadio:
|
||||
actionStartRadio(context, ref, track);
|
||||
break;
|
||||
}
|
||||
},
|
||||
icon: icon ?? const Icon(SpotubeIcons.moreHorizontal),
|
||||
@ -287,12 +368,18 @@ class TrackOptions extends HookConsumerWidget {
|
||||
: context.l10n.save_as_favorite,
|
||||
),
|
||||
),
|
||||
if (auth != null)
|
||||
if (auth != null) ...[
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.startRadio,
|
||||
leading: const Icon(SpotubeIcons.radio),
|
||||
title: Text(context.l10n.start_a_radio),
|
||||
),
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.addToPlaylist,
|
||||
leading: const Icon(SpotubeIcons.playlistAdd),
|
||||
title: Text(context.l10n.add_to_playlist),
|
||||
),
|
||||
],
|
||||
if (userPlaylist && auth != null)
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.removeFromPlaylist,
|
||||
|
||||
@ -70,9 +70,9 @@ class TrackViewHeaderActions extends HookConsumerWidget {
|
||||
tooltip: props.isLiked
|
||||
? context.l10n.remove_from_favorites
|
||||
: context.l10n.save_as_favorite,
|
||||
onPressed: () {
|
||||
props.onHeart?.call();
|
||||
if (isUserPlaylist) {
|
||||
onPressed: () async {
|
||||
final shouldPop = await props.onHeart?.call();
|
||||
if (isUserPlaylist && shouldPop == true && context.mounted) {
|
||||
context.pop();
|
||||
}
|
||||
},
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:flutter/material.dart' hide Page;
|
||||
import 'package:spotify/spotify.dart';
|
||||
@ -62,7 +64,7 @@ class InheritedTrackView extends InheritedWidget {
|
||||
final String shareUrl;
|
||||
|
||||
// events
|
||||
final VoidCallback? onHeart; // if null heart button will hidden
|
||||
final FutureOr<bool?> Function()? onHeart; // if null heart button will hidden
|
||||
|
||||
const InheritedTrackView({
|
||||
super.key,
|
||||
|
||||
103
lib/hooks/configurators/use_endless_playback.dart
Normal file
@ -0,0 +1,103 @@
|
||||
import 'package:catcher_2/catcher_2.dart';
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/provider/authentication_provider.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/provider/spotify_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/queries/search.dart';
|
||||
|
||||
void useEndlessPlayback(WidgetRef ref) {
|
||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
||||
final playback = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
final endlessPlayback =
|
||||
ref.watch(userPreferencesProvider.select((s) => s.endlessPlayback));
|
||||
|
||||
final queryClient = useQueryClient();
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
if (!endlessPlayback || auth == null) return null;
|
||||
|
||||
void listener(int index) async {
|
||||
try {
|
||||
final playlist = ref.read(ProxyPlaylistNotifier.provider);
|
||||
if (index != playlist.tracks.length - 1) return;
|
||||
|
||||
final track = playlist.tracks.last;
|
||||
|
||||
final query = "${track.name} Radio";
|
||||
final pages = await queryClient.fetchInfiniteQueryJob<List<Page>,
|
||||
dynamic, int, SearchParams>(
|
||||
job: SearchQueries.queryJob(query),
|
||||
args: (
|
||||
spotify: spotify,
|
||||
searchType: SearchType.playlist,
|
||||
query: query
|
||||
),
|
||||
) ??
|
||||
[];
|
||||
|
||||
final radios = pages
|
||||
.expand((e) => e.items?.toList() ?? <PlaylistSimple>[])
|
||||
.toList()
|
||||
.cast<PlaylistSimple>();
|
||||
|
||||
final artists = track.artists!.map((e) => e.name);
|
||||
|
||||
final radio = radios.firstWhere(
|
||||
(e) {
|
||||
final validPlaylists =
|
||||
artists.where((a) => e.description!.contains(a!));
|
||||
return e.name == "${track.name} Radio" &&
|
||||
(validPlaylists.length >= 2 ||
|
||||
validPlaylists.length == artists.length) &&
|
||||
e.owner?.displayName != "Spotify";
|
||||
},
|
||||
orElse: () => radios.first,
|
||||
);
|
||||
|
||||
final tracks =
|
||||
await spotify.playlists.getTracksByPlaylistId(radio.id!).all();
|
||||
|
||||
await playback.addTracks(
|
||||
tracks.toList()
|
||||
..removeWhere((e) {
|
||||
final playlist = ref.read(ProxyPlaylistNotifier.provider);
|
||||
final isDuplicate = playlist.tracks.any((t) => t.id == e.id);
|
||||
return e.id == track.id || isDuplicate;
|
||||
}),
|
||||
);
|
||||
} catch (e, stack) {
|
||||
Catcher2.reportCheckedError(e, stack);
|
||||
}
|
||||
}
|
||||
|
||||
// Sometimes user can change settings for which the currentIndexChanged
|
||||
// might not be called. So we need to check if the current track is the
|
||||
// last track and if it is then we need to call the listener manually.
|
||||
if (playlist.active == playlist.tracks.length - 1 &&
|
||||
audioPlayer.isPlaying) {
|
||||
listener(playlist.active!);
|
||||
}
|
||||
|
||||
final subscription =
|
||||
audioPlayer.currentIndexChangedStream.listen(listener);
|
||||
|
||||
return subscription.cancel;
|
||||
},
|
||||
[
|
||||
spotify,
|
||||
playback,
|
||||
queryClient,
|
||||
playlist.tracks,
|
||||
endlessPlayback,
|
||||
auth,
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -41,6 +41,7 @@
|
||||
"sort_z_a": "Sort by Z-A",
|
||||
"sort_artist": "Sort by Artist",
|
||||
"sort_album": "Sort by Album",
|
||||
"sort_duration": "Sort by Duration",
|
||||
"sort_tracks": "Sort Tracks",
|
||||
"currently_downloading": "Currently Downloading ({tracks_length})",
|
||||
"cancel_all": "Cancel All",
|
||||
@ -286,5 +287,12 @@
|
||||
"genres": "Genres",
|
||||
"explore_genres": "Explore Genres",
|
||||
"friends": "Friends",
|
||||
"no_lyrics_available": "Sorry, unable find lyrics for this track"
|
||||
"no_lyrics_available": "Sorry, unable find lyrics for this track",
|
||||
"start_a_radio": "Start a Radio",
|
||||
"how_to_start_radio": "How do you want to start the radio?",
|
||||
"replace_queue_question": "Do you want to replace the current queue or append to it?",
|
||||
"endless_playback": "Endless Playback",
|
||||
"delete_playlist": "Delete Playlist",
|
||||
"delete_playlist_confirmation": "Are you sure you want to delete this playlist?",
|
||||
"local_tracks": "Local Tracks"
|
||||
}
|
||||
@ -1,107 +1,107 @@
|
||||
{
|
||||
"guest": "Gast",
|
||||
"browse": "Bladeren",
|
||||
"search": "Zoek op",
|
||||
"search": "Zoeken",
|
||||
"library": "Bibliotheek",
|
||||
"lyrics": "Liedteksten",
|
||||
"lyrics": "Teksten",
|
||||
"settings": "Instellingen",
|
||||
"genre_categories_filter": "Categorieën of genres filteren...",
|
||||
"genre_categories_filter": "Categorieën of genres filteren…",
|
||||
"genre": "Genre",
|
||||
"personalized": "Gepersonaliseerd",
|
||||
"featured": "Aanbevolen",
|
||||
"new_releases": "Nieuwe uitgaves",
|
||||
"songs": "Liedjes",
|
||||
"playing_track": "{track} afspelen",
|
||||
"queue_clear_alert": "Dit zal de huidige wachtrij wissen. {track_length} tracks worden verwijderd\nWilt u doorgaan?",
|
||||
"queue_clear_alert": "Dit zal de huidige wachtrij wissen. {track_length} nummers worden verwijderd\nWil je doorgaan?",
|
||||
"load_more": "Meer laden",
|
||||
"playlists": "Afspeellijsten",
|
||||
"artists": "Kunstenaars",
|
||||
"artists": "Artiesten",
|
||||
"albums": "Albums",
|
||||
"tracks": "Nummers",
|
||||
"downloads": "Downloads",
|
||||
"filter_playlists": "Filter uw afspeellijsten...",
|
||||
"filter_playlists": "Afspeellijsten filteren…",
|
||||
"liked_tracks": "Geliefde tracks",
|
||||
"liked_tracks_description": "Al je favoriete nummers",
|
||||
"create_playlist": "Afspeellijst maken",
|
||||
"create_a_playlist": "Een afspeellijst maken",
|
||||
"create_playlist": "Afspeellijst aanmaken",
|
||||
"create_a_playlist": "Een afspeellijst aanmaken",
|
||||
"update_playlist": "Afspeellijst bijwerken",
|
||||
"create": "Maak",
|
||||
"create": "Aanmaken",
|
||||
"cancel": "Annuleren",
|
||||
"update": "Bijwerken",
|
||||
"playlist_name": "Afspeellijstnaam",
|
||||
"playlist_name": "Naam afspeellijst",
|
||||
"name_of_playlist": "Naam van de afspeellijst",
|
||||
"description": "Beschrijving",
|
||||
"public": "Openbaar",
|
||||
"collaborative": "Samenwerkend",
|
||||
"search_local_tracks": "Lokale nummers zoeken...",
|
||||
"play": "Speel",
|
||||
"search_local_tracks": "Lokale nummers zoeken…",
|
||||
"play": "Afspelen",
|
||||
"delete": "Wissen",
|
||||
"none": "Geen",
|
||||
"sort_a_z": "Sorteren op A-Z",
|
||||
"sort_z_a": "Sorteren op Z-A",
|
||||
"sort_artist": "Sorteren op kunstenaar",
|
||||
"sort_artist": "Sorteren op artiest",
|
||||
"sort_album": "Sorteren op album",
|
||||
"sort_tracks": "Nummers sorteren",
|
||||
"currently_downloading": "Momenteel aan het downloaden ({tracks_length})",
|
||||
"cancel_all": "Alle annuleren",
|
||||
"filter_artist": "Kunstenaars filteren...",
|
||||
"filter_artist": "Artiesten filteren…",
|
||||
"followers": "{followers} volgers",
|
||||
"add_artist_to_blacklist": "Kunstenaar toevoegen aan zwarte lijst",
|
||||
"add_artist_to_blacklist": "Artiest toevoegen aan zwarte lijst",
|
||||
"top_tracks": "Topsporen",
|
||||
"fans_also_like": "Liefhebbers willen ook",
|
||||
"loading": "Aan het laden...",
|
||||
"artist": "Kunstenaar",
|
||||
"blacklisted": "Op de zwarte lijst",
|
||||
"following": "Op volg",
|
||||
"loading": "Laden…",
|
||||
"artist": "Artiest",
|
||||
"blacklisted": "Zwarte lijst",
|
||||
"following": "Volgen",
|
||||
"follow": "Volgen",
|
||||
"artist_url_copied": "URL artiest gekopieerd naar klembord",
|
||||
"added_to_queue": "{tracks} tracks toegevoegd aan wachtrij",
|
||||
"filter_albums": "Albums filteren...",
|
||||
"added_to_queue": "{tracks} nummers toegevoegd aan wachtrij",
|
||||
"filter_albums": "Albums filteren…",
|
||||
"synced": "Gesynchroniseerd",
|
||||
"plain": "Eenvoudig",
|
||||
"shuffle": "Schuifelen",
|
||||
"search_tracks": "Zoek nummers...",
|
||||
"released": "Vrijgegeven",
|
||||
"shuffle": "Willekeurig",
|
||||
"search_tracks": "Nummers zoeken…",
|
||||
"released": "Uitgegeven",
|
||||
"error": "Fout {error}",
|
||||
"title": "Titel",
|
||||
"time": "Tijd",
|
||||
"more_actions": "Meer acties",
|
||||
"download_count": "({count}) downloads",
|
||||
"add_count_to_playlist": "Voeg ({count}) toe aan afspeellijst",
|
||||
"add_count_to_queue": "Voeg ({count}) toe aan wachtrij",
|
||||
"play_count_next": "Speel ({count}) volgende",
|
||||
"add_count_to_playlist": "({count}) aan afspeellijst toevoegen",
|
||||
"add_count_to_queue": "({count}) aan wachtrij toevoegen",
|
||||
"play_count_next": "Volgende ({count}) afspelen",
|
||||
"album": "Album",
|
||||
"copied_to_clipboard": "{data} naar klembord gekopieerd",
|
||||
"add_to_following_playlists": "Voeg {track} toe aan volgende afspeellijsten",
|
||||
"add_to_following_playlists": "{track} aan volgende afspeellijsten toevoegen",
|
||||
"add": "Toevoegen",
|
||||
"added_track_to_queue": "{track} toegevoegd aan wachtrij",
|
||||
"added_track_to_queue": "{track} aan wachtrij toegevoegd",
|
||||
"add_to_queue": "Toevoegen aan wachtrij",
|
||||
"track_will_play_next": "{track} zal hierna spelen",
|
||||
"track_will_play_next": "{track} wordt hierna afgespeeld",
|
||||
"play_next": "Volgende afspelen",
|
||||
"removed_track_from_queue": "{track} uit wachtrij verwijderd",
|
||||
"remove_from_queue": "Verwijderen uit wachtrij",
|
||||
"remove_from_favorites": "Verwijderen uit favorieten",
|
||||
"removed_track_from_queue": "{track} van wachtrij verwijderd",
|
||||
"remove_from_queue": "Van wachtrij verwijderen",
|
||||
"remove_from_favorites": "Van favorieten verwijderen",
|
||||
"save_as_favorite": "Opslaan als favoriet",
|
||||
"add_to_playlist": "Toevoegen aan afspeellijst",
|
||||
"remove_from_playlist": "Verwijderen uit afspeellijst",
|
||||
"add_to_blacklist": "Toevoegen aan zwarte lijst",
|
||||
"remove_from_blacklist": "Verwijderen uit zwarte lijst",
|
||||
"add_to_playlist": "Aan afspeellijst toevoegen",
|
||||
"remove_from_playlist": "Van afspeellijst verwijderen",
|
||||
"add_to_blacklist": "Aan zwarte lijst toevoegen",
|
||||
"remove_from_blacklist": "Van zwarte lijst verwijderen",
|
||||
"share": "Delen",
|
||||
"mini_player": "Minispeler",
|
||||
"slide_to_seek": "Schuif om vooruit of achteruit te zoeken",
|
||||
"slide_to_seek": "Schuiven om vooruit of achteruit te zoeken",
|
||||
"shuffle_playlist": "Afspeellijst schuifelen",
|
||||
"unshuffle_playlist": "Afspeellijst onschuifelen",
|
||||
"previous_track": "Vorige nummer",
|
||||
"next_track": "Volgende nummer",
|
||||
"pause_playback": "Weergave pauzeren",
|
||||
"resume_playback": "Weergave hervatten",
|
||||
"loop_track": "Nummer loopen",
|
||||
"pause_playback": "Afspelen pauzeren",
|
||||
"resume_playback": "Afspelen hervatten",
|
||||
"loop_track": "Nummer herhalen",
|
||||
"repeat_playlist": "Afspeellijst herhalen",
|
||||
"queue": "Wachtrij",
|
||||
"alternative_track_sources": "Alternatieve nummerbronnen",
|
||||
"download_track": "Nummer downloaden",
|
||||
"tracks_in_queue": "{tracks} tracks in wachtrij",
|
||||
"clear_all": "Wis alles",
|
||||
"tracks_in_queue": "{tracks} nummers in wachtrij",
|
||||
"clear_all": "Alles wissen",
|
||||
"show_hide_ui_on_hover": "UI tonen/verbergen bij zweven",
|
||||
"always_on_top": "Altijd bovenaan",
|
||||
"exit_mini_player": "Minispeler afsluiten",
|
||||
@ -111,7 +111,7 @@
|
||||
"connect_with_spotify": "Verbinden met Spotify",
|
||||
"logout": "Afmelden",
|
||||
"logout_of_this_account": "Afmelden van dit account",
|
||||
"language_region": "Taal & Regio",
|
||||
"language_region": "Taal & regio",
|
||||
"language": "Taal",
|
||||
"system_default": "Systeemstandaard",
|
||||
"market_place_region": "Marktplaats-regio",
|
||||
@ -119,76 +119,78 @@
|
||||
"appearance": "Uiterlijk",
|
||||
"layout_mode": "Opmaakmodus",
|
||||
"override_layout_settings": "Instellingen voor responsieve opmaakmodus opheffen",
|
||||
"adaptive": "Aanpassingsgericht",
|
||||
"adaptive": "Adaptief",
|
||||
"compact": "Compact",
|
||||
"extended": "Uitgebreide",
|
||||
"extended": "Uitgebreid",
|
||||
"theme": "Thema",
|
||||
"dark": "Donker",
|
||||
"light": "Licht",
|
||||
"system": "Systeem",
|
||||
"accent_color": "Accentkleur",
|
||||
"sync_album_color": "Albumkleur synchroniseren",
|
||||
"sync_album_color_description": "Gebruikt de overheersende kleur van het albumartikel als accentkleur",
|
||||
"sync_album_color_description": "Gebruikt de overheersende kleur van het album als accentkleur",
|
||||
"playback": "Weergave",
|
||||
"audio_quality": "Audiokwaliteit",
|
||||
"high": "Hoog",
|
||||
"low": "Laag",
|
||||
"pre_download_play": "Vooraf downloaden en spelen",
|
||||
"pre_download_play": "Vooraf downloaden en afspelen",
|
||||
"pre_download_play_description": "In plaats van audio te streamen, kun je bytes downloaden en afspelen (aanbevolen voor gebruikers met een hogere bandbreedte)",
|
||||
"skip_non_music": "Niet-muzieksegmenten overslaan (SponsorBlock)",
|
||||
"blacklist_description": "Nummers en artiesten op de zwarte lijst",
|
||||
"wait_for_download_to_finish": "Wacht tot de huidige download is voltooid",
|
||||
"desktop": "Bureaublad",
|
||||
"close_behavior": "Sluitgedrag",
|
||||
"close": "Sluit af",
|
||||
"minimize_to_tray": "Minimaliseren naar lade",
|
||||
"close": "Afsluiten",
|
||||
"minimize_to_tray": "Minimaliseren naar systeemvak",
|
||||
"show_tray_icon": "Systeemvakpictogram tonen",
|
||||
"about": "Over",
|
||||
"u_love_spotube": "We weten dat jullie van Spotube houden",
|
||||
"u_love_spotube": "We weten dat je van Spotube houd",
|
||||
"check_for_updates": "Controleren op updates",
|
||||
"about_spotube": "Over Spotube",
|
||||
"blacklist": "Zwarte lijst",
|
||||
"please_sponsor": "Sponsor/Doneer a.u.b.",
|
||||
"spotube_description": "Spotube, een lichtgewicht, cross-platform, vrij-voor-alles Spotify-client",
|
||||
"version": "Versie",
|
||||
"build_number": "Beeldnummer",
|
||||
"founder": "Stichter",
|
||||
"build_number": "Bouwnummer",
|
||||
"founder": "Grondlegger",
|
||||
"repository": "Opslagplaats",
|
||||
"bug_issues": "Bug+problemen",
|
||||
"made_with": "Gemaakt met ❤️ in Bangladesh🇧🇩",
|
||||
"made_with": "Met ❤️ gemaakt in Bangladesh🇧🇩",
|
||||
"kingkor_roy_tirtho": "Kingkor Roy Tirtho",
|
||||
"copyright": "© 2021-{current_year} Kingkor Roy Tirtho",
|
||||
"license": "Licentie",
|
||||
"add_spotify_credentials": "Voeg je spotify-referenties toe om te beginnen",
|
||||
"credentials_will_not_be_shared_disclaimer": "Maakt u geen zorgen, uw gegevens worden niet verzameld of gedeeld met anderen.",
|
||||
"know_how_to_login": "Weet u niet hoe u dit moet doen?",
|
||||
"follow_step_by_step_guide": "Volg de stap voor stap gids",
|
||||
"add_spotify_credentials": "Voeg om te beginnen je spotify-aanmeldgegevens toe",
|
||||
"credentials_will_not_be_shared_disclaimer": "Maak je geen zorgen, je gegevens worden niet verzameld of gedeeld met anderen.",
|
||||
"know_how_to_login": "Weet je niet hoe je dit moet doen?",
|
||||
"follow_step_by_step_guide": "Volg de stapsgewijze handleiding",
|
||||
"spotify_cookie": "Spotify {name} Cookie",
|
||||
"cookie_name_cookie": "{name} Cookie",
|
||||
"fill_in_all_fields": "Vul alle velden in a.u.b.",
|
||||
"submit": "Verzenden",
|
||||
"exit": "Ga weg",
|
||||
"exit": "Afronden",
|
||||
"previous": "Vorige",
|
||||
"next": "Volgende",
|
||||
"done": "Klaar",
|
||||
"step_1": "Stap 1",
|
||||
"first_go_to": "Ga eerst naar",
|
||||
"login_if_not_logged_in": "en Inloggen/Aanmelden als u niet bent ingelogd",
|
||||
"login_if_not_logged_in": "en Inloggen/Aanmelden als je niet bent ingelogd",
|
||||
"step_2": "Stap 2",
|
||||
"step_2_steps": "1. Zodra je bent aangemeld, druk je op F12 of klik je met de rechtermuisknop > Inspect om de Browser devtools te openen.\n2. Ga vervolgens naar het tabblad \"Toepassing\" (Chrome, Edge, Brave enz..) of naar het tabblad \"Opslag\" (Firefox, Palemoon enz..).\n3. Ga naar de sectie \"Cookies\" en vervolgens naar de subsectie \"https://accounts.spotify.com\".",
|
||||
"step_3": "Stap 3",
|
||||
"step_3_steps": "De waarde van cookie \"sp_dc\" kopiëren",
|
||||
"success_emoji": "Succes🥳",
|
||||
"success_message": "Je bent nu succesvol ingelogd met je Spotify account. Goed gedaan, maat!",
|
||||
"success_message": "Je bent nu ingelogd met je Spotify account. Goed gedaan!",
|
||||
"step_4": "Stap 4",
|
||||
"step_4_steps": "De gekopieerde waarde \"sp_dc\" plakken",
|
||||
"something_went_wrong": "Er ging iets mis",
|
||||
"piped_instance": "Piped-serverinstantie",
|
||||
"piped_description": "De Piped-serverinstantie die moet worden gebruikt voor het matchen van sporen",
|
||||
"piped_description": "De Piped-serverinstantie die moet worden gebruikt voor overeenkomstige nummers",
|
||||
"piped_warning": "Sommige werken misschien niet goed. Dus gebruik ze op eigen risico",
|
||||
"generate_playlist": "Afspeellijst genereren",
|
||||
"track_exists": "Nummer {track} bestaat al",
|
||||
"replace_downloaded_tracks": "Alle gedownloade nummers vervangen",
|
||||
"skip_download_tracks": "Downloaden van alle gedownloade nummers overslaan",
|
||||
"do_you_want_to_replace": "Wil je de bestaande nummer vervangen?",
|
||||
"do_you_want_to_replace": "Wil je het bestaande nummer vervangen?",
|
||||
"replace": "Vervangen",
|
||||
"skip": "Overslaan",
|
||||
"select_up_to_count_type": "Selecteer tot {count} {type}",
|
||||
@ -196,13 +198,13 @@
|
||||
"add_genres": "Genres toevoegen",
|
||||
"country": "Land",
|
||||
"number_of_tracks_generate": "Aantal nummers om te genereren",
|
||||
"acousticness": "Akoesticiteit",
|
||||
"acousticness": "Akoestiek",
|
||||
"danceability": "Dansbaarheid",
|
||||
"energy": "Energie",
|
||||
"instrumentalness": "Instrumentaliteit",
|
||||
"liveness": "Levendigheid",
|
||||
"loudness": "Luidheid",
|
||||
"speechiness": "Sprakeligheid",
|
||||
"speechiness": "Spraak",
|
||||
"valence": "Valentie",
|
||||
"popularity": "Populariteit",
|
||||
"key": "Sleutel",
|
||||
@ -217,16 +219,16 @@
|
||||
"max": "Max",
|
||||
"target": "Doel",
|
||||
"moderate": "Matig",
|
||||
"deselect_all": "Alles deselecteren",
|
||||
"deselect_all": "Selectie opheffen",
|
||||
"select_all": "Alles selecteren",
|
||||
"are_you_sure": "Weet je het zeker?",
|
||||
"generating_playlist": "Je aangepaste afspeellijst genereren...",
|
||||
"generating_playlist": "Aangepaste afspeellijst genereren…",
|
||||
"selected_count_tracks": "{count} nummers geselecteerd",
|
||||
"download_warning": "Als je alle Tracks in bulk downloadt, ben je duidelijk bezig met muziekpiraterij en breng je schade toe aan de creatieve muziekmaatschappij. Ik hoop dat je je hiervan bewust bent. Probeer altijd het harde werk van artiesten te respecteren en te steunen.",
|
||||
"download_ip_ban_warning": "BTW, je IP-adres kan worden geblokkeerd op YouTube als gevolg van buitensporige downloadverzoeken dan normaal. IP blokkering betekent dat je YouTube niet kunt gebruiken (zelfs als je ingelogd bent) voor tenminste 2-3 maanden vanaf dat IP apparaat. Spotube is niet verantwoordelijk als dit ooit gebeurt.",
|
||||
"download_warning": "Als je alle nummers in bulk downloadt, ben je duidelijk bezig met muziekpiraterij en breng je schade toe aan de creatieve muziekmaatschappij. Ik hoop dat je je hiervan bewust bent. Probeer altijd het harde werk van artiesten te respecteren en te steunen.",
|
||||
"download_ip_ban_warning": "BTW, je IP-adres kan worden geblokkeerd op YouTube als gevolg van buitensporige downloadverzoeken. IP-blokkering betekent dat je YouTube niet kunt gebruiken (zelfs als je ingelogd bent) voor tenminste 2-3 maanden vanaf dat IP-apparaat. Spotube is niet verantwoordelijk als dit ooit gebeurt.",
|
||||
"by_clicking_accept_terms": "Door op 'accepteren' te klikken ga je akkoord met de volgende voorwaarden:",
|
||||
"download_agreement_1": "Ik weet dat ik muziek illegaal verveel. Ik ben en crimineel.",
|
||||
"download_agreement_2": "Ik steun de kunstenaar waar ik kan en ik doe dit alleen omdat ik geen geld heb om hun kunst te kopen.",
|
||||
"download_agreement_1": "Ik weet dat ik muziek illegaal donload. Ik ben slecht.",
|
||||
"download_agreement_2": "Ik steun de artiest waar ik kan en ik doe dit alleen omdat ik geen geld heb om hun kunst te kopen.",
|
||||
"download_agreement_3": "Ik ben me er volledig van bewust dat mijn IP geblokkeerd kan worden op YouTube & ik houd Spotube of zijn eigenaars/contributeurs niet verantwoordelijk voor ongelukken die veroorzaakt worden door mijn huidige actie.",
|
||||
"decline": "Weigeren",
|
||||
"accept": "Accepteren",
|
||||
@ -247,45 +249,42 @@
|
||||
"custom_hours": "Aangepaste uren",
|
||||
"logs": "Logboeken",
|
||||
"developers": "Ontwikkelaars",
|
||||
"not_logged_in": "U bent niet aangemeld",
|
||||
"not_logged_in": "Je bent niet aangemeld",
|
||||
"search_mode": "Zoekmodus",
|
||||
"youtube_api_type": "API-type",
|
||||
"ok": "Oké",
|
||||
"failed_to_encrypt": "Versleuteling mislukt",
|
||||
"encryption_failed_warning": "Spotube gebruikt encryptie om je gegevens veilig op te slaan. Maar dat is niet gelukt. Dus zal het terugvallen op onveilige opslag.\nAls je linux gebruikt, zorg er dan voor dat je een geheim-dienst (gnome-keyring, kde-wallet, keepassxc etc) hebt geïnstalleerd.",
|
||||
"querying_info": "Info opvragen...",
|
||||
"encryption_failed_warning": "Spotube gebruikt versleuteling om je gegevens veilig op te slaan. Maar dat is niet gelukt. Dus zal het terugvallen op onveilige opslag.\nAls je linux gebruikt, zorg er dan voor dat je een geheim-dienst (gnome-keyring, kde-wallet, keepassxc etc) hebt geïnstalleerd.",
|
||||
"querying_info": "Info opvragen…",
|
||||
"piped_api_down": "Piped API is uit",
|
||||
"piped_down_error_instructions": "De Piped-instantie {pipedInstance} is momenteel uitgevallen\n\nVerander de instantie of verander het 'API-type' naar de officiële YouTube API.\n\nZorg ervoor dat u de app herstart na de wijziging",
|
||||
"you_are_offline": "U bent momenteel offline",
|
||||
"connection_restored": "Uw internetverbinding is hersteld",
|
||||
"you_are_offline": "Je bent momenteel offline",
|
||||
"connection_restored": "Je internetverbinding is hersteld",
|
||||
"use_system_title_bar": "Systeemtitelbalk gebruiken",
|
||||
"crunching_results": "Resultaten kraken...",
|
||||
"search_to_get_results": "Zoek om resultaten te krijgen",
|
||||
"crunching_results": "Resultaten verwerken…",
|
||||
"search_to_get_results": "Zoeken naar resultaten",
|
||||
"use_amoled_mode": "Pikzwart donkerthema",
|
||||
"pitch_dark_theme": "AMOLED-modus",
|
||||
"normalize_audio": "Audio normaliseren",
|
||||
"change_cover": "Dekking wijzigen",
|
||||
"add_cover": "Dekking toevoegen",
|
||||
"change_cover": "Hoes aanpassen",
|
||||
"add_cover": "Hoes toevoegen",
|
||||
"restore_defaults": "Standaardwaarden herstellen",
|
||||
"download_music_codec": "Muziek-codec downloaden",
|
||||
"streaming_music_codec": "Muziek-codec streamen",
|
||||
"login_with_lastfm": "Aanmelden met Last.fm",
|
||||
"download_music_codec": "Download-codec",
|
||||
"streaming_music_codec": "Streaming-codec",
|
||||
"login_with_lastfm": "Inloggen met Last.fm",
|
||||
"connect": "Verbinden",
|
||||
"disconnect_lastfm": "Last.fm verbreken",
|
||||
"disconnect": "Ontkoppelen",
|
||||
"disconnect": "Verbeken",
|
||||
"username": "Gebruikersnaam",
|
||||
"password": "Wachtwoord",
|
||||
"login": "Inloggen",
|
||||
"login_with_your_lastfm": "Inloggen met uw Last.fm account",
|
||||
"scrobble_to_lastfm": "Scrobbel naar Last.fm",
|
||||
"audio_source": "Audiobron",
|
||||
"login_with_your_lastfm": "Inloggen met je Last.fm account",
|
||||
"scrobble_to_lastfm": "Scrobbelen naar Last.fm",
|
||||
"go_to_album": "Ga naar album",
|
||||
"discord_rich_presence": "Discord Rich Presence",
|
||||
"browse_all": "Alles bekijken",
|
||||
"browse_all": "Alles doorbladeren",
|
||||
"genres": "Genres",
|
||||
"explore_genres": "Verken genres",
|
||||
"step_3_steps": "Kopieer de waarde van de \"sp_dc\"-cookie",
|
||||
"step_4_steps": "Plak de gekopieerde waarde van \"sp_dc\"",
|
||||
"explore_genres": "Genres verkennen",
|
||||
"friends": "Vrienden",
|
||||
"no_lyrics_available": "Sorry, kan geen teksten vinden voor deze track"
|
||||
}
|
||||
"no_lyrics_available": "Sorry, geen teksten gevonden voor dit nummer"
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
/// yuri-val@github => Ukrainian
|
||||
/// energywave@github, ncvescera@github, OpenCode@github => Italian
|
||||
/// mdksec@github => Turkish
|
||||
/// SecularSteve@github => Dutch
|
||||
/// Stephan-P@github, SecularSteve@github => Dutch
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class L10n {
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
|
||||
@ -27,7 +27,7 @@ class LibraryPage extends HookConsumerWidget {
|
||||
leading: ThemedButtonsTabBar(
|
||||
tabs: [
|
||||
Tab(text: " ${context.l10n.playlists} "),
|
||||
Tab(text: " ${context.l10n.tracks} "),
|
||||
Tab(text: " ${context.l10n.local_tracks} "),
|
||||
Tab(
|
||||
child: Badge(
|
||||
isLabelVisible: downloadingCount > 0,
|
||||
|
||||
@ -2,8 +2,11 @@ import 'package:flutter/material.dart' hide Page;
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/shared/dialogs/prompt_dialog.dart';
|
||||
import 'package:spotube/components/shared/tracks_view/sections/body/use_is_user_playlist.dart';
|
||||
import 'package:spotube/components/shared/tracks_view/track_view.dart';
|
||||
import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/extensions/infinite_query.dart';
|
||||
import 'package:spotube/provider/spotify_provider.dart';
|
||||
import 'package:spotube/services/mutations/mutations.dart';
|
||||
@ -45,6 +48,8 @@ class PlaylistPage extends HookConsumerWidget {
|
||||
],
|
||||
);
|
||||
|
||||
final isUserPlaylist = useIsUserPlaylist(ref, playlist.id!);
|
||||
|
||||
return InheritedTrackView(
|
||||
collectionId: playlist.id!,
|
||||
image: TypeConversionUtils.image_X_UrlString(
|
||||
@ -72,9 +77,20 @@ class PlaylistPage extends HookConsumerWidget {
|
||||
shareUrl: playlist.externalUrls?.spotify ?? "",
|
||||
onHeart: () async {
|
||||
if (!isLikedQuery.hasData || togglePlaylistLike.isMutating) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
await togglePlaylistLike.mutate(isLikedQuery.data!);
|
||||
final confirmed = isUserPlaylist
|
||||
? await showPromptDialog(
|
||||
context: context,
|
||||
title: context.l10n.delete_playlist,
|
||||
message: context.l10n.delete_playlist_confirmation,
|
||||
)
|
||||
: true;
|
||||
if (confirmed) {
|
||||
await togglePlaylistLike.mutate(isLikedQuery.data!);
|
||||
return isUserPlaylist;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
child: const TrackView(),
|
||||
);
|
||||
|
||||
@ -15,6 +15,7 @@ import 'package:spotube/components/root/bottom_player.dart';
|
||||
import 'package:spotube/components/root/sidebar.dart';
|
||||
import 'package:spotube/components/root/spotube_navigation_bar.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/hooks/configurators/use_endless_playback.dart';
|
||||
import 'package:spotube/hooks/configurators/use_update_checker.dart';
|
||||
import 'package:spotube/provider/download_manager_provider.dart';
|
||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
||||
@ -134,6 +135,8 @@ class RootApp extends HookConsumerWidget {
|
||||
// checks for latest version of the application
|
||||
useUpdateChecker(ref);
|
||||
|
||||
useEndlessPlayback(ref);
|
||||
|
||||
final backgroundColor = Theme.of(context).scaffoldBackgroundColor;
|
||||
|
||||
useEffect(() {
|
||||
@ -159,38 +162,47 @@ class RootApp extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: Sidebar(
|
||||
selectedIndex: rootPaths[location],
|
||||
onSelectedIndexChanged: onSelectIndexChanged,
|
||||
child: child,
|
||||
),
|
||||
extendBody: true,
|
||||
drawerScrimColor: Colors.transparent,
|
||||
endDrawer: DesktopTools.platform.isDesktop
|
||||
? Container(
|
||||
constraints: const BoxConstraints(maxWidth: 800),
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: theme.brightness == Brightness.light
|
||||
? null
|
||||
: kElevationToShadow[8],
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
top: 40,
|
||||
bottom: 100,
|
||||
),
|
||||
child: const PlayerQueue(floating: true),
|
||||
)
|
||||
: null,
|
||||
bottomNavigationBar: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
BottomPlayer(),
|
||||
SpotubeNavigationBar(
|
||||
selectedIndex: rootPaths[location],
|
||||
onSelectedIndexChanged: onSelectIndexChanged,
|
||||
),
|
||||
],
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (rootPaths[location] != 0) {
|
||||
onSelectIndexChanged(0);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
child: Scaffold(
|
||||
body: Sidebar(
|
||||
selectedIndex: rootPaths[location],
|
||||
onSelectedIndexChanged: onSelectIndexChanged,
|
||||
child: child,
|
||||
),
|
||||
extendBody: true,
|
||||
drawerScrimColor: Colors.transparent,
|
||||
endDrawer: DesktopTools.platform.isDesktop
|
||||
? Container(
|
||||
constraints: const BoxConstraints(maxWidth: 800),
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: theme.brightness == Brightness.light
|
||||
? null
|
||||
: kElevationToShadow[8],
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
top: 40,
|
||||
bottom: 100,
|
||||
),
|
||||
child: const PlayerQueue(floating: true),
|
||||
)
|
||||
: null,
|
||||
bottomNavigationBar: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
BottomPlayer(),
|
||||
SpotubeNavigationBar(
|
||||
selectedIndex: rootPaths[location],
|
||||
onSelectedIndexChanged: onSelectIndexChanged,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -221,6 +221,12 @@ class SettingsPlaybackSection extends HookConsumerWidget {
|
||||
preferencesNotifier.setDownloadMusicCodec(value);
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(SpotubeIcons.repeat),
|
||||
title: Text(context.l10n.endless_playback),
|
||||
value: preferences.endlessPlayback,
|
||||
onChanged: preferencesNotifier.setEndlessPlayback,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -144,8 +144,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
// Removing tracks that were not found to avoid queue interruption
|
||||
// TODO: Add a flag to enable/disable skip not found tracks
|
||||
if (e is TrackNotFoundException) {
|
||||
if (e is TrackNotFoundError) {
|
||||
final oldTrack =
|
||||
mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull;
|
||||
await removeTrack(oldTrack!.id!);
|
||||
|
||||
@ -123,6 +123,10 @@ class UserPreferencesNotifier extends PersistedStateNotifier<UserPreferences> {
|
||||
audioPlayer.setAudioNormalization(normalize);
|
||||
}
|
||||
|
||||
void setEndlessPlayback(bool endless) {
|
||||
state = state.copyWith(endlessPlayback: endless);
|
||||
}
|
||||
|
||||
Future<String> _getDefaultDownloadDirectory() async {
|
||||
if (kIsAndroid) return "/storage/emulated/0/Download/Spotube";
|
||||
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/settings/color_scheme_picker_dialog.dart';
|
||||
import 'package:spotube/services/sourced_track/enums.dart';
|
||||
|
||||
part 'user_preferences_state.g.dart';
|
||||
part 'user_preferences_state.freezed.dart';
|
||||
|
||||
@JsonEnum()
|
||||
enum LayoutMode {
|
||||
@ -53,40 +54,48 @@ enum SearchMode {
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
final class UserPreferences {
|
||||
@JsonKey(
|
||||
defaultValue: SourceQualities.high,
|
||||
unknownEnumValue: SourceQualities.high,
|
||||
)
|
||||
final SourceQualities audioQuality;
|
||||
@freezed
|
||||
class UserPreferences with _$UserPreferences {
|
||||
const factory UserPreferences({
|
||||
@Default(SourceQualities.high) SourceQualities audioQuality,
|
||||
@Default(true) bool albumColorSync,
|
||||
@Default(false) bool amoledDarkTheme,
|
||||
@Default(true) bool checkUpdate,
|
||||
@Default(false) bool normalizeAudio,
|
||||
@Default(true) bool showSystemTrayIcon,
|
||||
@Default(false) bool skipNonMusic,
|
||||
@Default(false) bool systemTitleBar,
|
||||
@Default(CloseBehavior.minimizeToTray) CloseBehavior closeBehavior,
|
||||
@Default(SpotubeColor(0xFF2196F3, name: "Blue"))
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._accentColorSchemeFromJson,
|
||||
toJson: UserPreferences._accentColorSchemeToJson,
|
||||
readValue: UserPreferences._accentColorSchemeReadValue,
|
||||
)
|
||||
SpotubeColor accentColorScheme,
|
||||
@Default(LayoutMode.adaptive) LayoutMode layoutMode,
|
||||
@Default(Locale("system", "system"))
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._localeFromJson,
|
||||
toJson: UserPreferences._localeToJson,
|
||||
readValue: UserPreferences._localeReadValue,
|
||||
)
|
||||
Locale locale,
|
||||
@Default(Market.US) Market recommendationMarket,
|
||||
@Default(SearchMode.youtube) SearchMode searchMode,
|
||||
@Default("") String downloadLocation,
|
||||
@Default("https://pipedapi.kavin.rocks") String pipedInstance,
|
||||
@Default(ThemeMode.system) ThemeMode themeMode,
|
||||
@Default(AudioSource.youtube) AudioSource audioSource,
|
||||
@Default(SourceCodecs.weba) SourceCodecs streamMusicCodec,
|
||||
@Default(SourceCodecs.m4a) SourceCodecs downloadMusicCodec,
|
||||
@Default(true) bool discordPresence,
|
||||
@Default(true) bool endlessPlayback,
|
||||
}) = _UserPreferences;
|
||||
factory UserPreferences.fromJson(Map<String, dynamic> json) =>
|
||||
_$UserPreferencesFromJson(json);
|
||||
|
||||
@JsonKey(defaultValue: true)
|
||||
final bool albumColorSync;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool amoledDarkTheme;
|
||||
|
||||
@JsonKey(defaultValue: true)
|
||||
final bool checkUpdate;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool normalizeAudio;
|
||||
|
||||
@JsonKey(defaultValue: true)
|
||||
final bool showSystemTrayIcon;
|
||||
|
||||
@JsonKey(defaultValue: true)
|
||||
final bool skipNonMusic;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool systemTitleBar;
|
||||
|
||||
@JsonKey(
|
||||
defaultValue: CloseBehavior.minimizeToTray,
|
||||
unknownEnumValue: CloseBehavior.minimizeToTray,
|
||||
)
|
||||
final CloseBehavior closeBehavior;
|
||||
factory UserPreferences.withDefaults() => UserPreferences.fromJson({});
|
||||
|
||||
static SpotubeColor _accentColorSchemeFromJson(Map<String, dynamic> json) {
|
||||
return SpotubeColor.fromString(json["color"]);
|
||||
@ -105,23 +114,6 @@ final class UserPreferences {
|
||||
return {"color": color.toString()};
|
||||
}
|
||||
|
||||
static SpotubeColor _defaultAccentColorScheme() =>
|
||||
const SpotubeColor(0xFF2196F3, name: "Blue");
|
||||
|
||||
@JsonKey(
|
||||
defaultValue: UserPreferences._defaultAccentColorScheme,
|
||||
fromJson: UserPreferences._accentColorSchemeFromJson,
|
||||
toJson: UserPreferences._accentColorSchemeToJson,
|
||||
readValue: UserPreferences._accentColorSchemeReadValue,
|
||||
)
|
||||
final SpotubeColor accentColorScheme;
|
||||
|
||||
@JsonKey(
|
||||
defaultValue: LayoutMode.adaptive,
|
||||
unknownEnumValue: LayoutMode.adaptive,
|
||||
)
|
||||
final LayoutMode layoutMode;
|
||||
|
||||
static Locale _localeFromJson(Map<String, dynamic> json) {
|
||||
return Locale(json["languageCode"], json["countryCode"]);
|
||||
}
|
||||
@ -145,144 +137,4 @@ final class UserPreferences {
|
||||
|
||||
return json[key] as Map<String, dynamic>?;
|
||||
}
|
||||
|
||||
static Locale _defaultLocaleValue() => const Locale("system", "system");
|
||||
|
||||
@JsonKey(
|
||||
defaultValue: UserPreferences._defaultLocaleValue,
|
||||
toJson: UserPreferences._localeToJson,
|
||||
fromJson: UserPreferences._localeFromJson,
|
||||
readValue: UserPreferences._localeReadValue,
|
||||
)
|
||||
final Locale locale;
|
||||
|
||||
@JsonKey(
|
||||
defaultValue: Market.US,
|
||||
unknownEnumValue: Market.US,
|
||||
)
|
||||
final Market recommendationMarket;
|
||||
|
||||
@JsonKey(
|
||||
defaultValue: SearchMode.youtube,
|
||||
unknownEnumValue: SearchMode.youtube,
|
||||
)
|
||||
final SearchMode searchMode;
|
||||
|
||||
@JsonKey(defaultValue: "")
|
||||
final String downloadLocation;
|
||||
|
||||
@JsonKey(defaultValue: "https://pipedapi.kavin.rocks")
|
||||
final String pipedInstance;
|
||||
|
||||
@JsonKey(
|
||||
defaultValue: ThemeMode.system,
|
||||
unknownEnumValue: ThemeMode.system,
|
||||
)
|
||||
final ThemeMode themeMode;
|
||||
|
||||
@JsonKey(
|
||||
defaultValue: AudioSource.youtube,
|
||||
unknownEnumValue: AudioSource.youtube,
|
||||
)
|
||||
final AudioSource audioSource;
|
||||
|
||||
@JsonKey(
|
||||
defaultValue: SourceCodecs.weba,
|
||||
unknownEnumValue: SourceCodecs.weba,
|
||||
)
|
||||
final SourceCodecs streamMusicCodec;
|
||||
|
||||
@JsonKey(
|
||||
defaultValue: SourceCodecs.m4a,
|
||||
unknownEnumValue: SourceCodecs.m4a,
|
||||
)
|
||||
final SourceCodecs downloadMusicCodec;
|
||||
|
||||
@JsonKey(defaultValue: true)
|
||||
final bool discordPresence;
|
||||
|
||||
UserPreferences({
|
||||
required this.audioQuality,
|
||||
required this.albumColorSync,
|
||||
required this.amoledDarkTheme,
|
||||
required this.checkUpdate,
|
||||
required this.normalizeAudio,
|
||||
required this.showSystemTrayIcon,
|
||||
required this.skipNonMusic,
|
||||
required this.systemTitleBar,
|
||||
required this.closeBehavior,
|
||||
required this.accentColorScheme,
|
||||
required this.layoutMode,
|
||||
required this.locale,
|
||||
required this.recommendationMarket,
|
||||
required this.searchMode,
|
||||
required this.downloadLocation,
|
||||
required this.pipedInstance,
|
||||
required this.themeMode,
|
||||
required this.audioSource,
|
||||
required this.streamMusicCodec,
|
||||
required this.downloadMusicCodec,
|
||||
required this.discordPresence,
|
||||
});
|
||||
|
||||
factory UserPreferences.withDefaults() {
|
||||
return UserPreferences.fromJson({});
|
||||
}
|
||||
|
||||
factory UserPreferences.fromJson(Map<String, dynamic> json) {
|
||||
return _$UserPreferencesFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$UserPreferencesToJson(this);
|
||||
}
|
||||
|
||||
UserPreferences copyWith({
|
||||
ThemeMode? themeMode,
|
||||
SpotubeColor? accentColorScheme,
|
||||
bool? albumColorSync,
|
||||
bool? checkUpdate,
|
||||
SourceQualities? audioQuality,
|
||||
String? downloadLocation,
|
||||
LayoutMode? layoutMode,
|
||||
CloseBehavior? closeBehavior,
|
||||
bool? showSystemTrayIcon,
|
||||
Locale? locale,
|
||||
String? pipedInstance,
|
||||
SearchMode? searchMode,
|
||||
bool? skipNonMusic,
|
||||
AudioSource? audioSource,
|
||||
Market? recommendationMarket,
|
||||
bool? saveTrackLyrics,
|
||||
bool? amoledDarkTheme,
|
||||
bool? normalizeAudio,
|
||||
SourceCodecs? downloadMusicCodec,
|
||||
SourceCodecs? streamMusicCodec,
|
||||
bool? systemTitleBar,
|
||||
bool? discordPresence,
|
||||
}) {
|
||||
return UserPreferences(
|
||||
themeMode: themeMode ?? this.themeMode,
|
||||
accentColorScheme: accentColorScheme ?? this.accentColorScheme,
|
||||
albumColorSync: albumColorSync ?? this.albumColorSync,
|
||||
checkUpdate: checkUpdate ?? this.checkUpdate,
|
||||
audioQuality: audioQuality ?? this.audioQuality,
|
||||
downloadLocation: downloadLocation ?? this.downloadLocation,
|
||||
layoutMode: layoutMode ?? this.layoutMode,
|
||||
closeBehavior: closeBehavior ?? this.closeBehavior,
|
||||
showSystemTrayIcon: showSystemTrayIcon ?? this.showSystemTrayIcon,
|
||||
locale: locale ?? this.locale,
|
||||
pipedInstance: pipedInstance ?? this.pipedInstance,
|
||||
searchMode: searchMode ?? this.searchMode,
|
||||
skipNonMusic: skipNonMusic ?? this.skipNonMusic,
|
||||
audioSource: audioSource ?? this.audioSource,
|
||||
recommendationMarket: recommendationMarket ?? this.recommendationMarket,
|
||||
amoledDarkTheme: amoledDarkTheme ?? this.amoledDarkTheme,
|
||||
downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec,
|
||||
normalizeAudio: normalizeAudio ?? this.normalizeAudio,
|
||||
streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec,
|
||||
systemTitleBar: systemTitleBar ?? this.systemTitleBar,
|
||||
discordPresence: discordPresence ?? this.discordPresence,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,697 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'user_preferences_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||
|
||||
UserPreferences _$UserPreferencesFromJson(Map<String, dynamic> json) {
|
||||
return _UserPreferences.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$UserPreferences {
|
||||
SourceQualities get audioQuality => throw _privateConstructorUsedError;
|
||||
bool get albumColorSync => throw _privateConstructorUsedError;
|
||||
bool get amoledDarkTheme => throw _privateConstructorUsedError;
|
||||
bool get checkUpdate => throw _privateConstructorUsedError;
|
||||
bool get normalizeAudio => throw _privateConstructorUsedError;
|
||||
bool get showSystemTrayIcon => throw _privateConstructorUsedError;
|
||||
bool get skipNonMusic => throw _privateConstructorUsedError;
|
||||
bool get systemTitleBar => throw _privateConstructorUsedError;
|
||||
CloseBehavior get closeBehavior => throw _privateConstructorUsedError;
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._accentColorSchemeFromJson,
|
||||
toJson: UserPreferences._accentColorSchemeToJson,
|
||||
readValue: UserPreferences._accentColorSchemeReadValue)
|
||||
SpotubeColor get accentColorScheme => throw _privateConstructorUsedError;
|
||||
LayoutMode get layoutMode => throw _privateConstructorUsedError;
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._localeFromJson,
|
||||
toJson: UserPreferences._localeToJson,
|
||||
readValue: UserPreferences._localeReadValue)
|
||||
Locale get locale => throw _privateConstructorUsedError;
|
||||
Market get recommendationMarket => throw _privateConstructorUsedError;
|
||||
SearchMode get searchMode => throw _privateConstructorUsedError;
|
||||
String get downloadLocation => throw _privateConstructorUsedError;
|
||||
String get pipedInstance => throw _privateConstructorUsedError;
|
||||
ThemeMode get themeMode => throw _privateConstructorUsedError;
|
||||
AudioSource get audioSource => throw _privateConstructorUsedError;
|
||||
SourceCodecs get streamMusicCodec => throw _privateConstructorUsedError;
|
||||
SourceCodecs get downloadMusicCodec => throw _privateConstructorUsedError;
|
||||
bool get discordPresence => throw _privateConstructorUsedError;
|
||||
bool get endlessPlayback => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$UserPreferencesCopyWith<UserPreferences> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $UserPreferencesCopyWith<$Res> {
|
||||
factory $UserPreferencesCopyWith(
|
||||
UserPreferences value, $Res Function(UserPreferences) then) =
|
||||
_$UserPreferencesCopyWithImpl<$Res, UserPreferences>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{SourceQualities audioQuality,
|
||||
bool albumColorSync,
|
||||
bool amoledDarkTheme,
|
||||
bool checkUpdate,
|
||||
bool normalizeAudio,
|
||||
bool showSystemTrayIcon,
|
||||
bool skipNonMusic,
|
||||
bool systemTitleBar,
|
||||
CloseBehavior closeBehavior,
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._accentColorSchemeFromJson,
|
||||
toJson: UserPreferences._accentColorSchemeToJson,
|
||||
readValue: UserPreferences._accentColorSchemeReadValue)
|
||||
SpotubeColor accentColorScheme,
|
||||
LayoutMode layoutMode,
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._localeFromJson,
|
||||
toJson: UserPreferences._localeToJson,
|
||||
readValue: UserPreferences._localeReadValue)
|
||||
Locale locale,
|
||||
Market recommendationMarket,
|
||||
SearchMode searchMode,
|
||||
String downloadLocation,
|
||||
String pipedInstance,
|
||||
ThemeMode themeMode,
|
||||
AudioSource audioSource,
|
||||
SourceCodecs streamMusicCodec,
|
||||
SourceCodecs downloadMusicCodec,
|
||||
bool discordPresence,
|
||||
bool endlessPlayback});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$UserPreferencesCopyWithImpl<$Res, $Val extends UserPreferences>
|
||||
implements $UserPreferencesCopyWith<$Res> {
|
||||
_$UserPreferencesCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? audioQuality = null,
|
||||
Object? albumColorSync = null,
|
||||
Object? amoledDarkTheme = null,
|
||||
Object? checkUpdate = null,
|
||||
Object? normalizeAudio = null,
|
||||
Object? showSystemTrayIcon = null,
|
||||
Object? skipNonMusic = null,
|
||||
Object? systemTitleBar = null,
|
||||
Object? closeBehavior = null,
|
||||
Object? accentColorScheme = null,
|
||||
Object? layoutMode = null,
|
||||
Object? locale = null,
|
||||
Object? recommendationMarket = null,
|
||||
Object? searchMode = null,
|
||||
Object? downloadLocation = null,
|
||||
Object? pipedInstance = null,
|
||||
Object? themeMode = null,
|
||||
Object? audioSource = null,
|
||||
Object? streamMusicCodec = null,
|
||||
Object? downloadMusicCodec = null,
|
||||
Object? discordPresence = null,
|
||||
Object? endlessPlayback = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
audioQuality: null == audioQuality
|
||||
? _value.audioQuality
|
||||
: audioQuality // ignore: cast_nullable_to_non_nullable
|
||||
as SourceQualities,
|
||||
albumColorSync: null == albumColorSync
|
||||
? _value.albumColorSync
|
||||
: albumColorSync // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
amoledDarkTheme: null == amoledDarkTheme
|
||||
? _value.amoledDarkTheme
|
||||
: amoledDarkTheme // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
checkUpdate: null == checkUpdate
|
||||
? _value.checkUpdate
|
||||
: checkUpdate // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
normalizeAudio: null == normalizeAudio
|
||||
? _value.normalizeAudio
|
||||
: normalizeAudio // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
showSystemTrayIcon: null == showSystemTrayIcon
|
||||
? _value.showSystemTrayIcon
|
||||
: showSystemTrayIcon // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
skipNonMusic: null == skipNonMusic
|
||||
? _value.skipNonMusic
|
||||
: skipNonMusic // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
systemTitleBar: null == systemTitleBar
|
||||
? _value.systemTitleBar
|
||||
: systemTitleBar // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
closeBehavior: null == closeBehavior
|
||||
? _value.closeBehavior
|
||||
: closeBehavior // ignore: cast_nullable_to_non_nullable
|
||||
as CloseBehavior,
|
||||
accentColorScheme: null == accentColorScheme
|
||||
? _value.accentColorScheme
|
||||
: accentColorScheme // ignore: cast_nullable_to_non_nullable
|
||||
as SpotubeColor,
|
||||
layoutMode: null == layoutMode
|
||||
? _value.layoutMode
|
||||
: layoutMode // ignore: cast_nullable_to_non_nullable
|
||||
as LayoutMode,
|
||||
locale: null == locale
|
||||
? _value.locale
|
||||
: locale // ignore: cast_nullable_to_non_nullable
|
||||
as Locale,
|
||||
recommendationMarket: null == recommendationMarket
|
||||
? _value.recommendationMarket
|
||||
: recommendationMarket // ignore: cast_nullable_to_non_nullable
|
||||
as Market,
|
||||
searchMode: null == searchMode
|
||||
? _value.searchMode
|
||||
: searchMode // ignore: cast_nullable_to_non_nullable
|
||||
as SearchMode,
|
||||
downloadLocation: null == downloadLocation
|
||||
? _value.downloadLocation
|
||||
: downloadLocation // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
pipedInstance: null == pipedInstance
|
||||
? _value.pipedInstance
|
||||
: pipedInstance // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
themeMode: null == themeMode
|
||||
? _value.themeMode
|
||||
: themeMode // ignore: cast_nullable_to_non_nullable
|
||||
as ThemeMode,
|
||||
audioSource: null == audioSource
|
||||
? _value.audioSource
|
||||
: audioSource // ignore: cast_nullable_to_non_nullable
|
||||
as AudioSource,
|
||||
streamMusicCodec: null == streamMusicCodec
|
||||
? _value.streamMusicCodec
|
||||
: streamMusicCodec // ignore: cast_nullable_to_non_nullable
|
||||
as SourceCodecs,
|
||||
downloadMusicCodec: null == downloadMusicCodec
|
||||
? _value.downloadMusicCodec
|
||||
: downloadMusicCodec // ignore: cast_nullable_to_non_nullable
|
||||
as SourceCodecs,
|
||||
discordPresence: null == discordPresence
|
||||
? _value.discordPresence
|
||||
: discordPresence // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
endlessPlayback: null == endlessPlayback
|
||||
? _value.endlessPlayback
|
||||
: endlessPlayback // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$UserPreferencesImplCopyWith<$Res>
|
||||
implements $UserPreferencesCopyWith<$Res> {
|
||||
factory _$$UserPreferencesImplCopyWith(_$UserPreferencesImpl value,
|
||||
$Res Function(_$UserPreferencesImpl) then) =
|
||||
__$$UserPreferencesImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{SourceQualities audioQuality,
|
||||
bool albumColorSync,
|
||||
bool amoledDarkTheme,
|
||||
bool checkUpdate,
|
||||
bool normalizeAudio,
|
||||
bool showSystemTrayIcon,
|
||||
bool skipNonMusic,
|
||||
bool systemTitleBar,
|
||||
CloseBehavior closeBehavior,
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._accentColorSchemeFromJson,
|
||||
toJson: UserPreferences._accentColorSchemeToJson,
|
||||
readValue: UserPreferences._accentColorSchemeReadValue)
|
||||
SpotubeColor accentColorScheme,
|
||||
LayoutMode layoutMode,
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._localeFromJson,
|
||||
toJson: UserPreferences._localeToJson,
|
||||
readValue: UserPreferences._localeReadValue)
|
||||
Locale locale,
|
||||
Market recommendationMarket,
|
||||
SearchMode searchMode,
|
||||
String downloadLocation,
|
||||
String pipedInstance,
|
||||
ThemeMode themeMode,
|
||||
AudioSource audioSource,
|
||||
SourceCodecs streamMusicCodec,
|
||||
SourceCodecs downloadMusicCodec,
|
||||
bool discordPresence,
|
||||
bool endlessPlayback});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$UserPreferencesImplCopyWithImpl<$Res>
|
||||
extends _$UserPreferencesCopyWithImpl<$Res, _$UserPreferencesImpl>
|
||||
implements _$$UserPreferencesImplCopyWith<$Res> {
|
||||
__$$UserPreferencesImplCopyWithImpl(
|
||||
_$UserPreferencesImpl _value, $Res Function(_$UserPreferencesImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? audioQuality = null,
|
||||
Object? albumColorSync = null,
|
||||
Object? amoledDarkTheme = null,
|
||||
Object? checkUpdate = null,
|
||||
Object? normalizeAudio = null,
|
||||
Object? showSystemTrayIcon = null,
|
||||
Object? skipNonMusic = null,
|
||||
Object? systemTitleBar = null,
|
||||
Object? closeBehavior = null,
|
||||
Object? accentColorScheme = null,
|
||||
Object? layoutMode = null,
|
||||
Object? locale = null,
|
||||
Object? recommendationMarket = null,
|
||||
Object? searchMode = null,
|
||||
Object? downloadLocation = null,
|
||||
Object? pipedInstance = null,
|
||||
Object? themeMode = null,
|
||||
Object? audioSource = null,
|
||||
Object? streamMusicCodec = null,
|
||||
Object? downloadMusicCodec = null,
|
||||
Object? discordPresence = null,
|
||||
Object? endlessPlayback = null,
|
||||
}) {
|
||||
return _then(_$UserPreferencesImpl(
|
||||
audioQuality: null == audioQuality
|
||||
? _value.audioQuality
|
||||
: audioQuality // ignore: cast_nullable_to_non_nullable
|
||||
as SourceQualities,
|
||||
albumColorSync: null == albumColorSync
|
||||
? _value.albumColorSync
|
||||
: albumColorSync // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
amoledDarkTheme: null == amoledDarkTheme
|
||||
? _value.amoledDarkTheme
|
||||
: amoledDarkTheme // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
checkUpdate: null == checkUpdate
|
||||
? _value.checkUpdate
|
||||
: checkUpdate // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
normalizeAudio: null == normalizeAudio
|
||||
? _value.normalizeAudio
|
||||
: normalizeAudio // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
showSystemTrayIcon: null == showSystemTrayIcon
|
||||
? _value.showSystemTrayIcon
|
||||
: showSystemTrayIcon // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
skipNonMusic: null == skipNonMusic
|
||||
? _value.skipNonMusic
|
||||
: skipNonMusic // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
systemTitleBar: null == systemTitleBar
|
||||
? _value.systemTitleBar
|
||||
: systemTitleBar // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
closeBehavior: null == closeBehavior
|
||||
? _value.closeBehavior
|
||||
: closeBehavior // ignore: cast_nullable_to_non_nullable
|
||||
as CloseBehavior,
|
||||
accentColorScheme: null == accentColorScheme
|
||||
? _value.accentColorScheme
|
||||
: accentColorScheme // ignore: cast_nullable_to_non_nullable
|
||||
as SpotubeColor,
|
||||
layoutMode: null == layoutMode
|
||||
? _value.layoutMode
|
||||
: layoutMode // ignore: cast_nullable_to_non_nullable
|
||||
as LayoutMode,
|
||||
locale: null == locale
|
||||
? _value.locale
|
||||
: locale // ignore: cast_nullable_to_non_nullable
|
||||
as Locale,
|
||||
recommendationMarket: null == recommendationMarket
|
||||
? _value.recommendationMarket
|
||||
: recommendationMarket // ignore: cast_nullable_to_non_nullable
|
||||
as Market,
|
||||
searchMode: null == searchMode
|
||||
? _value.searchMode
|
||||
: searchMode // ignore: cast_nullable_to_non_nullable
|
||||
as SearchMode,
|
||||
downloadLocation: null == downloadLocation
|
||||
? _value.downloadLocation
|
||||
: downloadLocation // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
pipedInstance: null == pipedInstance
|
||||
? _value.pipedInstance
|
||||
: pipedInstance // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
themeMode: null == themeMode
|
||||
? _value.themeMode
|
||||
: themeMode // ignore: cast_nullable_to_non_nullable
|
||||
as ThemeMode,
|
||||
audioSource: null == audioSource
|
||||
? _value.audioSource
|
||||
: audioSource // ignore: cast_nullable_to_non_nullable
|
||||
as AudioSource,
|
||||
streamMusicCodec: null == streamMusicCodec
|
||||
? _value.streamMusicCodec
|
||||
: streamMusicCodec // ignore: cast_nullable_to_non_nullable
|
||||
as SourceCodecs,
|
||||
downloadMusicCodec: null == downloadMusicCodec
|
||||
? _value.downloadMusicCodec
|
||||
: downloadMusicCodec // ignore: cast_nullable_to_non_nullable
|
||||
as SourceCodecs,
|
||||
discordPresence: null == discordPresence
|
||||
? _value.discordPresence
|
||||
: discordPresence // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
endlessPlayback: null == endlessPlayback
|
||||
? _value.endlessPlayback
|
||||
: endlessPlayback // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$UserPreferencesImpl implements _UserPreferences {
|
||||
const _$UserPreferencesImpl(
|
||||
{this.audioQuality = SourceQualities.high,
|
||||
this.albumColorSync = true,
|
||||
this.amoledDarkTheme = false,
|
||||
this.checkUpdate = true,
|
||||
this.normalizeAudio = false,
|
||||
this.showSystemTrayIcon = true,
|
||||
this.skipNonMusic = false,
|
||||
this.systemTitleBar = false,
|
||||
this.closeBehavior = CloseBehavior.minimizeToTray,
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._accentColorSchemeFromJson,
|
||||
toJson: UserPreferences._accentColorSchemeToJson,
|
||||
readValue: UserPreferences._accentColorSchemeReadValue)
|
||||
this.accentColorScheme = const SpotubeColor(0xFF2196F3, name: "Blue"),
|
||||
this.layoutMode = LayoutMode.adaptive,
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._localeFromJson,
|
||||
toJson: UserPreferences._localeToJson,
|
||||
readValue: UserPreferences._localeReadValue)
|
||||
this.locale = const Locale("system", "system"),
|
||||
this.recommendationMarket = Market.US,
|
||||
this.searchMode = SearchMode.youtube,
|
||||
this.downloadLocation = "",
|
||||
this.pipedInstance = "https://pipedapi.kavin.rocks",
|
||||
this.themeMode = ThemeMode.system,
|
||||
this.audioSource = AudioSource.youtube,
|
||||
this.streamMusicCodec = SourceCodecs.weba,
|
||||
this.downloadMusicCodec = SourceCodecs.m4a,
|
||||
this.discordPresence = true,
|
||||
this.endlessPlayback = true});
|
||||
|
||||
factory _$UserPreferencesImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$UserPreferencesImplFromJson(json);
|
||||
|
||||
@override
|
||||
@JsonKey()
|
||||
final SourceQualities audioQuality;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool albumColorSync;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool amoledDarkTheme;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool checkUpdate;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool normalizeAudio;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool showSystemTrayIcon;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool skipNonMusic;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool systemTitleBar;
|
||||
@override
|
||||
@JsonKey()
|
||||
final CloseBehavior closeBehavior;
|
||||
@override
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._accentColorSchemeFromJson,
|
||||
toJson: UserPreferences._accentColorSchemeToJson,
|
||||
readValue: UserPreferences._accentColorSchemeReadValue)
|
||||
final SpotubeColor accentColorScheme;
|
||||
@override
|
||||
@JsonKey()
|
||||
final LayoutMode layoutMode;
|
||||
@override
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._localeFromJson,
|
||||
toJson: UserPreferences._localeToJson,
|
||||
readValue: UserPreferences._localeReadValue)
|
||||
final Locale locale;
|
||||
@override
|
||||
@JsonKey()
|
||||
final Market recommendationMarket;
|
||||
@override
|
||||
@JsonKey()
|
||||
final SearchMode searchMode;
|
||||
@override
|
||||
@JsonKey()
|
||||
final String downloadLocation;
|
||||
@override
|
||||
@JsonKey()
|
||||
final String pipedInstance;
|
||||
@override
|
||||
@JsonKey()
|
||||
final ThemeMode themeMode;
|
||||
@override
|
||||
@JsonKey()
|
||||
final AudioSource audioSource;
|
||||
@override
|
||||
@JsonKey()
|
||||
final SourceCodecs streamMusicCodec;
|
||||
@override
|
||||
@JsonKey()
|
||||
final SourceCodecs downloadMusicCodec;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool discordPresence;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool endlessPlayback;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UserPreferences(audioQuality: $audioQuality, albumColorSync: $albumColorSync, amoledDarkTheme: $amoledDarkTheme, checkUpdate: $checkUpdate, normalizeAudio: $normalizeAudio, showSystemTrayIcon: $showSystemTrayIcon, skipNonMusic: $skipNonMusic, systemTitleBar: $systemTitleBar, closeBehavior: $closeBehavior, accentColorScheme: $accentColorScheme, layoutMode: $layoutMode, locale: $locale, recommendationMarket: $recommendationMarket, searchMode: $searchMode, downloadLocation: $downloadLocation, pipedInstance: $pipedInstance, themeMode: $themeMode, audioSource: $audioSource, streamMusicCodec: $streamMusicCodec, downloadMusicCodec: $downloadMusicCodec, discordPresence: $discordPresence, endlessPlayback: $endlessPlayback)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$UserPreferencesImpl &&
|
||||
(identical(other.audioQuality, audioQuality) ||
|
||||
other.audioQuality == audioQuality) &&
|
||||
(identical(other.albumColorSync, albumColorSync) ||
|
||||
other.albumColorSync == albumColorSync) &&
|
||||
(identical(other.amoledDarkTheme, amoledDarkTheme) ||
|
||||
other.amoledDarkTheme == amoledDarkTheme) &&
|
||||
(identical(other.checkUpdate, checkUpdate) ||
|
||||
other.checkUpdate == checkUpdate) &&
|
||||
(identical(other.normalizeAudio, normalizeAudio) ||
|
||||
other.normalizeAudio == normalizeAudio) &&
|
||||
(identical(other.showSystemTrayIcon, showSystemTrayIcon) ||
|
||||
other.showSystemTrayIcon == showSystemTrayIcon) &&
|
||||
(identical(other.skipNonMusic, skipNonMusic) ||
|
||||
other.skipNonMusic == skipNonMusic) &&
|
||||
(identical(other.systemTitleBar, systemTitleBar) ||
|
||||
other.systemTitleBar == systemTitleBar) &&
|
||||
(identical(other.closeBehavior, closeBehavior) ||
|
||||
other.closeBehavior == closeBehavior) &&
|
||||
(identical(other.accentColorScheme, accentColorScheme) ||
|
||||
other.accentColorScheme == accentColorScheme) &&
|
||||
(identical(other.layoutMode, layoutMode) ||
|
||||
other.layoutMode == layoutMode) &&
|
||||
(identical(other.locale, locale) || other.locale == locale) &&
|
||||
(identical(other.recommendationMarket, recommendationMarket) ||
|
||||
other.recommendationMarket == recommendationMarket) &&
|
||||
(identical(other.searchMode, searchMode) ||
|
||||
other.searchMode == searchMode) &&
|
||||
(identical(other.downloadLocation, downloadLocation) ||
|
||||
other.downloadLocation == downloadLocation) &&
|
||||
(identical(other.pipedInstance, pipedInstance) ||
|
||||
other.pipedInstance == pipedInstance) &&
|
||||
(identical(other.themeMode, themeMode) ||
|
||||
other.themeMode == themeMode) &&
|
||||
(identical(other.audioSource, audioSource) ||
|
||||
other.audioSource == audioSource) &&
|
||||
(identical(other.streamMusicCodec, streamMusicCodec) ||
|
||||
other.streamMusicCodec == streamMusicCodec) &&
|
||||
(identical(other.downloadMusicCodec, downloadMusicCodec) ||
|
||||
other.downloadMusicCodec == downloadMusicCodec) &&
|
||||
(identical(other.discordPresence, discordPresence) ||
|
||||
other.discordPresence == discordPresence) &&
|
||||
(identical(other.endlessPlayback, endlessPlayback) ||
|
||||
other.endlessPlayback == endlessPlayback));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hashAll([
|
||||
runtimeType,
|
||||
audioQuality,
|
||||
albumColorSync,
|
||||
amoledDarkTheme,
|
||||
checkUpdate,
|
||||
normalizeAudio,
|
||||
showSystemTrayIcon,
|
||||
skipNonMusic,
|
||||
systemTitleBar,
|
||||
closeBehavior,
|
||||
accentColorScheme,
|
||||
layoutMode,
|
||||
locale,
|
||||
recommendationMarket,
|
||||
searchMode,
|
||||
downloadLocation,
|
||||
pipedInstance,
|
||||
themeMode,
|
||||
audioSource,
|
||||
streamMusicCodec,
|
||||
downloadMusicCodec,
|
||||
discordPresence,
|
||||
endlessPlayback
|
||||
]);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$UserPreferencesImplCopyWith<_$UserPreferencesImpl> get copyWith =>
|
||||
__$$UserPreferencesImplCopyWithImpl<_$UserPreferencesImpl>(
|
||||
this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$UserPreferencesImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _UserPreferences implements UserPreferences {
|
||||
const factory _UserPreferences(
|
||||
{final SourceQualities audioQuality,
|
||||
final bool albumColorSync,
|
||||
final bool amoledDarkTheme,
|
||||
final bool checkUpdate,
|
||||
final bool normalizeAudio,
|
||||
final bool showSystemTrayIcon,
|
||||
final bool skipNonMusic,
|
||||
final bool systemTitleBar,
|
||||
final CloseBehavior closeBehavior,
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._accentColorSchemeFromJson,
|
||||
toJson: UserPreferences._accentColorSchemeToJson,
|
||||
readValue: UserPreferences._accentColorSchemeReadValue)
|
||||
final SpotubeColor accentColorScheme,
|
||||
final LayoutMode layoutMode,
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._localeFromJson,
|
||||
toJson: UserPreferences._localeToJson,
|
||||
readValue: UserPreferences._localeReadValue)
|
||||
final Locale locale,
|
||||
final Market recommendationMarket,
|
||||
final SearchMode searchMode,
|
||||
final String downloadLocation,
|
||||
final String pipedInstance,
|
||||
final ThemeMode themeMode,
|
||||
final AudioSource audioSource,
|
||||
final SourceCodecs streamMusicCodec,
|
||||
final SourceCodecs downloadMusicCodec,
|
||||
final bool discordPresence,
|
||||
final bool endlessPlayback}) = _$UserPreferencesImpl;
|
||||
|
||||
factory _UserPreferences.fromJson(Map<String, dynamic> json) =
|
||||
_$UserPreferencesImpl.fromJson;
|
||||
|
||||
@override
|
||||
SourceQualities get audioQuality;
|
||||
@override
|
||||
bool get albumColorSync;
|
||||
@override
|
||||
bool get amoledDarkTheme;
|
||||
@override
|
||||
bool get checkUpdate;
|
||||
@override
|
||||
bool get normalizeAudio;
|
||||
@override
|
||||
bool get showSystemTrayIcon;
|
||||
@override
|
||||
bool get skipNonMusic;
|
||||
@override
|
||||
bool get systemTitleBar;
|
||||
@override
|
||||
CloseBehavior get closeBehavior;
|
||||
@override
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._accentColorSchemeFromJson,
|
||||
toJson: UserPreferences._accentColorSchemeToJson,
|
||||
readValue: UserPreferences._accentColorSchemeReadValue)
|
||||
SpotubeColor get accentColorScheme;
|
||||
@override
|
||||
LayoutMode get layoutMode;
|
||||
@override
|
||||
@JsonKey(
|
||||
fromJson: UserPreferences._localeFromJson,
|
||||
toJson: UserPreferences._localeToJson,
|
||||
readValue: UserPreferences._localeReadValue)
|
||||
Locale get locale;
|
||||
@override
|
||||
Market get recommendationMarket;
|
||||
@override
|
||||
SearchMode get searchMode;
|
||||
@override
|
||||
String get downloadLocation;
|
||||
@override
|
||||
String get pipedInstance;
|
||||
@override
|
||||
ThemeMode get themeMode;
|
||||
@override
|
||||
AudioSource get audioSource;
|
||||
@override
|
||||
SourceCodecs get streamMusicCodec;
|
||||
@override
|
||||
SourceCodecs get downloadMusicCodec;
|
||||
@override
|
||||
bool get discordPresence;
|
||||
@override
|
||||
bool get endlessPlayback;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$UserPreferencesImplCopyWith<_$UserPreferencesImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
@ -6,67 +6,63 @@ part of 'user_preferences_state.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
UserPreferences _$UserPreferencesFromJson(Map<String, dynamic> json) =>
|
||||
UserPreferences(
|
||||
audioQuality: $enumDecodeNullable(
|
||||
_$SourceQualitiesEnumMap, json['audioQuality'],
|
||||
unknownValue: SourceQualities.high) ??
|
||||
SourceQualities.high,
|
||||
_$UserPreferencesImpl _$$UserPreferencesImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$UserPreferencesImpl(
|
||||
audioQuality:
|
||||
$enumDecodeNullable(_$SourceQualitiesEnumMap, json['audioQuality']) ??
|
||||
SourceQualities.high,
|
||||
albumColorSync: json['albumColorSync'] as bool? ?? true,
|
||||
amoledDarkTheme: json['amoledDarkTheme'] as bool? ?? false,
|
||||
checkUpdate: json['checkUpdate'] as bool? ?? true,
|
||||
normalizeAudio: json['normalizeAudio'] as bool? ?? false,
|
||||
showSystemTrayIcon: json['showSystemTrayIcon'] as bool? ?? true,
|
||||
skipNonMusic: json['skipNonMusic'] as bool? ?? true,
|
||||
skipNonMusic: json['skipNonMusic'] as bool? ?? false,
|
||||
systemTitleBar: json['systemTitleBar'] as bool? ?? false,
|
||||
closeBehavior: $enumDecodeNullable(
|
||||
_$CloseBehaviorEnumMap, json['closeBehavior'],
|
||||
unknownValue: CloseBehavior.minimizeToTray) ??
|
||||
CloseBehavior.minimizeToTray,
|
||||
closeBehavior:
|
||||
$enumDecodeNullable(_$CloseBehaviorEnumMap, json['closeBehavior']) ??
|
||||
CloseBehavior.minimizeToTray,
|
||||
accentColorScheme: UserPreferences._accentColorSchemeReadValue(
|
||||
json, 'accentColorScheme') ==
|
||||
null
|
||||
? UserPreferences._defaultAccentColorScheme()
|
||||
? const SpotubeColor(0xFF2196F3, name: "Blue")
|
||||
: UserPreferences._accentColorSchemeFromJson(
|
||||
UserPreferences._accentColorSchemeReadValue(
|
||||
json, 'accentColorScheme') as Map<String, dynamic>),
|
||||
layoutMode: $enumDecodeNullable(_$LayoutModeEnumMap, json['layoutMode'],
|
||||
unknownValue: LayoutMode.adaptive) ??
|
||||
LayoutMode.adaptive,
|
||||
layoutMode:
|
||||
$enumDecodeNullable(_$LayoutModeEnumMap, json['layoutMode']) ??
|
||||
LayoutMode.adaptive,
|
||||
locale: UserPreferences._localeReadValue(json, 'locale') == null
|
||||
? UserPreferences._defaultLocaleValue()
|
||||
? const Locale("system", "system")
|
||||
: UserPreferences._localeFromJson(
|
||||
UserPreferences._localeReadValue(json, 'locale')
|
||||
as Map<String, dynamic>),
|
||||
recommendationMarket: $enumDecodeNullable(
|
||||
_$MarketEnumMap, json['recommendationMarket'],
|
||||
unknownValue: Market.US) ??
|
||||
Market.US,
|
||||
searchMode: $enumDecodeNullable(_$SearchModeEnumMap, json['searchMode'],
|
||||
unknownValue: SearchMode.youtube) ??
|
||||
SearchMode.youtube,
|
||||
downloadLocation: json['downloadLocation'] as String? ?? '',
|
||||
recommendationMarket:
|
||||
$enumDecodeNullable(_$MarketEnumMap, json['recommendationMarket']) ??
|
||||
Market.US,
|
||||
searchMode:
|
||||
$enumDecodeNullable(_$SearchModeEnumMap, json['searchMode']) ??
|
||||
SearchMode.youtube,
|
||||
downloadLocation: json['downloadLocation'] as String? ?? "",
|
||||
pipedInstance:
|
||||
json['pipedInstance'] as String? ?? 'https://pipedapi.kavin.rocks',
|
||||
themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode'],
|
||||
unknownValue: ThemeMode.system) ??
|
||||
json['pipedInstance'] as String? ?? "https://pipedapi.kavin.rocks",
|
||||
themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ??
|
||||
ThemeMode.system,
|
||||
audioSource: $enumDecodeNullable(
|
||||
_$AudioSourceEnumMap, json['audioSource'],
|
||||
unknownValue: AudioSource.youtube) ??
|
||||
AudioSource.youtube,
|
||||
audioSource:
|
||||
$enumDecodeNullable(_$AudioSourceEnumMap, json['audioSource']) ??
|
||||
AudioSource.youtube,
|
||||
streamMusicCodec: $enumDecodeNullable(
|
||||
_$SourceCodecsEnumMap, json['streamMusicCodec'],
|
||||
unknownValue: SourceCodecs.weba) ??
|
||||
_$SourceCodecsEnumMap, json['streamMusicCodec']) ??
|
||||
SourceCodecs.weba,
|
||||
downloadMusicCodec: $enumDecodeNullable(
|
||||
_$SourceCodecsEnumMap, json['downloadMusicCodec'],
|
||||
unknownValue: SourceCodecs.m4a) ??
|
||||
_$SourceCodecsEnumMap, json['downloadMusicCodec']) ??
|
||||
SourceCodecs.m4a,
|
||||
discordPresence: json['discordPresence'] as bool? ?? true,
|
||||
endlessPlayback: json['endlessPlayback'] as bool? ?? true,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$UserPreferencesToJson(UserPreferences instance) =>
|
||||
Map<String, dynamic> _$$UserPreferencesImplToJson(
|
||||
_$UserPreferencesImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'audioQuality': _$SourceQualitiesEnumMap[instance.audioQuality]!,
|
||||
'albumColorSync': instance.albumColorSync,
|
||||
@ -90,6 +86,7 @@ Map<String, dynamic> _$UserPreferencesToJson(UserPreferences instance) =>
|
||||
'streamMusicCodec': _$SourceCodecsEnumMap[instance.streamMusicCodec]!,
|
||||
'downloadMusicCodec': _$SourceCodecsEnumMap[instance.downloadMusicCodec]!,
|
||||
'discordPresence': instance.discordPresence,
|
||||
'endlessPlayback': instance.endlessPlayback,
|
||||
};
|
||||
|
||||
const _$SourceQualitiesEnumMap = {
|
||||
|
||||
@ -1,36 +1,60 @@
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/hooks/spotify/use_spotify_infinite_query.dart';
|
||||
import 'package:spotube/provider/spotify_provider.dart';
|
||||
|
||||
typedef SearchParams = ({
|
||||
SpotifyApi spotify,
|
||||
SearchType searchType,
|
||||
String query
|
||||
});
|
||||
|
||||
class SearchQueries {
|
||||
const SearchQueries();
|
||||
|
||||
static final queryJob =
|
||||
InfiniteQueryJob.withVariableKey<List<Page>, dynamic, int, SearchParams>(
|
||||
baseQueryKey: "search-query",
|
||||
task: (variableKey, page, args) => args!.spotify.search.get(
|
||||
args.query,
|
||||
types: [args.searchType],
|
||||
).getPage(10, page),
|
||||
initialPage: 0,
|
||||
nextPage: (lastPage, lastPageData) {
|
||||
if (lastPageData.isEmpty) return null;
|
||||
if ((lastPageData.first.isLast ||
|
||||
(lastPageData.first.items ?? []).length < 10)) {
|
||||
return null;
|
||||
}
|
||||
return lastPageData.first.nextOffset;
|
||||
},
|
||||
enabled: false,
|
||||
);
|
||||
|
||||
InfiniteQuery<List<Page>, dynamic, int> query(
|
||||
WidgetRef ref,
|
||||
String query,
|
||||
String queryStr,
|
||||
SearchType searchType,
|
||||
) {
|
||||
return useSpotifyInfiniteQuery<List<Page>, dynamic, int>(
|
||||
"search-query/${searchType.name}",
|
||||
(page, spotify) {
|
||||
if (query.trim().isEmpty) return [];
|
||||
final queryString = query;
|
||||
return spotify.search.get(
|
||||
queryString,
|
||||
types: [searchType],
|
||||
).getPage(10, page);
|
||||
},
|
||||
enabled: false,
|
||||
ref: ref,
|
||||
initialPage: 0,
|
||||
nextPage: (lastPage, lastPageData) {
|
||||
if (lastPageData.isEmpty) return null;
|
||||
if ((lastPageData.first.isLast ||
|
||||
(lastPageData.first.items ?? []).length < 10)) {
|
||||
return null;
|
||||
}
|
||||
return lastPageData.first.nextOffset;
|
||||
},
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
final query = useInfiniteQueryJob<List<Page>, dynamic, int, SearchParams>(
|
||||
job: queryJob(searchType.name),
|
||||
args: (spotify: spotify, searchType: searchType, query: queryStr),
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
return ref.listenManual(
|
||||
spotifyProvider,
|
||||
(previous, next) {
|
||||
if (previous != next) {
|
||||
query.refreshAll();
|
||||
}
|
||||
},
|
||||
).close;
|
||||
}, [query]);
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
import 'package:spotify/spotify.dart';
|
||||
|
||||
class TrackNotFoundException implements Exception {
|
||||
factory TrackNotFoundException(Track track) {
|
||||
throw Exception("Failed to find any results for ${track.name}");
|
||||
class TrackNotFoundError extends Error {
|
||||
final Track track;
|
||||
|
||||
TrackNotFoundError(this.track);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '[TrackNotFoundError] ${track.name} - ${track.artists?.map((e) => e.name).join(", ")}';
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +1,21 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
||||
import 'package:spotube/services/sourced_track/enums.dart';
|
||||
import 'package:spotube/services/sourced_track/exceptions.dart';
|
||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
||||
import 'package:spotube/services/sourced_track/models/source_map.dart';
|
||||
import 'package:spotube/services/sourced_track/sources/jiosaavn.dart';
|
||||
import 'package:spotube/services/sourced_track/sources/piped.dart';
|
||||
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
abstract class SourcedTrack extends Track {
|
||||
final SourceMap source;
|
||||
@ -101,9 +107,8 @@ abstract class SourcedTrack extends Track {
|
||||
required Track track,
|
||||
required Ref ref,
|
||||
}) async {
|
||||
final preferences = ref.read(userPreferencesProvider);
|
||||
try {
|
||||
final preferences = ref.read(userPreferencesProvider);
|
||||
|
||||
return switch (preferences.audioSource) {
|
||||
AudioSource.piped =>
|
||||
await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
@ -112,8 +117,35 @@ abstract class SourcedTrack extends Track {
|
||||
AudioSource.jiosaavn =>
|
||||
await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
};
|
||||
} on TrackNotFoundError catch (_) {
|
||||
return switch (preferences.audioSource) {
|
||||
AudioSource.piped ||
|
||||
AudioSource.youtube =>
|
||||
await JioSaavnSourcedTrack.fetchFromTrack(
|
||||
track: track,
|
||||
ref: ref,
|
||||
weakMatch: true,
|
||||
),
|
||||
AudioSource.jiosaavn =>
|
||||
await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
};
|
||||
} on HttpClientClosedException catch (_) {
|
||||
return await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref);
|
||||
} catch (e) {
|
||||
return YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref);
|
||||
if (e is DioException || e is ClientException || e is SocketException) {
|
||||
if (preferences.audioSource == AudioSource.jiosaavn) {
|
||||
return await JioSaavnSourcedTrack.fetchFromTrack(
|
||||
track: track,
|
||||
ref: ref,
|
||||
weakMatch: true,
|
||||
);
|
||||
}
|
||||
return await JioSaavnSourcedTrack.fetchFromTrack(
|
||||
track: track,
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -37,15 +37,17 @@ class JioSaavnSourcedTrack extends SourcedTrack {
|
||||
static Future<SourcedTrack> fetchFromTrack({
|
||||
required Track track,
|
||||
required Ref ref,
|
||||
bool weakMatch = false,
|
||||
}) async {
|
||||
final cachedSource = await SourceMatch.box.get(track.id);
|
||||
|
||||
if (cachedSource == null ||
|
||||
cachedSource.sourceType != SourceType.jiosaavn) {
|
||||
final siblings = await fetchSiblings(ref: ref, track: track);
|
||||
final siblings =
|
||||
await fetchSiblings(ref: ref, track: track, weakMatch: weakMatch);
|
||||
|
||||
if (siblings.isEmpty) {
|
||||
throw TrackNotFoundException(track);
|
||||
throw TrackNotFoundError(track);
|
||||
}
|
||||
|
||||
await SourceMatch.box.put(
|
||||
@ -119,6 +121,7 @@ class JioSaavnSourcedTrack extends SourcedTrack {
|
||||
static Future<List<SiblingType>> fetchSiblings({
|
||||
required Track track,
|
||||
required Ref ref,
|
||||
bool weakMatch = false,
|
||||
}) async {
|
||||
final query = SourcedTrack.getSearchTerm(track);
|
||||
|
||||
@ -126,9 +129,12 @@ class JioSaavnSourcedTrack extends SourcedTrack {
|
||||
await jiosaavnClient.search.songs(query, limit: 20);
|
||||
|
||||
final trackArtistNames = track.artists?.map((ar) => ar.name).toList();
|
||||
return results
|
||||
|
||||
final matchedResults = results
|
||||
.where(
|
||||
(s) {
|
||||
s.name?.unescapeHtml().contains(track.name!) ?? false;
|
||||
|
||||
final sameName = s.name?.unescapeHtml() == track.name;
|
||||
final artistNames = [
|
||||
s.primaryArtists,
|
||||
@ -139,12 +145,27 @@ class JioSaavnSourcedTrack extends SourcedTrack {
|
||||
(artist) =>
|
||||
trackArtistNames?.any((ar) => artist == ar) ?? false,
|
||||
);
|
||||
if (weakMatch) {
|
||||
final containsName =
|
||||
s.name?.unescapeHtml().contains(track.name!) ?? false;
|
||||
final containsPrimaryArtist = s.primaryArtists
|
||||
.unescapeHtml()
|
||||
.contains(trackArtistNames?.first ?? "");
|
||||
|
||||
return containsName && containsPrimaryArtist;
|
||||
}
|
||||
|
||||
return sameName && sameArtists;
|
||||
},
|
||||
)
|
||||
.map(toSiblingType)
|
||||
.toList();
|
||||
|
||||
if (weakMatch && matchedResults.isEmpty) {
|
||||
return results.map(toSiblingType).toList();
|
||||
}
|
||||
|
||||
return matchedResults;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -55,7 +55,7 @@ class PipedSourcedTrack extends SourcedTrack {
|
||||
if (cachedSource == null) {
|
||||
final siblings = await fetchSiblings(ref: ref, track: track);
|
||||
if (siblings.isEmpty) {
|
||||
throw TrackNotFoundException(track);
|
||||
throw TrackNotFoundError(track);
|
||||
}
|
||||
|
||||
await SourceMatch.box.put(
|
||||
@ -157,16 +157,20 @@ class PipedSourcedTrack extends SourcedTrack {
|
||||
}) async {
|
||||
final pipedClient = ref.read(pipedProvider);
|
||||
final preference = ref.read(userPreferencesProvider);
|
||||
|
||||
final query = SourcedTrack.getSearchTerm(track);
|
||||
|
||||
final PipedSearchResult(items: searchResults) = await pipedClient.search(
|
||||
"$query - Topic",
|
||||
query,
|
||||
preference.searchMode == SearchMode.youtube
|
||||
? PipedFilter.video
|
||||
? PipedFilter.videos
|
||||
: PipedFilter.musicSongs,
|
||||
);
|
||||
|
||||
final isYouTubeMusic = preference.searchMode == SearchMode.youtubeMusic;
|
||||
// when falling back to piped API make sure to use the YouTube mode
|
||||
final isYouTubeMusic = preference.audioSource != AudioSource.piped
|
||||
? false
|
||||
: preference.searchMode == SearchMode.youtubeMusic;
|
||||
|
||||
if (isYouTubeMusic) {
|
||||
final artists = (track.artists ?? [])
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/models/source_match.dart';
|
||||
import 'package:spotube/services/sourced_track/enums.dart';
|
||||
@ -48,7 +49,7 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
||||
if (cachedSource == null || cachedSource.sourceType != SourceType.youtube) {
|
||||
final siblings = await fetchSiblings(ref: ref, track: track);
|
||||
if (siblings.isEmpty) {
|
||||
throw TrackNotFoundException(track);
|
||||
throw TrackNotFoundError(track);
|
||||
}
|
||||
|
||||
await SourceMatch.box.put(
|
||||
@ -70,9 +71,14 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
||||
);
|
||||
}
|
||||
final item = await youtubeClient.videos.get(cachedSource.sourceId);
|
||||
final manifest = await youtubeClient.videos.streamsClient.getManifest(
|
||||
cachedSource.sourceId,
|
||||
);
|
||||
final manifest = await youtubeClient.videos.streamsClient
|
||||
.getManifest(
|
||||
cachedSource.sourceId,
|
||||
)
|
||||
.timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => throw ClientException("Timeout"),
|
||||
);
|
||||
return YoutubeSourcedTrack(
|
||||
ref: ref,
|
||||
siblings: [],
|
||||
@ -125,7 +131,10 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
||||
SourceMap? sourceMap;
|
||||
if (index == 0) {
|
||||
final manifest =
|
||||
await youtubeClient.videos.streamsClient.getManifest(item.id);
|
||||
await youtubeClient.videos.streamsClient.getManifest(item.id).timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => throw ClientException("Timeout"),
|
||||
);
|
||||
sourceMap = toSourceMap(manifest);
|
||||
}
|
||||
|
||||
@ -243,8 +252,12 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
||||
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
|
||||
..insert(0, sourceInfo);
|
||||
|
||||
final manifest =
|
||||
await youtubeClient.videos.streamsClient.getManifest(newSourceInfo.id);
|
||||
final manifest = await youtubeClient.videos.streamsClient
|
||||
.getManifest(newSourceInfo.id)
|
||||
.timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => throw ClientException("Timeout"),
|
||||
);
|
||||
|
||||
await SourceMatch.box.put(
|
||||
id!,
|
||||
|
||||
@ -53,9 +53,9 @@ abstract class ServiceUtils {
|
||||
|
||||
return "$title ${artists.map((e) => e.replaceAll(",", " ")).join(", ")}"
|
||||
.toLowerCase()
|
||||
.replaceAll(RegExp(" *\\[[^\\]]*]"), '')
|
||||
.replaceAll(RegExp("feat.|ft."), '')
|
||||
.replaceAll(RegExp("\\s+"), ' ')
|
||||
.replaceAll(RegExp(r"\s*\[[^\]]*]"), ' ')
|
||||
.replaceAll(RegExp(r"\sfeat\.|\sft\."), ' ')
|
||||
.replaceAll(RegExp(r"\s+"), ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
@ -292,24 +292,24 @@ abstract class ServiceUtils {
|
||||
return List<T>.from(tracks)
|
||||
..sort((a, b) {
|
||||
switch (sortBy) {
|
||||
case SortBy.album:
|
||||
return a.album?.name?.compareTo(b.album?.name ?? "") ?? 0;
|
||||
case SortBy.artist:
|
||||
return a.artists?.first.name
|
||||
?.compareTo(b.artists?.first.name ?? "") ??
|
||||
0;
|
||||
case SortBy.ascending:
|
||||
return a.name?.compareTo(b.name ?? "") ?? 0;
|
||||
case SortBy.oldest:
|
||||
final aDate = parseSpotifyAlbumDate(a.album);
|
||||
final bDate = parseSpotifyAlbumDate(b.album);
|
||||
return aDate.compareTo(bDate);
|
||||
case SortBy.descending:
|
||||
return b.name?.compareTo(a.name ?? "") ?? 0;
|
||||
case SortBy.newest:
|
||||
final aDate = parseSpotifyAlbumDate(a.album);
|
||||
final bDate = parseSpotifyAlbumDate(b.album);
|
||||
return bDate.compareTo(aDate);
|
||||
case SortBy.descending:
|
||||
return b.name?.compareTo(a.name ?? "") ?? 0;
|
||||
case SortBy.oldest:
|
||||
final aDate = parseSpotifyAlbumDate(a.album);
|
||||
final bDate = parseSpotifyAlbumDate(b.album);
|
||||
return aDate.compareTo(bDate);
|
||||
case SortBy.duration:
|
||||
return a.durationMs?.compareTo(b.durationMs ?? 0) ?? 0;
|
||||
case SortBy.artist:
|
||||
return a.artists?.first.name?.compareTo(b.artists?.first.name ?? "") ?? 0;
|
||||
case SortBy.album:
|
||||
return a.album?.name?.compareTo(b.album?.name ?? "") ?? 0;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -143,4 +143,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7
|
||||
|
||||
COCOAPODS: 1.14.3
|
||||
COCOAPODS: 1.15.2
|
||||
|
||||
25
pubspec.lock
@ -957,8 +957,16 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
freezed:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: freezed
|
||||
sha256: "6c5031daae12c7072b3a87eff98983076434b4889ef2a44384d0cae3f82372ba"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.6"
|
||||
freezed_annotation:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: freezed_annotation
|
||||
sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d
|
||||
@ -1006,10 +1014,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: "3b40e751eaaa855179b416974d59d29669e750d2e50fcdb2b37f1cb0ca8c803a"
|
||||
sha256: c5fa45fa502ee880839e3b2152d987c44abae26d064a2376d4aad434cf0f7b15
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "13.0.1"
|
||||
version: "12.1.3"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1610,11 +1618,12 @@ packages:
|
||||
piped_client:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: piped_client
|
||||
sha256: "8b96e1f9d8533c1da7eff7fbbd4bf188256fc76a20900d378b52be09418ea771"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: "64631732eefe3d93889756dc2e4ff5c8523ed763"
|
||||
url: "https://github.com/KRTirtho/piped_client.git"
|
||||
source: git
|
||||
version: "0.1.1"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@ -55,7 +55,7 @@ dependencies:
|
||||
flutter_svg: ^1.1.6
|
||||
form_validator: ^2.1.1
|
||||
fuzzywuzzy: ^1.1.6
|
||||
go_router: ^13.0.1
|
||||
go_router: 12.1.3 # Stuck on this https://github.com/flutter/flutter/issues/140869
|
||||
google_fonts: ^6.1.0
|
||||
hive: ^2.2.3
|
||||
hive_flutter: ^1.1.0
|
||||
@ -76,7 +76,9 @@ dependencies:
|
||||
path: ^1.8.0
|
||||
path_provider: ^2.0.8
|
||||
permission_handler: ^11.0.1
|
||||
piped_client: ^0.1.0
|
||||
piped_client:
|
||||
git:
|
||||
url: https://github.com/KRTirtho/piped_client.git
|
||||
popover: ^0.2.6+3
|
||||
scrobblenaut:
|
||||
git:
|
||||
@ -123,6 +125,7 @@ dependencies:
|
||||
win32_registry: ^1.1.2
|
||||
flutter_sharing_intent: ^1.1.0
|
||||
flutter_broadcasts: ^0.4.0
|
||||
freezed_annotation: ^2.4.1
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.3.2
|
||||
@ -139,6 +142,7 @@ dev_dependencies:
|
||||
json_serializable: ^6.6.2
|
||||
pub_api_client: ^2.4.0
|
||||
pubspec_parse: ^1.2.2
|
||||
freezed: ^2.4.6
|
||||
|
||||
dependency_overrides:
|
||||
http: ^1.1.0
|
||||
|
||||
@ -1 +1,200 @@
|
||||
{}
|
||||
{
|
||||
"ar": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks"
|
||||
],
|
||||
|
||||
"bn": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks"
|
||||
],
|
||||
|
||||
"ca": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks"
|
||||
],
|
||||
|
||||
"de": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks"
|
||||
],
|
||||
|
||||
"fa": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks"
|
||||
],
|
||||
|
||||
"hi": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks"
|
||||
],
|
||||
|
||||
"it": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks"
|
||||
],
|
||||
|
||||
"ja": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks"
|
||||
],
|
||||
|
||||
"ne": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks"
|
||||
],
|
||||
|
||||
"nl": [
|
||||
"sort_duration",
|
||||
"audio_source",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks"
|
||||
],
|
||||
|
||||
"pl": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks"
|
||||
],
|
||||
|
||||
"tr": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks"
|
||||
],
|
||||
|
||||
"uk": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
"sort_duration",
|
||||
"start_a_radio",
|
||||
"how_to_start_radio",
|
||||
"replace_queue_question",
|
||||
"endless_playback",
|
||||
"delete_playlist",
|
||||
"delete_playlist_confirmation",
|
||||
"local_tracks"
|
||||
]
|
||||
}
|
||||
|
||||
13
website/.eslintignore
Normal file
@ -0,0 +1,13 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
31
website/.eslintrc.cjs
Normal file
@ -0,0 +1,31 @@
|
||||
/** @type { import("eslint").Linter.Config } */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:svelte/recommended',
|
||||
'prettier'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
extraFileExtensions: ['.svelte']
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
parser: 'svelte-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
@ -1,6 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"prettier"
|
||||
]
|
||||
}
|
||||
44
website/.gitignore
vendored
Executable file → Normal file
@ -1,35 +1,11 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
.netlify
|
||||
|
||||
1
website/.node-version
Normal file
@ -0,0 +1 @@
|
||||
20.11.0
|
||||
1
website/.npmrc
Normal file
@ -0,0 +1 @@
|
||||
engine-strict=true
|
||||
4
website/.prettierignore
Normal file
@ -0,0 +1,4 @@
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
@ -1,4 +1,8 @@
|
||||
{
|
||||
"singleQuote": false,
|
||||
"useTabs": false
|
||||
}
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
|
||||
120
website/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,120 @@
|
||||
{
|
||||
"prettier.documentSelectors": [
|
||||
"**/*.svelte"
|
||||
],
|
||||
"tailwindCSS.classAttributes": [
|
||||
"class",
|
||||
"accent",
|
||||
"active",
|
||||
"animIndeterminate",
|
||||
"aspectRatio",
|
||||
"background",
|
||||
"badge",
|
||||
"bgBackdrop",
|
||||
"bgDark",
|
||||
"bgDrawer",
|
||||
"bgLight",
|
||||
"blur",
|
||||
"border",
|
||||
"button",
|
||||
"buttonAction",
|
||||
"buttonBack",
|
||||
"buttonClasses",
|
||||
"buttonComplete",
|
||||
"buttonDismiss",
|
||||
"buttonNeutral",
|
||||
"buttonNext",
|
||||
"buttonPositive",
|
||||
"buttonTextCancel",
|
||||
"buttonTextConfirm",
|
||||
"buttonTextFirst",
|
||||
"buttonTextLast",
|
||||
"buttonTextNext",
|
||||
"buttonTextPrevious",
|
||||
"buttonTextSubmit",
|
||||
"caretClosed",
|
||||
"caretOpen",
|
||||
"chips",
|
||||
"color",
|
||||
"controlSeparator",
|
||||
"controlVariant",
|
||||
"cursor",
|
||||
"display",
|
||||
"element",
|
||||
"fill",
|
||||
"fillDark",
|
||||
"fillLight",
|
||||
"flex",
|
||||
"flexDirection",
|
||||
"gap",
|
||||
"gridColumns",
|
||||
"height",
|
||||
"hover",
|
||||
"inactive",
|
||||
"indent",
|
||||
"justify",
|
||||
"meter",
|
||||
"padding",
|
||||
"position",
|
||||
"regionAnchor",
|
||||
"regionBackdrop",
|
||||
"regionBody",
|
||||
"regionCaption",
|
||||
"regionCaret",
|
||||
"regionCell",
|
||||
"regionChildren",
|
||||
"regionChipList",
|
||||
"regionChipWrapper",
|
||||
"regionCone",
|
||||
"regionContent",
|
||||
"regionControl",
|
||||
"regionDefault",
|
||||
"regionDrawer",
|
||||
"regionFoot",
|
||||
"regionFootCell",
|
||||
"regionFooter",
|
||||
"regionHead",
|
||||
"regionHeadCell",
|
||||
"regionHeader",
|
||||
"regionIcon",
|
||||
"regionInput",
|
||||
"regionInterface",
|
||||
"regionInterfaceText",
|
||||
"regionLabel",
|
||||
"regionLead",
|
||||
"regionLegend",
|
||||
"regionList",
|
||||
"regionListItem",
|
||||
"regionNavigation",
|
||||
"regionPage",
|
||||
"regionPanel",
|
||||
"regionRowHeadline",
|
||||
"regionRowMain",
|
||||
"regionSummary",
|
||||
"regionSymbol",
|
||||
"regionTab",
|
||||
"regionTrail",
|
||||
"ring",
|
||||
"rounded",
|
||||
"select",
|
||||
"shadow",
|
||||
"slotDefault",
|
||||
"slotFooter",
|
||||
"slotHeader",
|
||||
"slotLead",
|
||||
"slotMessage",
|
||||
"slotMeta",
|
||||
"slotPageContent",
|
||||
"slotPageFooter",
|
||||
"slotPageHeader",
|
||||
"slotSidebarLeft",
|
||||
"slotSidebarRight",
|
||||
"slotTrail",
|
||||
"spacing",
|
||||
"text",
|
||||
"track",
|
||||
"transition",
|
||||
"width",
|
||||
"zIndex"
|
||||
]
|
||||
}
|
||||
50
website/README.md
Executable file → Normal file
@ -1,34 +1,38 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
# create-svelte
|
||||
|
||||
## Getting Started
|
||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
|
||||
|
||||
First, run the development server:
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm create svelte@latest
|
||||
|
||||
# create a new project in my-app
|
||||
npm create svelte@latest my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
## Building
|
||||
|
||||
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
|
||||
To create a production version of your app:
|
||||
|
||||
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
||||
|
||||
@ -1,133 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
CloseButton,
|
||||
Heading,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Stack,
|
||||
Text,
|
||||
Tooltip,
|
||||
useDisclosure,
|
||||
VStack,
|
||||
} from "@chakra-ui/react";
|
||||
import { FC, ReactNode, useEffect, useState } from "react";
|
||||
|
||||
const AdDetector: FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const [adBlockEnabled, setAdBlockEnabled] = useState(false);
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [joke, setJoke] = useState<Record<string, any>>({});
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const googleAdUrl =
|
||||
"https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js";
|
||||
try {
|
||||
await fetch(new Request(googleAdUrl));
|
||||
} catch (e) {
|
||||
setAdBlockEnabled(true);
|
||||
setJoke(
|
||||
await (
|
||||
await fetch(
|
||||
"https://v2.jokeapi.dev/joke/Any?blacklistFlags=racist,sexist"
|
||||
)
|
||||
).json()
|
||||
);
|
||||
onOpen();
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent mt="5" mx="3">
|
||||
<ModalHeader>Support the Creator💚</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>
|
||||
Open source developers work really hard to provide the best,
|
||||
secure & efficient software experience for you & people all around
|
||||
the world. Most of the time we work without any wages at all but
|
||||
we need minimum support to live & these <b> Ads Helps Us</b> earn
|
||||
the minimum wage that we need to live.{" "}
|
||||
<Text color="green.500" fontWeight="bold" textAlign="justify">
|
||||
So, please support Spotube by disabling the AdBlocker on this
|
||||
page or by sponsoring or donating to our collectives directly
|
||||
</Text>
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button onClick={() => window.location.reload()}>
|
||||
Reload without AdBlocker
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{!adBlockEnabled ? (
|
||||
children
|
||||
) : (
|
||||
<Stack
|
||||
direction="column"
|
||||
w="100vw"
|
||||
h="100vh"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
p="5"
|
||||
pos="relative"
|
||||
>
|
||||
<Tooltip label="You made me sad 😢">
|
||||
<CloseButton
|
||||
pos="absolute"
|
||||
right="5"
|
||||
variant="ghost"
|
||||
onClick={() => setAdBlockEnabled(false)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<VStack spacing="2" alignItems="flex-start">
|
||||
<Heading size="sm">Here's something interesting:</Heading>
|
||||
<Heading size="md">
|
||||
{joke.joke ?? (
|
||||
<>
|
||||
<p>{joke.setup}</p>
|
||||
<p>{joke.delivery}</p>
|
||||
</>
|
||||
)}
|
||||
</Heading>
|
||||
</VStack>
|
||||
<VStack justifySelf="flex-end">
|
||||
<Heading
|
||||
mt="10"
|
||||
size={{
|
||||
base: "lg",
|
||||
lg: "xl",
|
||||
}}
|
||||
maxW="700px"
|
||||
textAlign="justify"
|
||||
lineHeight="1.5"
|
||||
>
|
||||
Be grateful for all the favors you get. But don't let it
|
||||
become a pile of debt. Try returning them as soon as you can.
|
||||
You'll feel relieved
|
||||
</Heading>
|
||||
<Heading
|
||||
size={{
|
||||
lg: "lg",
|
||||
base: "md",
|
||||
}}
|
||||
alignSelf="flex-end"
|
||||
>
|
||||
- Kingkor Roy Tirtho
|
||||
</Heading>
|
||||
</VStack>
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdDetector;
|
||||
@ -1,138 +0,0 @@
|
||||
import { Link, Flex, Box, chakra, Image, Button, Text } from "@chakra-ui/react";
|
||||
import { BlogPost } from "pages/blog";
|
||||
import { FC } from "react";
|
||||
import NavLink from "next/link";
|
||||
|
||||
const ArticleCard: FC<BlogPost> = ({
|
||||
metadata: {
|
||||
author,
|
||||
author_avatar_url,
|
||||
cover_image,
|
||||
tags,
|
||||
title,
|
||||
summary,
|
||||
date,
|
||||
},
|
||||
slug,
|
||||
}) => {
|
||||
return (
|
||||
<Box
|
||||
mx="auto"
|
||||
rounded="lg"
|
||||
shadow="md"
|
||||
bg="white"
|
||||
_dark={{
|
||||
bg: "#212121",
|
||||
}}
|
||||
maxW="2xl"
|
||||
>
|
||||
<Image
|
||||
roundedTop="lg"
|
||||
w="full"
|
||||
h={64}
|
||||
fit="cover"
|
||||
src={cover_image}
|
||||
alt="Article"
|
||||
/>
|
||||
|
||||
<Box p={6}>
|
||||
<Box>
|
||||
{tags.map((tag, i) => {
|
||||
return (
|
||||
<chakra.span
|
||||
key={i}
|
||||
px={3}
|
||||
py={1}
|
||||
mx="1"
|
||||
bg="gray.600"
|
||||
color="gray.100"
|
||||
fontSize="sm"
|
||||
fontWeight="700"
|
||||
rounded="md"
|
||||
>
|
||||
{tag}
|
||||
</chakra.span>
|
||||
);
|
||||
})}
|
||||
<NavLink href={`/blog/${slug}`} passHref>
|
||||
<Link
|
||||
display="block"
|
||||
color="gray.800"
|
||||
_dark={{
|
||||
color: "white",
|
||||
}}
|
||||
fontWeight="bold"
|
||||
fontSize="2xl"
|
||||
mt={2}
|
||||
_hover={{
|
||||
color: "gray.600",
|
||||
textDecor: "underline",
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Link>
|
||||
</NavLink>
|
||||
<chakra.p
|
||||
mt={2}
|
||||
fontSize="sm"
|
||||
color="gray.600"
|
||||
_dark={{
|
||||
color: "gray.400",
|
||||
}}
|
||||
>
|
||||
{summary}
|
||||
</chakra.p>
|
||||
</Box>
|
||||
|
||||
<Box mt={4}>
|
||||
<Flex
|
||||
alignItems="center"
|
||||
justify="space-between"
|
||||
flexDirection={{ base: "column", md: "row" }}
|
||||
>
|
||||
<Flex alignItems="center">
|
||||
<Image
|
||||
h={10}
|
||||
fit="cover"
|
||||
rounded="full"
|
||||
src={author_avatar_url}
|
||||
alt="Avatar"
|
||||
/>
|
||||
<Text
|
||||
mx={2}
|
||||
fontWeight="bold"
|
||||
color="gray.700"
|
||||
_dark={{
|
||||
color: "gray.200",
|
||||
}}
|
||||
>
|
||||
{author}
|
||||
</Text>
|
||||
<chakra.span
|
||||
mx={1}
|
||||
fontSize="sm"
|
||||
color="gray.600"
|
||||
_dark={{
|
||||
color: "gray.300",
|
||||
}}
|
||||
>
|
||||
{date}
|
||||
</chakra.span>
|
||||
</Flex>
|
||||
<NavLink href={`/blog/${slug}`} passHref>
|
||||
<Button
|
||||
_hover={{ textDecor: "none" }}
|
||||
colorScheme="purple"
|
||||
as={Link}
|
||||
>
|
||||
Read the Full Article
|
||||
</Button>
|
||||
</NavLink>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ArticleCard;
|
||||
@ -1,25 +0,0 @@
|
||||
import { chakra } from "@chakra-ui/react";
|
||||
import { FC, ReactNode } from "react";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const CodeBlock: FC<Props> = ({ children }) => {
|
||||
return (
|
||||
<chakra.pre
|
||||
bgColor="gray.200"
|
||||
p="3"
|
||||
borderRadius="md"
|
||||
_dark={{
|
||||
bgColor: "gray.700",
|
||||
}}
|
||||
w="100%"
|
||||
whiteSpace="pre-wrap"
|
||||
overflowX="auto"
|
||||
wordBreak="break-word"
|
||||
>
|
||||
<chakra.code>{children}</chakra.code>
|
||||
</chakra.pre>
|
||||
);
|
||||
};
|
||||
@ -1,118 +0,0 @@
|
||||
import {
|
||||
Menu,
|
||||
ButtonGroup,
|
||||
Button,
|
||||
MenuButton,
|
||||
IconButton,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
Link as Anchor,
|
||||
} from "@chakra-ui/react";
|
||||
import { Platform, usePlatform } from "hooks/usePlatform";
|
||||
import React from "react";
|
||||
import {
|
||||
FaApple,
|
||||
FaCaretDown,
|
||||
FaUbuntu,
|
||||
FaLinux,
|
||||
FaWindows,
|
||||
FaAndroid,
|
||||
} from "react-icons/fa";
|
||||
import { MdOutlineFileDownload } from "react-icons/md";
|
||||
|
||||
const baseURL = "https://github.com/KRTirtho/spotube/releases/latest/download/";
|
||||
|
||||
const DownloadLinks = Object.freeze({
|
||||
[Platform.linux]: [
|
||||
{
|
||||
name: "deb",
|
||||
url: baseURL + "Spotube-linux-x86_64.deb",
|
||||
icon: <FaUbuntu />,
|
||||
},
|
||||
{
|
||||
name: "tar",
|
||||
url: baseURL + "Spotube-linux-x86_64.tar.xz",
|
||||
icon: <FaLinux />,
|
||||
},
|
||||
{
|
||||
name: "AppImage",
|
||||
url: baseURL + "Spotube-linux-x86_64.AppImage",
|
||||
icon: <FaLinux />,
|
||||
},
|
||||
],
|
||||
[Platform.android]: [
|
||||
{
|
||||
name: "apk",
|
||||
url: baseURL + "Spotube-android-all-arch.apk",
|
||||
icon: <FaAndroid />,
|
||||
},
|
||||
],
|
||||
[Platform.mac]: [
|
||||
{
|
||||
name: "dmg",
|
||||
url: baseURL + "Spotube-macos-universal.dmg",
|
||||
icon: <FaApple />,
|
||||
},
|
||||
],
|
||||
[Platform.windows]: [
|
||||
{
|
||||
name: "exe",
|
||||
url: baseURL + "Spotube-windows-x86_64-setup.exe",
|
||||
icon: <FaWindows />,
|
||||
},
|
||||
{
|
||||
name: "nupkg",
|
||||
url: baseURL + "Spotube-windows-x86_64.nupkg ",
|
||||
icon: <FaWindows />,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const DownloadButton = () => {
|
||||
const platform = usePlatform();
|
||||
|
||||
const allPlatforms = Object.entries(Platform)
|
||||
.map(([, value]) => {
|
||||
return DownloadLinks[value].map((s) => ({
|
||||
...s,
|
||||
name: `${value} (.${s.name})`,
|
||||
}));
|
||||
})
|
||||
.flat(1);
|
||||
|
||||
const currentPlatform = DownloadLinks[platform][0];
|
||||
return (
|
||||
<Menu placement="bottom-end">
|
||||
<ButtonGroup spacing="0.5">
|
||||
<Button
|
||||
variant="solid"
|
||||
as={Anchor}
|
||||
href={currentPlatform.url}
|
||||
_hover={{ textDecoration: "none" }}
|
||||
leftIcon={
|
||||
<MdOutlineFileDownload fontSize="24"/>
|
||||
}
|
||||
>
|
||||
Download for {platform} (.{currentPlatform.name})
|
||||
</Button>
|
||||
<MenuButton
|
||||
aria-label="Show More Downloads"
|
||||
as={IconButton}
|
||||
variant="solid"
|
||||
icon={<FaCaretDown />}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
<MenuList>
|
||||
{allPlatforms.map(({ name, url, icon }) => {
|
||||
return (
|
||||
<MenuItem key={url} as={Anchor} href={url} icon={icon}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadButton;
|
||||
@ -1,88 +0,0 @@
|
||||
import { Flex, chakra, Link, IconButton } from "@chakra-ui/react";
|
||||
|
||||
import { FaGithub, FaRedditAlien } from "react-icons/fa";
|
||||
import { FiTwitter } from "react-icons/fi";
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<Flex
|
||||
w="full"
|
||||
as="footer"
|
||||
flexDir={{
|
||||
base: "column",
|
||||
sm: "row",
|
||||
}}
|
||||
align="center"
|
||||
justify="space-between"
|
||||
px="6"
|
||||
py="4"
|
||||
bg="white"
|
||||
_dark={{
|
||||
bg: "#282828",
|
||||
}}
|
||||
>
|
||||
<chakra.a
|
||||
href="#"
|
||||
fontSize="xl"
|
||||
fontWeight="bold"
|
||||
color="gray.600"
|
||||
_dark={{
|
||||
color: "white",
|
||||
_hover: {
|
||||
color: "gray.300",
|
||||
},
|
||||
}}
|
||||
_hover={{
|
||||
color: "gray.700",
|
||||
}}
|
||||
>
|
||||
Spotube
|
||||
</chakra.a>
|
||||
|
||||
<chakra.p
|
||||
py={{
|
||||
base: "2",
|
||||
sm: "0",
|
||||
}}
|
||||
color="gray.800"
|
||||
_dark={{
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
© {new Date().getFullYear()}, Spotube. All rights reserved
|
||||
</chakra.p>
|
||||
|
||||
<Flex mx="-2">
|
||||
<IconButton
|
||||
colorScheme="gray"
|
||||
as={Link}
|
||||
aria-label="Github Link"
|
||||
href="https://github.com/KRTirtho/spotube"
|
||||
target="_blank"
|
||||
icon={<FaGithub />}
|
||||
variant="link"
|
||||
/>
|
||||
<IconButton
|
||||
colorScheme="gray"
|
||||
as={Link}
|
||||
aria-label="Twitter Link"
|
||||
href="https://twitter.com/@KrTirtho"
|
||||
target="_blank"
|
||||
icon={<FiTwitter />}
|
||||
variant="link"
|
||||
/>
|
||||
<IconButton
|
||||
colorScheme="gray"
|
||||
as={Link}
|
||||
aria-label="Reddit Link"
|
||||
href="https://reddit.com/r/FlutterDev/search/?q=spotube&restrict_sr=1&sr_nsfw="
|
||||
target="_blank"
|
||||
icon={<FaRedditAlien />}
|
||||
variant="link"
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
@ -1,218 +0,0 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
chakra,
|
||||
CloseButton,
|
||||
Flex,
|
||||
Heading,
|
||||
HStack,
|
||||
IconButton,
|
||||
Link,
|
||||
useColorMode,
|
||||
useColorModeValue,
|
||||
useDisclosure,
|
||||
VisuallyHidden,
|
||||
VStack,
|
||||
} from "@chakra-ui/react";
|
||||
import NavLink from "next/link";
|
||||
import { GoLightBulb } from "react-icons/go";
|
||||
import { FiGithub, FiSun } from "react-icons/fi";
|
||||
import Image from "next/image";
|
||||
import React from "react";
|
||||
import { AiOutlineMenu } from "react-icons/ai";
|
||||
import { BsHeartFill } from "react-icons/bs";
|
||||
|
||||
const Navbar = () => {
|
||||
const bg = useColorModeValue("white", "gray.800");
|
||||
const mobileNav = useDisclosure();
|
||||
const { colorMode, toggleColorMode } = useColorMode();
|
||||
return (
|
||||
<React.Fragment>
|
||||
<chakra.header
|
||||
bg={bg}
|
||||
w="full"
|
||||
px={{
|
||||
base: 1,
|
||||
sm: 3,
|
||||
}}
|
||||
py={2}
|
||||
shadow="md"
|
||||
_dark={{
|
||||
bgColor: "#212121",
|
||||
}}
|
||||
>
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<Flex align="center">
|
||||
<NavLink href="/">
|
||||
<Image
|
||||
src="/spotube-logo.svg"
|
||||
alt="Logo"
|
||||
height="60"
|
||||
width="60"
|
||||
layout="fixed"
|
||||
/>
|
||||
</NavLink>
|
||||
<VisuallyHidden>Spotube</VisuallyHidden>
|
||||
<NavLink href="/" passHref>
|
||||
<Heading p="2" as="a" size="lg" mr="2">
|
||||
Spotube
|
||||
</Heading>
|
||||
</NavLink>
|
||||
</Flex>
|
||||
<HStack display="flex" alignItems="center" spacing={1}>
|
||||
<HStack
|
||||
spacing={1}
|
||||
mr={1}
|
||||
color="brand.500"
|
||||
display={{
|
||||
base: "none",
|
||||
md: "inline-flex",
|
||||
}}
|
||||
>
|
||||
<NavLink href="/other-downloads" passHref>
|
||||
<Button as="a" colorScheme="gray" variant="ghost">
|
||||
Downloads
|
||||
</Button>
|
||||
</NavLink>
|
||||
<NavLink href="/blog" passHref>
|
||||
<Button as="a" variant="ghost" colorScheme="gray">
|
||||
Blog
|
||||
</Button>
|
||||
</NavLink>
|
||||
<NavLink href="/about" passHref>
|
||||
<Button as="a" variant="ghost" colorScheme="gray">
|
||||
About
|
||||
</Button>
|
||||
</NavLink>
|
||||
<Button
|
||||
as={Link}
|
||||
href="https://github.com/KRTirtho/spotube"
|
||||
bgColor="black"
|
||||
color="white"
|
||||
target="_blank"
|
||||
_hover={{
|
||||
textDecor: "none",
|
||||
bgColor: "blackAlpha.800",
|
||||
}}
|
||||
_active={{
|
||||
bgColor: "blackAlpha.700",
|
||||
}}
|
||||
rightIcon={<FiGithub />}
|
||||
>
|
||||
Give us a ⭐ on
|
||||
</Button>
|
||||
</HStack>
|
||||
<Button
|
||||
size={{
|
||||
base: "sm",
|
||||
md: "sm",
|
||||
lg: "md",
|
||||
}}
|
||||
as={Link}
|
||||
href="https://opencollective.com/spotube"
|
||||
bgColor="pink.100"
|
||||
color="pink.500"
|
||||
_hover={{
|
||||
bgColor: "pink.200",
|
||||
textDecor: "none",
|
||||
}}
|
||||
_active={{
|
||||
bgColor: "pink.100",
|
||||
}}
|
||||
rightIcon={<BsHeartFill />}
|
||||
target="_blank"
|
||||
>
|
||||
Donate us
|
||||
</Button>
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
icon={colorMode == "light" ? <GoLightBulb /> : <FiSun />}
|
||||
aria-label="Dark Mode Toggle"
|
||||
onClick={() => {
|
||||
toggleColorMode();
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
display={{
|
||||
base: "inline-flex",
|
||||
md: "none",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
display={{
|
||||
base: "flex",
|
||||
md: "none",
|
||||
}}
|
||||
aria-label="Open menu"
|
||||
fontSize="20px"
|
||||
color="gray.800"
|
||||
_dark={{
|
||||
color: "inherit",
|
||||
}}
|
||||
variant="ghost"
|
||||
icon={<AiOutlineMenu />}
|
||||
onClick={mobileNav.onOpen}
|
||||
/>
|
||||
|
||||
<VStack
|
||||
pos="absolute"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
display={mobileNav.isOpen ? "flex" : "none"}
|
||||
flexDirection="column"
|
||||
p={2}
|
||||
pb={4}
|
||||
m={2}
|
||||
bg={bg}
|
||||
spacing={3}
|
||||
rounded="sm"
|
||||
shadow="sm"
|
||||
>
|
||||
<CloseButton
|
||||
aria-label="Close menu"
|
||||
onClick={mobileNav.onClose}
|
||||
/>
|
||||
<NavLink href="/other-downloads" passHref>
|
||||
<Button w="full" as="a" colorScheme="gray" variant="ghost">
|
||||
Downloads
|
||||
</Button>
|
||||
</NavLink>
|
||||
<NavLink href="/blog" passHref>
|
||||
<Button w="full" as="a" variant="ghost" colorScheme="gray">
|
||||
Blog
|
||||
</Button>
|
||||
</NavLink>
|
||||
<NavLink href="/about" passHref>
|
||||
<Button w="full" as="a" variant="ghost" colorScheme="gray">
|
||||
About
|
||||
</Button>
|
||||
</NavLink>
|
||||
<Button
|
||||
as={Link}
|
||||
href="https://github.com/KRTirtho/spotube"
|
||||
bgColor="black"
|
||||
color="white"
|
||||
target="_blank"
|
||||
w="full"
|
||||
_hover={{
|
||||
textDecor: "none",
|
||||
bgColor: "blackAlpha.800",
|
||||
}}
|
||||
_active={{
|
||||
bgColor: "blackAlpha.700",
|
||||
}}
|
||||
rightIcon={<FiGithub />}
|
||||
>
|
||||
Give us a ⭐ on
|
||||
</Button>
|
||||
</VStack>
|
||||
</Box>
|
||||
</HStack>
|
||||
</Flex>
|
||||
</chakra.header>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
@ -1,153 +0,0 @@
|
||||
import {
|
||||
Flex,
|
||||
Box,
|
||||
Icon,
|
||||
chakra,
|
||||
Image,
|
||||
HStack,
|
||||
IconButton,
|
||||
Link,
|
||||
CircularProgress,
|
||||
} from "@chakra-ui/react";
|
||||
import { MdEmail, MdLocationOn } from "react-icons/md";
|
||||
import { BsFillBriefcaseFill } from "react-icons/bs";
|
||||
import { FC } from "react";
|
||||
import { FaGithub } from "react-icons/fa";
|
||||
import { FiTwitter } from "react-icons/fi";
|
||||
import { octokit } from "configurations/ocotokit";
|
||||
import useSWR from "swr";
|
||||
|
||||
interface UserDetailedCardProps {
|
||||
username: string;
|
||||
emoji: string;
|
||||
}
|
||||
|
||||
const UserDetailedCard: FC<UserDetailedCardProps> = ({ username, emoji }) => {
|
||||
const { data } = useSWR(`user-${username}}`, () =>
|
||||
octokit.users.getByUsername({ username })
|
||||
);
|
||||
|
||||
if (!data) {
|
||||
return <CircularProgress />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
w="xs"
|
||||
bg="white"
|
||||
_dark={{
|
||||
bg: "#212121",
|
||||
}}
|
||||
shadow="xl"
|
||||
rounded="lg"
|
||||
overflow="hidden"
|
||||
>
|
||||
<Image
|
||||
w="full"
|
||||
h={56}
|
||||
fit="cover"
|
||||
objectPosition="center"
|
||||
src={data.data.avatar_url}
|
||||
alt="avatar"
|
||||
/>
|
||||
|
||||
<Flex
|
||||
alignItems="center"
|
||||
px={6}
|
||||
py={3}
|
||||
bg="#1c1c1c"
|
||||
_light={{ bg: "gray.50" }}
|
||||
>
|
||||
<span>{emoji}</span>
|
||||
<chakra.h1 mx={3} fontWeight="bold" fontSize="lg">
|
||||
{data.data.name ?? data.data.login}
|
||||
</chakra.h1>
|
||||
</Flex>
|
||||
|
||||
<Box py={4} px={6}>
|
||||
<chakra.p
|
||||
py={2}
|
||||
color="gray.700"
|
||||
_dark={{
|
||||
color: "gray.400",
|
||||
}}
|
||||
>
|
||||
{data.data.bio}
|
||||
</chakra.p>
|
||||
|
||||
{data.data.company && (
|
||||
<Flex
|
||||
alignItems="center"
|
||||
mt={4}
|
||||
color="gray.700"
|
||||
_dark={{
|
||||
color: "gray.200",
|
||||
}}
|
||||
>
|
||||
<Icon as={BsFillBriefcaseFill} h={6} w={6} mr={2} />
|
||||
|
||||
<chakra.h1 px={2} fontSize="sm">
|
||||
{data.data.company}
|
||||
</chakra.h1>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{data.data.location && (
|
||||
<Flex
|
||||
alignItems="center"
|
||||
mt={4}
|
||||
color="gray.700"
|
||||
_dark={{
|
||||
color: "gray.200",
|
||||
}}
|
||||
>
|
||||
<Icon as={MdLocationOn} h={6} w={6} mr={2} />
|
||||
|
||||
<chakra.h1 px={2} fontSize="sm">
|
||||
{data.data.location}
|
||||
</chakra.h1>
|
||||
</Flex>
|
||||
)}
|
||||
{data.data.email && (
|
||||
<Flex
|
||||
alignItems="center"
|
||||
mt={4}
|
||||
color="gray.700"
|
||||
_dark={{
|
||||
color: "gray.200",
|
||||
}}
|
||||
>
|
||||
<Icon as={MdEmail} h={6} w={6} mr={2} />
|
||||
<chakra.h1 px={2} fontSize="sm">
|
||||
{data.data.email}
|
||||
</chakra.h1>
|
||||
</Flex>
|
||||
)}
|
||||
<HStack justify="center" pt="4">
|
||||
<IconButton
|
||||
colorScheme="gray"
|
||||
as={Link}
|
||||
aria-label="Github Link"
|
||||
href={data.data.html_url}
|
||||
target="_blank"
|
||||
icon={<FaGithub />}
|
||||
variant="link"
|
||||
/>
|
||||
{data.data.twitter_username && (
|
||||
<IconButton
|
||||
colorScheme="gray"
|
||||
as={Link}
|
||||
aria-label="Twitter Link"
|
||||
href={`https://twitter.com/${data.data.twitter_username}`}
|
||||
target="_blank"
|
||||
icon={<FiTwitter />}
|
||||
variant="link"
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserDetailedCard;
|
||||
@ -1,72 +0,0 @@
|
||||
import Script from "next/script";
|
||||
import { FC, useId } from "react";
|
||||
|
||||
type AdComponent = FC<{
|
||||
slot: string;
|
||||
}>;
|
||||
|
||||
export const DisplayAd: AdComponent = ({ slot }) => {
|
||||
const id = useId();
|
||||
return (
|
||||
<>
|
||||
<ins
|
||||
className="adsbygoogle"
|
||||
style={{ display: "block" }}
|
||||
data-ad-client={process.env.NEXT_PUBLIC_ADSENSE_ID}
|
||||
data-ad-slot={slot}
|
||||
data-ad-format="auto"
|
||||
data-full-width-responsive="true"
|
||||
></ins>
|
||||
<Script
|
||||
id={id + "#" + slot}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `(adsbygoogle = window.adsbygoogle || []).push({});`,
|
||||
}}
|
||||
></Script>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const GridMultiplexAd: AdComponent = ({ slot }) => {
|
||||
const id = useId();
|
||||
return (
|
||||
<>
|
||||
<ins
|
||||
className="adsbygoogle"
|
||||
style={{ display: "block" }}
|
||||
data-ad-format="autorelaxed"
|
||||
data-ad-client={process.env.NEXT_PUBLIC_ADSENSE_ID}
|
||||
data-ad-slot={slot}
|
||||
></ins>
|
||||
<Script
|
||||
id={id + "#" + slot}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `(adsbygoogle = window.adsbygoogle || []).push({});`,
|
||||
}}
|
||||
></Script>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const InFeedAd = () => {
|
||||
const id = useId();
|
||||
return (
|
||||
<>
|
||||
<ins
|
||||
className="adsbygoogle"
|
||||
style={{ display: "block" }}
|
||||
data-ad-format="fluid"
|
||||
data-ad-layout-key="-e5+6n-34-bt+x0"
|
||||
data-ad-client="ca-pub-6419300932495863"
|
||||
data-ad-slot="6460144484"
|
||||
></ins>
|
||||
<Script
|
||||
id={id}
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `(adsbygoogle = window.adsbygoogle || []).push({});`,
|
||||
}}
|
||||
></Script>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,17 +0,0 @@
|
||||
export const GA_TRACKING_ID = process.env.NEXT_PUBLIC_GA_ID;
|
||||
|
||||
// https://developers.google.com/analytics/devguides/collection/gtagjs/pages
|
||||
export const pageview = (url: any) => {
|
||||
(window as any).gtag("config", GA_TRACKING_ID, {
|
||||
page_path: url,
|
||||
});
|
||||
};
|
||||
|
||||
// https://developers.google.com/analytics/devguides/collection/gtagjs/events
|
||||
export const event = ({ action, category, label, value }: any) => {
|
||||
(window as any).gtag("event", action, {
|
||||
event_category: category,
|
||||
event_label: label,
|
||||
value: value,
|
||||
});
|
||||
};
|
||||
@ -1,3 +0,0 @@
|
||||
import { Octokit } from "@octokit/rest";
|
||||
|
||||
export const octokit: Octokit = new Octokit();
|
||||
@ -1,26 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { detectOS } from "detect-browser";
|
||||
|
||||
export enum Platform {
|
||||
linux = "Linux",
|
||||
windows = "Windows",
|
||||
mac = "Mac",
|
||||
android = "Android",
|
||||
}
|
||||
|
||||
export function usePlatform(): Platform {
|
||||
const [platform, setPlatform] = useState(Platform.linux);
|
||||
|
||||
useEffect(() => {
|
||||
const detectedPlatform = detectOS(navigator.userAgent)?.toLowerCase();
|
||||
|
||||
if (!detectedPlatform) return;
|
||||
|
||||
if (detectedPlatform.includes("windows")) setPlatform(Platform.windows);
|
||||
else if (detectedPlatform.includes("mac")) setPlatform(Platform.mac);
|
||||
else if (detectedPlatform.includes("android"))
|
||||
setPlatform(Platform.android);
|
||||
}, []);
|
||||
|
||||
return platform;
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
import {
|
||||
Link as Anchor,
|
||||
Heading,
|
||||
Text,
|
||||
chakra,
|
||||
Code,
|
||||
HStack,
|
||||
Divider,
|
||||
Box,
|
||||
} from "@chakra-ui/react";
|
||||
import { Options } from "react-markdown";
|
||||
|
||||
export const MarkdownComponentDefs: Options["components"] = {
|
||||
a: (props: any) => <Anchor {...props} color="blue.500" />,
|
||||
h1: (props: any) => <Heading {...props} size="xl" mt="5" mb="1.5" />,
|
||||
h2: (props: any) => <Heading {...props} size="lg" mt="5" mb="1.5" />,
|
||||
h3: (props: any) => <Heading {...props} size="md" mt="5" mb="1.5" />,
|
||||
h4: (props: any) => <Heading {...props} size="sm" />,
|
||||
h5: (props: any) => <Heading {...props} size="xs" />,
|
||||
h6: (props: any) => <Heading {...props} size="xs" />,
|
||||
p: (props: any) => <Text {...props} />,
|
||||
li: (props: any) => <chakra.li {...props} ml="4" />,
|
||||
code: (props) => (
|
||||
<Code
|
||||
{...props}
|
||||
p={!props.inline ? 5 : 0}
|
||||
overflow="scroll"
|
||||
colorScheme="gray"
|
||||
maxW="full"
|
||||
/>
|
||||
),
|
||||
blockquote: (props) => {
|
||||
return (
|
||||
<HStack bgColor="blackAlpha.300" py="3" px="2">
|
||||
<Box borderLeft="2px solid gray" pl="2">
|
||||
<Text as="span" fontSize="sm">
|
||||
{props.children}
|
||||
</Text>
|
||||
</Box>
|
||||
</HStack>
|
||||
);
|
||||
},
|
||||
};
|
||||
5
website/next-env.d.ts
vendored
@ -1,5 +0,0 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
@ -1,7 +0,0 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
swcMinify: true,
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
6391
website/package-lock.json
generated
Normal file
@ -1,43 +1,63 @@
|
||||
{
|
||||
"name": "website",
|
||||
"version": "0.1.0",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.18.6",
|
||||
"@chakra-ui/react": "^2.2.4",
|
||||
"@chakra-ui/system": "^2.2.2",
|
||||
"@chakra-ui/theme-tools": "^2.0.5",
|
||||
"@emotion/react": "^11",
|
||||
"@emotion/styled": "^11",
|
||||
"@octokit/rest": "^19.0.3",
|
||||
"@types/progress": "^2.0.5",
|
||||
"detect-browser": "^5.3.0",
|
||||
"framer-motion": "^6",
|
||||
"gray-matter": "^4.0.3",
|
||||
"next": "12.2.2",
|
||||
"nextjs-progressbar": "^0.0.14",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-icons": "^4.4.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"remark-gemoji": "^7.0.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"swr": "^1.3.0"
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "playwright test",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.0.5",
|
||||
"@types/react": "18.0.15",
|
||||
"@types/react-dom": "18.0.6",
|
||||
"@types/react-syntax-highlighter": "^15.5.3",
|
||||
"eslint": "8.20.0",
|
||||
"eslint-config-next": "12.2.2",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"typescript": "4.7.4"
|
||||
"@playwright/test": "^1.28.1",
|
||||
"@skeletonlabs/skeleton": "2.8.0",
|
||||
"@skeletonlabs/tw-plugin": "0.3.1",
|
||||
"@sveltejs/adapter-cloudflare": "^4.1.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@tailwindcss/typography": "0.5.10",
|
||||
"@types/eslint": "8.56.0",
|
||||
"@types/node": "^20.11.16",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"autoprefixer": "10.4.17",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.35.1",
|
||||
"mdsvex": "^0.11.0",
|
||||
"postcss": "8.4.35",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-svelte": "^3.1.2",
|
||||
"svelte": "^4.2.7",
|
||||
"svelte-check": "^3.6.0",
|
||||
"tailwindcss": "3.4.1",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^5.0.3",
|
||||
"vite-plugin-tailwind-purgecss": "0.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "1.6.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.1",
|
||||
"@octokit/openapi-types": "^19.1.0",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"date-fns": "^3.3.1",
|
||||
"highlight.js": "11.9.0",
|
||||
"lucide-svelte": "^0.323.0",
|
||||
"mdsvex-relative-images": "^1.0.3",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"remark-container": "^0.1.2",
|
||||
"remark-external-links": "^9.0.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-github": "^12.0.0",
|
||||
"remark-reading-time": "^1.0.1",
|
||||
"svelte-fa": "^4.0.2",
|
||||
"svelte-markdown": "^0.4.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,138 +0,0 @@
|
||||
import "../styles/globals.css";
|
||||
import type { AppProps } from "next/app";
|
||||
import {
|
||||
ChakraProvider,
|
||||
extendTheme,
|
||||
withDefaultColorScheme,
|
||||
} from "@chakra-ui/react";
|
||||
import Navbar from "components/Navbar";
|
||||
import { chakra } from "@chakra-ui/react";
|
||||
import { mode } from "@chakra-ui/theme-tools";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
// import Script from "next/script";
|
||||
// import * as gtag from "configurations/gtag";
|
||||
// import AdDetector from "components/AdDetector";
|
||||
import Footer from "components/Footer";
|
||||
import NextNProgress from "nextjs-progressbar";
|
||||
|
||||
const customTheme = extendTheme(
|
||||
{
|
||||
initialColorMode: 'system',
|
||||
useSystemColorMode: true,
|
||||
styles: {
|
||||
global: (props: any) => ({
|
||||
body: {
|
||||
bg: mode("white", "#171717")(props),
|
||||
},
|
||||
}),
|
||||
},
|
||||
colors: {
|
||||
blue: {
|
||||
50: "#e6f2ff",
|
||||
100: "#e6f2ff",
|
||||
200: "#e6f2ff",
|
||||
300: "#1681bd",
|
||||
400: "#1681bd",
|
||||
500: "#3a4da5",
|
||||
600: "#2d3c7d",
|
||||
700: "#1f2b55",
|
||||
800: "#121c2e",
|
||||
900: "#080e18",
|
||||
},
|
||||
components: {
|
||||
Link: {
|
||||
baseStyle: {
|
||||
color: "blue",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
withDefaultColorScheme({ colorScheme: "blue" })
|
||||
);
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
const router = useRouter();
|
||||
// useEffect(() => {
|
||||
// const handleRouteChange = (url: string) => {
|
||||
// gtag.pageview(url);
|
||||
// };
|
||||
// router.events.on("routeChangeComplete", handleRouteChange);
|
||||
// router.events.on("hashChangeComplete", handleRouteChange);
|
||||
// return () => {
|
||||
// router.events.off("routeChangeComplete", handleRouteChange);
|
||||
// router.events.off("hashChangeComplete", handleRouteChange);
|
||||
// };
|
||||
// }, [router.events]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <Script
|
||||
async
|
||||
onError={(e) => {
|
||||
console.error("Script failed to load", e);
|
||||
}}
|
||||
src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${process.env.NEXT_PUBLIC_ADSENSE_ID}`}
|
||||
crossOrigin="anonymous"
|
||||
/> */}
|
||||
{/* Global Site Tag (gtag.js) - Google Analytics */}
|
||||
{/* <Script
|
||||
strategy="afterInteractive"
|
||||
src={`https://www.googletagmanager.com/gtag/js?id=${gtag.GA_TRACKING_ID}`}
|
||||
/> */}
|
||||
{/* <Script
|
||||
id="gtag-init"
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '${gtag.GA_TRACKING_ID}', {
|
||||
page_path: window.location.pathname,
|
||||
});
|
||||
`,
|
||||
}}
|
||||
/> */}
|
||||
<ChakraProvider theme={customTheme}>
|
||||
<Head>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<title>Spotube</title>
|
||||
</Head>
|
||||
<NextNProgress color="#00a7a4" />
|
||||
<chakra.div
|
||||
minH="100vh"
|
||||
display="flex"
|
||||
flexDir="column"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<div>
|
||||
<Navbar />
|
||||
<Component {...pageProps} />
|
||||
</div>
|
||||
<Footer />
|
||||
</chakra.div>
|
||||
</ChakraProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyApp;
|
||||
@ -1,68 +0,0 @@
|
||||
import {
|
||||
Center,
|
||||
CircularProgress,
|
||||
Heading,
|
||||
HStack,
|
||||
VStack,
|
||||
} from "@chakra-ui/react";
|
||||
import UserDetailedCard from "components/UserDetailedCard";
|
||||
import { octokit } from "configurations/ocotokit";
|
||||
import useSwr from "swr";
|
||||
|
||||
const maintainers = ["KRTirtho", "RustyApple"];
|
||||
|
||||
const About = () => {
|
||||
const { data } = useSwr("contributors", () =>
|
||||
octokit.repos.listContributors({
|
||||
owner: "KRTirtho",
|
||||
repo: "spotube",
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<VStack my="20" mx="10">
|
||||
<Heading>Maintainers</Heading>
|
||||
|
||||
<HStack pb="20" gap="40px" wrap="wrap" justify="center" align="start">
|
||||
{data ? (
|
||||
data.data.map((contributor) => {
|
||||
if (!maintainers.includes(contributor.login!)) return;
|
||||
return (
|
||||
<UserDetailedCard
|
||||
key={contributor.id}
|
||||
emoji="⚡"
|
||||
username={contributor.login!}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Center>
|
||||
<CircularProgress />
|
||||
</Center>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<Heading>Valuable Code Contributors💝</Heading>
|
||||
<HStack gap="40px" wrap="wrap" justify="center" align="start">
|
||||
{data ? (
|
||||
data.data.map((contributor) => {
|
||||
if (maintainers.includes(contributor.login!)) return;
|
||||
return (
|
||||
<UserDetailedCard
|
||||
key={contributor.id}
|
||||
emoji="💪💝"
|
||||
username={contributor.login!}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Center>
|
||||
<CircularProgress />
|
||||
</Center>
|
||||
)}
|
||||
</HStack>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default About;
|
||||
@ -1,119 +0,0 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { GetStaticPaths, GetStaticProps, NextPage } from "next";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import matter from "gray-matter";
|
||||
import { BlogMetadata } from ".";
|
||||
import gfm from "remark-gfm";
|
||||
import gemoji from "remark-gemoji";
|
||||
import { MarkdownComponentDefs } from "misc/MarkdownComponentDefs";
|
||||
import {
|
||||
Box,
|
||||
chakra,
|
||||
Flex,
|
||||
Heading,
|
||||
Image,
|
||||
Text,
|
||||
VStack,
|
||||
} from "@chakra-ui/react";
|
||||
import Head from "next/head";
|
||||
|
||||
interface Props {
|
||||
metadata: BlogMetadata;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const BlogPost: NextPage<Props> = ({
|
||||
content,
|
||||
metadata: { author, author_avatar_url, cover_image, date, tags, title },
|
||||
}) => {
|
||||
return (
|
||||
<VStack>
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
</Head>
|
||||
<Box w="full" maxH="xl" overflow="hidden" mb="5">
|
||||
<Image fit="cover" src={cover_image} alt={title} />
|
||||
</Box>
|
||||
<VStack
|
||||
align="start"
|
||||
spacing="4"
|
||||
maxW={{
|
||||
base: "full",
|
||||
lg: "70%",
|
||||
xl: "60%",
|
||||
}}
|
||||
py="5"
|
||||
px="10"
|
||||
w="full"
|
||||
>
|
||||
<Flex alignItems="center">
|
||||
<Image
|
||||
h="12"
|
||||
fit="cover"
|
||||
rounded="full"
|
||||
src={author_avatar_url}
|
||||
alt="Avatar"
|
||||
/>
|
||||
<VStack spacing="0">
|
||||
<Text
|
||||
mx={2}
|
||||
fontWeight="bold"
|
||||
color="gray.700"
|
||||
_dark={{
|
||||
color: "gray.200",
|
||||
}}
|
||||
>
|
||||
{author}
|
||||
</Text>
|
||||
<chakra.span
|
||||
mx={1}
|
||||
fontSize="sm"
|
||||
color="gray.600"
|
||||
_dark={{
|
||||
color: "gray.300",
|
||||
}}
|
||||
>
|
||||
{date}
|
||||
</chakra.span>
|
||||
</VStack>
|
||||
</Flex>
|
||||
<Heading>{title}</Heading>
|
||||
<ReactMarkdown
|
||||
components={MarkdownComponentDefs}
|
||||
remarkPlugins={[gfm, gemoji]}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
</VStack>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlogPost;
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
const paths = fs.readdirSync("posts").map((file) => {
|
||||
return {
|
||||
params: {
|
||||
slug: file.replace(".md", ""),
|
||||
},
|
||||
};
|
||||
});
|
||||
return {
|
||||
paths,
|
||||
fallback: false,
|
||||
};
|
||||
};
|
||||
|
||||
export const getStaticProps: GetStaticProps<Props> = async (ctx) => {
|
||||
const { content, data } = matter(
|
||||
fs.readFileSync(path.join("posts", `${ctx.params?.slug}.md`), "utf-8")
|
||||
);
|
||||
return {
|
||||
props: {
|
||||
content,
|
||||
metadata: data as BlogMetadata,
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -1,66 +0,0 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { GetStaticProps, NextPage } from "next";
|
||||
import matter from "gray-matter";
|
||||
import ArticleCard from "components/ArticleCard";
|
||||
import { VStack } from "@chakra-ui/react";
|
||||
import Head from "next/head";
|
||||
|
||||
export interface BlogMetadata {
|
||||
title: string;
|
||||
cover_image: string;
|
||||
author: string;
|
||||
date: string;
|
||||
author_avatar_url: string;
|
||||
tags: string[];
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export interface BlogPost {
|
||||
slug: string;
|
||||
metadata: BlogMetadata;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
posts: BlogPost[];
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps<Props> = async () => {
|
||||
const posts = fs.readdirSync("posts").map((file) => {
|
||||
return {
|
||||
slug: file.replace(".md", ""),
|
||||
metadata: matter(fs.readFileSync(path.join("posts", file)))
|
||||
.data as BlogMetadata,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
posts: posts.sort(
|
||||
// @ts-ignore
|
||||
(a, b) => new Date(b.metadata.date) - new Date(a.metadata.date)
|
||||
),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const Blog: NextPage<Props> = ({ posts }) => {
|
||||
return (
|
||||
<VStack mx="5" my="5" spacing="7">
|
||||
<Head>
|
||||
<title>Spotube - Blogs</title>
|
||||
</Head>
|
||||
{posts.map((post) => {
|
||||
return (
|
||||
<ArticleCard
|
||||
key={post.slug}
|
||||
metadata={post.metadata}
|
||||
slug={post.slug}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Blog;
|
||||
@ -1,180 +0,0 @@
|
||||
import {
|
||||
Heading,
|
||||
VStack,
|
||||
chakra,
|
||||
HStack,
|
||||
Text,
|
||||
useColorModeValue,
|
||||
} from "@chakra-ui/react";
|
||||
import DownloadButton from "components/DownloadButton";
|
||||
import Image from 'next/image';
|
||||
|
||||
const Root = () => {
|
||||
const textColor = useColorModeValue("#171717", "#f5f5f5");
|
||||
|
||||
return (
|
||||
<>
|
||||
<VStack spacing="$4" alignItems="stretch">
|
||||
<chakra.section
|
||||
h="60vh"
|
||||
bgColor={useColorModeValue("#f5f5f5", "#171717")}
|
||||
bgImage="url(/spotube-screenshot-web.jpg)"
|
||||
bgRepeat="no-repeat"
|
||||
bgSize="contain"
|
||||
bgPos={useColorModeValue("right", "left")}
|
||||
>
|
||||
<VStack mt="10" mx="6" spacing="4" alignItems={useColorModeValue("flex-start", "flex-end")}>
|
||||
<chakra.section
|
||||
p={{ base: "5", md: "0" }}
|
||||
borderRadius="2xl"
|
||||
bgColor={{
|
||||
base: "#f5f5f599",
|
||||
md: "transparent",
|
||||
}}
|
||||
>
|
||||
<Heading color={textColor} size="2xl">
|
||||
Spotube
|
||||
</Heading>
|
||||
<Heading color={textColor} size="lg" maxW="500px">
|
||||
A fast, modern, lightweight & efficient Spotify Music Client for
|
||||
every platform
|
||||
</Heading>
|
||||
</chakra.section>
|
||||
<DownloadButton />
|
||||
</VStack>
|
||||
</chakra.section>
|
||||
{/* <DisplayAd slot="9501208974" /> */}
|
||||
<chakra.div bgGradient="linear-gradient(180deg, rgba(249,207,143,1) 0%, rgba(250,250,250,1) 45%)">
|
||||
<VStack
|
||||
p="5"
|
||||
mt="10"
|
||||
pos="relative"
|
||||
_before={{
|
||||
content: "''",
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: -1,
|
||||
display: "block",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
background: "url('/headline-2b.png') rgba(0, 0, 0, 0.9)",
|
||||
bgSize: "contain",
|
||||
bgRepeat: "no-repeat",
|
||||
bgPos: "center",
|
||||
filter: "blur(2px)",
|
||||
}}
|
||||
flexDirection={{ base: "column", md: "row" }} // Stack content vertically on mobile, reverse order on desktop
|
||||
alignItems="center" // Center align content vertically on mobile
|
||||
textAlign="center" // Center align text horizontally on mobile
|
||||
>
|
||||
<chakra.div maxW="600px">
|
||||
<Image
|
||||
src="/headline-1.png"
|
||||
m="10"
|
||||
bgRepeat="no-repeat"
|
||||
bgSize="contain"
|
||||
h="60vh"
|
||||
width="1020"
|
||||
height="780"
|
||||
layout="intrinsic"
|
||||
alt="Headline 2"
|
||||
/>
|
||||
</chakra.div>
|
||||
<chakra.div
|
||||
maxW="500px"
|
||||
bgGradient={{
|
||||
base: "radial-gradient(circle, #ffffff 23%, rgba(0,0,0,0) 82%)",
|
||||
md: "radial-gradient(circle, #ffffff00 23%, rgba(0,0,0,0) 82%)",
|
||||
}}
|
||||
color="gray.800"
|
||||
>
|
||||
<Heading>Access all your Spotify Music & Playlists</Heading>
|
||||
<Text>
|
||||
You can use all your Spotify tracks & playlists here. Everything
|
||||
will be in Sync & some action taken from Spotube will also
|
||||
reflect on your Spotify Account.
|
||||
</Text>
|
||||
</chakra.div>
|
||||
</VStack>
|
||||
</chakra.div>
|
||||
<VStack
|
||||
p="5"
|
||||
mt="10"
|
||||
pos="relative"
|
||||
_before={{
|
||||
content: "''",
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: -1,
|
||||
display: "block",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
background: "url('/headline-2b.png') rgba(0, 0, 0, 0.9)",
|
||||
bgSize: "contain",
|
||||
bgRepeat: "no-repeat",
|
||||
bgPos: "center",
|
||||
filter: "blur(2px)",
|
||||
}}
|
||||
>
|
||||
<chakra.div maxW="600px">
|
||||
<Image
|
||||
src="/headline-2a.svg"
|
||||
width="1920"
|
||||
height="1080"
|
||||
layout="intrinsic"
|
||||
alt="Headline 2"
|
||||
/>
|
||||
</chakra.div>
|
||||
<chakra.div
|
||||
maxW="600px"
|
||||
color="gray.50"
|
||||
bgColor="blackAlpha.500"
|
||||
p="3"
|
||||
borderRadius="3xl"
|
||||
>
|
||||
<Heading>On your Mobile, PC, Tablet everywhere</Heading>
|
||||
<Text>
|
||||
Spotube is available for all "Major" Platforms including
|
||||
<b> Linux, Android, Windows & MacOS </b>natively unlike Spotify
|
||||
Desktop App which uses Electron, that is basically the entire
|
||||
Chrome
|
||||
</Text>
|
||||
</chakra.div>
|
||||
</VStack>
|
||||
{/* <DisplayAd slot="9501208974" /> */}
|
||||
<HStack wrap="wrap-reverse" justify="center" px="8" align="center">
|
||||
<chakra.div maxW="400px">
|
||||
<Heading>Open Source, privacy respecting & No ads</Heading>
|
||||
<Text>
|
||||
Spotube is an Open Source App being developed & maintained by
|
||||
Kingkor Roy Tirtho & is built by the Contributions of
|
||||
the Community and Contributors
|
||||
</Text>
|
||||
</chakra.div>
|
||||
<chakra.div maxW="500px">
|
||||
<Image
|
||||
src="/headline-3.svg"
|
||||
width="1920"
|
||||
height="1080"
|
||||
layout="intrinsic"
|
||||
alt="Headline 2"
|
||||
/>
|
||||
</chakra.div>
|
||||
</HStack>
|
||||
<VStack py="10" alignItems="center" textAlign="center">
|
||||
<div>
|
||||
<Heading size="lg">Download Now</Heading>
|
||||
<Text>
|
||||
Download Spotube for every platform you want. It's available everywhere.
|
||||
</Text>
|
||||
</div>
|
||||
<DownloadButton />
|
||||
</VStack>
|
||||
</VStack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Root;
|
||||
@ -1,109 +0,0 @@
|
||||
import {
|
||||
Link as Anchor,
|
||||
Heading,
|
||||
VStack,
|
||||
chakra,
|
||||
Box,
|
||||
Flex,
|
||||
Stack,
|
||||
HStack,
|
||||
useColorModeValue,
|
||||
} from "@chakra-ui/react";
|
||||
import NavLink from "next/link";
|
||||
// import { GridMultiplexAd } from "components/special";
|
||||
import { GiBackwardTime } from "react-icons/gi";
|
||||
import { FiPackage } from "react-icons/fi";
|
||||
import { HiOutlineSparkles } from "react-icons/hi";
|
||||
import { useRouter } from "next/router";
|
||||
import { FC } from "react";
|
||||
|
||||
function OtherDownloads() {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex justify="center">
|
||||
<VStack my="20" mx="5" maxW="3xl" spacing="28">
|
||||
<VStack spacing="2" align="start">
|
||||
<Heading size="2xl">Other ways to install?</Heading>
|
||||
<Heading size="md">
|
||||
Here's some alternative ways & versions of Spotube that you
|
||||
can install try out
|
||||
</Heading>
|
||||
</VStack>
|
||||
<Stack direction={["column", null, "row"]} spacing="4">
|
||||
<OtherDownloadLinkItem
|
||||
href={"/package-manager"}
|
||||
icon={<FiPackage />}
|
||||
>
|
||||
Package Managers & AppStores
|
||||
</OtherDownloadLinkItem>
|
||||
<OtherDownloadLinkItem
|
||||
href="/nightly-downloads"
|
||||
icon={<HiOutlineSparkles />}
|
||||
color={useColorModeValue("red.500", "red.200")}
|
||||
bgColor={useColorModeValue("red.100", "red.800")}
|
||||
>
|
||||
Nightly versions
|
||||
</OtherDownloadLinkItem>
|
||||
<OtherDownloadLinkItem
|
||||
href={"/stable-downloads"}
|
||||
icon={<GiBackwardTime />}
|
||||
>
|
||||
Previous versions
|
||||
</OtherDownloadLinkItem>
|
||||
(Nightly releases)
|
||||
</Stack>
|
||||
</VStack>
|
||||
</Flex>
|
||||
{/* <GridMultiplexAd slot="4575915852" /> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default OtherDownloads;
|
||||
|
||||
interface OtherDownloadLinkItemType {
|
||||
href: string;
|
||||
icon: React.ReactNode;
|
||||
color?: string;
|
||||
bgColor?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const OtherDownloadLinkItem: FC<OtherDownloadLinkItemType> = ({
|
||||
href,
|
||||
icon,
|
||||
color,
|
||||
bgColor,
|
||||
children,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const dColor = useColorModeValue("blue.500", "blue.200");
|
||||
const dBColor = useColorModeValue("blue.100", "blue.800");
|
||||
|
||||
return (
|
||||
<NavLink href={router.pathname + href} passHref style={{ width: "100%" }}>
|
||||
<Anchor color={color ?? dColor} w="100%">
|
||||
<Box
|
||||
w="100%"
|
||||
h="40"
|
||||
bgColor={bgColor ?? dBColor}
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
p="5"
|
||||
borderRadius="lg"
|
||||
fontSize="1.2rem"
|
||||
textAlign="center"
|
||||
>
|
||||
<chakra.p mb="2" fontSize="4xl">
|
||||
{icon}
|
||||
</chakra.p>
|
||||
{children}
|
||||
</Box>
|
||||
</Anchor>
|
||||
</NavLink>
|
||||
);
|
||||
};
|
||||
@ -1,118 +0,0 @@
|
||||
import {
|
||||
Link as Anchor,
|
||||
Button,
|
||||
Heading,
|
||||
Table,
|
||||
Td,
|
||||
Th,
|
||||
VStack,
|
||||
chakra,
|
||||
Text,
|
||||
Tr,
|
||||
HStack,
|
||||
} from "@chakra-ui/react";
|
||||
import { octokit } from "configurations/ocotokit";
|
||||
import { GetServerSideProps, NextPage } from "next";
|
||||
// import { GridMultiplexAd } from "components/special";
|
||||
import NavLink from "next/link";
|
||||
import { ReleaseResponse } from "./stable-downloads";
|
||||
|
||||
type NightlyProps = ReleaseResponse;
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<
|
||||
NightlyProps
|
||||
> = async () => {
|
||||
const { data } = await octokit.repos.getReleaseByTag({
|
||||
owner: "KRTirtho",
|
||||
repo: "spotube",
|
||||
tag: "nightly",
|
||||
});
|
||||
return {
|
||||
props: {
|
||||
tag_name: data.tag_name,
|
||||
id: data.id,
|
||||
body: data.body,
|
||||
assets: data.assets.map((asset) => ({
|
||||
id: asset.id,
|
||||
name: asset.name,
|
||||
browser_download_url: asset.browser_download_url,
|
||||
})),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const NightlyDownloads: NextPage<NightlyProps> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<VStack>
|
||||
<VStack
|
||||
alignSelf="center"
|
||||
alignItems="flex-start"
|
||||
spacing="4"
|
||||
maxW="500px"
|
||||
overflow="auto"
|
||||
m="5"
|
||||
>
|
||||
<Heading size="2xl">Nightly Release</Heading>
|
||||
<Text>Download latest & most bleeding edge version of Spotube</Text>
|
||||
<Text size="sm" color="red.500" textAlign="justify">
|
||||
Disclaimer!: Nightly versions are untested and not the final version
|
||||
of spotube. So it can consists of many hidden or unknown bugs but at
|
||||
the same time will be opted with latest & greatest features too. So
|
||||
use it at your own risk! If you don't know what you're
|
||||
doing than we recommend you to download the{" "}
|
||||
<NavLink href="/" passHref>
|
||||
<Anchor color="blue.500">latest stable version of</Anchor>
|
||||
</NavLink>{" "}
|
||||
Spotube
|
||||
</Text>
|
||||
<VStack
|
||||
p="2"
|
||||
w="100%"
|
||||
borderRadius="md"
|
||||
spacing="4"
|
||||
bgColor="gray.50"
|
||||
_dark={{ bgColor: "gray.900" }}
|
||||
>
|
||||
{Object.entries(props.assets).map(
|
||||
([_, { name, id, browser_download_url }], i) => {
|
||||
const segments = name.split("-");
|
||||
const platform = segments[1];
|
||||
const executable = segments[segments.length - 1].split(".")[1];
|
||||
return (
|
||||
<HStack
|
||||
key={id}
|
||||
p="4"
|
||||
w="100%"
|
||||
borderRadius="md"
|
||||
bgColor="gray.100"
|
||||
_dark={{ bgColor: "gray.800" }}
|
||||
>
|
||||
<Text w="200px" textTransform="capitalize">
|
||||
{platform}{" "}
|
||||
<chakra.span color="gray.500">({executable})</chakra.span>
|
||||
</Text>
|
||||
<Anchor
|
||||
overflowWrap="break-word"
|
||||
wordBreak="break-word"
|
||||
w="full"
|
||||
href={browser_download_url}
|
||||
color="blue.500"
|
||||
>
|
||||
{name}
|
||||
</Anchor>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</VStack>
|
||||
</VStack>
|
||||
<chakra.div w="full">
|
||||
{/* <GridMultiplexAd slot="3192619797" /> */}
|
||||
</chakra.div>
|
||||
</VStack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NightlyDownloads;
|
||||
@ -1,117 +0,0 @@
|
||||
import {
|
||||
Box,
|
||||
Code,
|
||||
Heading,
|
||||
HStack,
|
||||
Icon,
|
||||
Image,
|
||||
Link,
|
||||
Text,
|
||||
VStack,
|
||||
} from "@chakra-ui/react";
|
||||
import { CodeBlock } from "components/CodeBlock";
|
||||
import { FcLinux } from "react-icons/fc";
|
||||
import { BsWindows } from "react-icons/bs";
|
||||
// import { DisplayAd } from "components/special";
|
||||
|
||||
export default function PackageManagerPage() {
|
||||
return (
|
||||
<VStack p="5" spacing="5">
|
||||
<VStack align="start" w="full" maxW="2xl">
|
||||
<Heading>Installation using Package Managers</Heading>
|
||||
<Text>
|
||||
If you don't want to download the binary of Spotube, you can use
|
||||
various package managers to install Spotube too (only Windows & Linux
|
||||
now)
|
||||
</Text>
|
||||
<Heading size="lg" pt="5">
|
||||
Linux
|
||||
<Icon>
|
||||
<FcLinux />
|
||||
</Icon>
|
||||
</Heading>
|
||||
<Heading size="md" pt="3">
|
||||
Flatpak
|
||||
</Heading>
|
||||
<Text>
|
||||
Make sure Flatpak is installed in your Linux device & Run the
|
||||
following command in the terminal:
|
||||
</Text>
|
||||
<CodeBlock>$ flatpak install com.github.KRTirtho.Spotube</CodeBlock>
|
||||
<Heading size="md" pt="3">
|
||||
AUR
|
||||
</Heading>
|
||||
<Text>
|
||||
If you're an Arch Linux user, you can also install Spotube from
|
||||
AUR. Make sure you have <Code>yay</Code> or <Code>pamac</Code> or{" "}
|
||||
<Code>paru</Code> installed in your system. And Run the Following
|
||||
command in the Terminal:
|
||||
</Text>
|
||||
<CodeBlock>
|
||||
# for pamac user
|
||||
<br />$ pamac install spotube-bin
|
||||
</CodeBlock>
|
||||
<CodeBlock>
|
||||
# for paru user
|
||||
<br />$ paru -Sy spotube-bin
|
||||
</CodeBlock>
|
||||
<CodeBlock>
|
||||
# for yay user
|
||||
<br />$ yay -Sy spotube-bin
|
||||
</CodeBlock>
|
||||
<Box w="full">
|
||||
{/* <DisplayAd slot="9501208974" /> */}
|
||||
</Box>
|
||||
<HStack align="center" pt="5">
|
||||
<Heading size="lg">Windows</Heading>
|
||||
<BsWindows fontSize="25px" color="#00A4EF" />
|
||||
</HStack>
|
||||
<Heading size="md" pt="3">
|
||||
Chocolatey
|
||||
</Heading>
|
||||
<Text>
|
||||
Spotube is available in{" "}
|
||||
<Link
|
||||
color="blue.500"
|
||||
target="_blank"
|
||||
href="community.chocolatey.org"
|
||||
>
|
||||
community.chocolatey.org
|
||||
</Link>{" "}
|
||||
repo. If you have chocolatey install in your system just run following
|
||||
command in an Elevated Command Prompt or PowerShell:
|
||||
</Text>
|
||||
<CodeBlock>$ choco install spotube</CodeBlock>
|
||||
<Heading size="md" pt="3">
|
||||
WinGet
|
||||
</Heading>
|
||||
<Text>
|
||||
Yes, Spotube is also available in the Official Windows PackageManager
|
||||
WinGet. Make sure you have WinGet installed in your Windows machine
|
||||
and run following in a Terminal:
|
||||
</Text>
|
||||
<CodeBlock>$ winget install --id KRTirtho.Spotube</CodeBlock>
|
||||
<Box w="full">
|
||||
{/* <DisplayAd slot="9501208974" /> */}
|
||||
</Box>
|
||||
<Heading pt="5">Install from App Stores</Heading>
|
||||
<Heading size="md">Android (F-Droid)</Heading>
|
||||
<Text>
|
||||
Spotube for Android is available in the F-Droid repositories too. So
|
||||
you can install it directly from F-Droid Android application too
|
||||
</Text>
|
||||
<Link
|
||||
href="https://f-droid.org/packages/oss.krtirtho.spotube/"
|
||||
target="_blank"
|
||||
>
|
||||
<Image
|
||||
src="https://user-images.githubusercontent.com/61944859/174589876-bace24c0-b3fd-4c4a-bdb4-6fa82b5853ec.png"
|
||||
alt="F-Droid Download"
|
||||
height="70"
|
||||
width="240"
|
||||
/>
|
||||
</Link>
|
||||
</VStack>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
@ -1,195 +0,0 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Link as Anchor,
|
||||
Heading,
|
||||
HStack,
|
||||
Text,
|
||||
VStack,
|
||||
chakra,
|
||||
} from "@chakra-ui/react";
|
||||
import { Octokit, RestEndpointMethodTypes } from "@octokit/rest";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { Platform } from "hooks/usePlatform";
|
||||
import gfm from "remark-gfm";
|
||||
// import { DisplayAd, InFeedAd } from "components/special";
|
||||
import { GetServerSideProps, NextPage } from "next";
|
||||
import { MarkdownComponentDefs } from "misc/MarkdownComponentDefs";
|
||||
import { octokit } from "configurations/ocotokit";
|
||||
import gemoji from "remark-gemoji";
|
||||
|
||||
enum AssetTypes {
|
||||
sums = "sums",
|
||||
linux = "linux",
|
||||
mac = "mac",
|
||||
windows = "windows",
|
||||
android = "android",
|
||||
}
|
||||
|
||||
export type ReleaseResponse = {
|
||||
id: number;
|
||||
body: string | null | undefined;
|
||||
tag_name: string;
|
||||
assets: {
|
||||
id: number;
|
||||
name: string;
|
||||
browser_download_url: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
type Props = {
|
||||
data: ReleaseResponse[];
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<Props> = async ({
|
||||
res,
|
||||
}) => {
|
||||
res.setHeader(
|
||||
"Cache-Control",
|
||||
"public, s-maxage=10, stale-while-revalidate=59"
|
||||
);
|
||||
const { data } = await octokit.repos.listReleases({
|
||||
owner: "KRTirtho",
|
||||
repo: "spotube",
|
||||
});
|
||||
const releaseResponse: ReleaseResponse[] = data
|
||||
.map((data) => {
|
||||
return {
|
||||
tag_name: data.tag_name,
|
||||
id: data.id,
|
||||
body: data.body,
|
||||
assets: data.assets.map((asset) => ({
|
||||
id: asset.id,
|
||||
name: asset.name,
|
||||
browser_download_url: asset.browser_download_url,
|
||||
})),
|
||||
};
|
||||
})
|
||||
.filter((release) => release.tag_name !== "nightly");
|
||||
return {
|
||||
props: {
|
||||
data: releaseResponse,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const StableDownloads: NextPage<Props> = ({ data }) => {
|
||||
return (
|
||||
<VStack alignItems="center">
|
||||
<VStack alignItems="stretch" m="3">
|
||||
<Heading size="xl">Previous Versions</Heading>
|
||||
<Text my="5">
|
||||
If any of your version is not working correctly than you can download
|
||||
& use previous versions of Spotube too
|
||||
</Text>
|
||||
|
||||
<VStack
|
||||
alignItems="flex-start"
|
||||
spacing="3"
|
||||
mr="1"
|
||||
>
|
||||
{data.map((release, i) => {
|
||||
const releaseSome = release.assets
|
||||
.map((asset) => {
|
||||
const platform =
|
||||
Object.keys(Platform).find((platform) =>
|
||||
asset.name.toLowerCase().includes(platform)
|
||||
) ?? "sums";
|
||||
return {
|
||||
type: AssetTypes[platform as keyof typeof AssetTypes],
|
||||
asset,
|
||||
};
|
||||
})
|
||||
.reduce((acc, val) => {
|
||||
acc[val.type] = [...(acc[val.type] ?? []), val.asset];
|
||||
return acc;
|
||||
}, {} as any) as {
|
||||
[key in AssetTypes]: RestEndpointMethodTypes["repos"]["listReleases"]["response"]["data"][0]["assets"];
|
||||
};
|
||||
return (
|
||||
<VStack key={release.id} py="3" w="full">
|
||||
<Heading size="md">
|
||||
Version{" "}
|
||||
<Text as="span" color="blue.500">
|
||||
{release.tag_name}
|
||||
</Text>{" "}
|
||||
{i == 0 && "(Latest)"}
|
||||
</Heading>
|
||||
{Object.entries(releaseSome).map(([type, assets], i) => {
|
||||
return (
|
||||
<HStack
|
||||
key={i}
|
||||
spacing={0}
|
||||
p="2"
|
||||
alignItems="flex-start"
|
||||
bgColor="gray.50"
|
||||
_dark={{ bgColor: "gray.900" }}
|
||||
>
|
||||
<Heading
|
||||
w={90}
|
||||
p="2"
|
||||
colorScheme="blue"
|
||||
borderRadius="5px 0 0 5px"
|
||||
borderRight="none"
|
||||
size="sm"
|
||||
>
|
||||
{type[0].toUpperCase() + type.slice(1)}
|
||||
</Heading>
|
||||
<VStack
|
||||
alignItems="flex-start"
|
||||
w={{
|
||||
base: "full",
|
||||
sm: "72",
|
||||
}}
|
||||
spacing={2}
|
||||
>
|
||||
{assets.map((asset) => {
|
||||
return (
|
||||
<Anchor
|
||||
key={asset.id}
|
||||
color="blue.500"
|
||||
width="full"
|
||||
p="1.5"
|
||||
href={asset.browser_download_url}
|
||||
target="_blank"
|
||||
referrerPolicy="no-referrer"
|
||||
bgColor="gray.100"
|
||||
_dark={{ bgColor: "gray.800" }}
|
||||
borderRadius="md"
|
||||
>
|
||||
{asset.name}
|
||||
</Anchor>
|
||||
);
|
||||
})}
|
||||
</VStack>
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
<Accordion defaultIndex={i} allowToggle>
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
Release Notes <AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel>
|
||||
<ReactMarkdown
|
||||
components={MarkdownComponentDefs}
|
||||
remarkPlugins={[gfm, gemoji]}
|
||||
>
|
||||
{release.body ?? ""}
|
||||
</ReactMarkdown>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</VStack>
|
||||
);
|
||||
})}
|
||||
</VStack>
|
||||
</VStack>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default StableDownloads;
|
||||
12
website/playwright.config.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import type { PlaywrightTestConfig } from '@playwright/test';
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
webServer: {
|
||||
command: 'npm run build && npm run preview',
|
||||
port: 4173
|
||||
},
|
||||
testDir: 'tests',
|
||||
testMatch: /(.+\.)?(test|spec)\.[jt]s/
|
||||
};
|
||||
|
||||
export default config;
|
||||
6
website/postcss.config.cjs
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@ -1,84 +0,0 @@
|
||||
---
|
||||
title: Getting Started With Spotube
|
||||
cover_image: https://github.com/KRTirtho/spotube/raw/master/assets/spotube-screenshot.png
|
||||
date: "July 16, 2022"
|
||||
author: Kingkor Roy Tirtho
|
||||
author_avatar_url: https://avatars.githubusercontent.com/u/61944859?v=4
|
||||
tags:
|
||||
- getting-started
|
||||
- spotube
|
||||
summary: You installed Spotube, don't know what to do now? Then don't worry we Gotchu covered here. We'll guide you through the basics of using Spotube & how you can use it to enrich your daily life with music.
|
||||
---
|
||||
|
||||
So installing Spotube is done. It’s a new app & although most of the things can be understood just by using the app as the UI is descriptive but still sometimes newcomers become overwhelmed by its UI & functionalities. In this article, we’re going to guide you through the problems that you can face while using Spotube by misunderstanding
|
||||
|
||||
## Login with Spotify
|
||||
|
||||
The most common issue with Spotube when someone uses it for the first is with its Login mechanism. Spotube is a Spotify client. And Spotify clients usually require *Premium Accounts* as the Spotify Server only allows premium accounts to download music. But **in Spotube you don’t need a Premium Account**
|
||||
|
||||
Since Spotube doesn’t require premium accounts it needs to special workarounds to supply the Music for Free. Thus, the Login mechanism in Spotube is a bit lengthy, but it’s actually more secure than any other Spotify Clients as the data stays in your Account & doesn’t need to go through a middleware unlike other Spotify Clients
|
||||
|
||||
Now, let’s get through the login Part. For Login, you’ll need two special things
|
||||
|
||||
- Client ID
|
||||
- Client Secret
|
||||
|
||||
**What is a Client ID?**
|
||||
|
||||
Client ID is kind of a Public key (ID) that is a unique identifier for the Spotify API client & is usually used to pair with Client Secret
|
||||
|
||||
**What is a Client Secret?**
|
||||
|
||||
A client secret is **a secret known only to the application instance and the authorization server**. It protects Spotify Data by only granting tokens to authorized requestors
|
||||
|
||||
Now In Spotube, tap on the Settings Icon in the Sidebar/Bottom Bar & click on the **Login With Spotify** button.
|
||||
|
||||
Now you’ll have to open a browser & have to go to [developers.spotify.com/dashboard](https://developers.spotify.com/dashboard) & press the Login button
|
||||
|
||||

|
||||
|
||||
After Login, create a Spotify Developer App by clicking on the CREATE AN APP button. Give the App a name & description & hit CREATE. Finally, the view will look somewhat similar to below
|
||||
|
||||

|
||||
|
||||
Now comes the **Most Important Part…**
|
||||
|
||||
First Tap on the App to enter the dashboard page. In there, tap EDIT SETTINGS & a Dialog with multiple configuration will Open
|
||||
|
||||

|
||||
|
||||
Now find the **Redirect Uris** and type/paste `http://localhost:4304/auth/spotify/callback` in the field and press Add. Finally scroll down to the bottom section of the Dialog and press the SAVE button to save the changes
|
||||
|
||||

|
||||
|
||||
Now close the Dialog & see in the Left/Below of the EDIT SETTINGS button, there you’ll find **Client ID** and a SHOW CLIENT SECRET button. Copy the *Client ID* & paste it in the Spotube’s Text Field. Then tap/click on the SHOW CLIENT SECRET button to reveal the **Client Secret.** Finally, copy the **Client Secret** & paste it in the Spotube’s corresponding Text Field
|
||||
|
||||

|
||||
|
||||
Finally, press on the *Submit Button* which will open a Browser Window/Tab (desktop) or a Browser Application (android). Press/Click ALLOW button in that page & now you’re successfully Logged In with your Spotify Account in Spotube
|
||||
|
||||
Close the Browser Tab (optional) & Go back to Spotube and Enjoy your lifetime (probably) free Music
|
||||
|
||||
## Playing Playlists & Tracks
|
||||
|
||||
You can play any playlists in the Home Screen of Spotube just by pressing the Play button of the playlist. But this is not just it. You can also play any playlist from starting from any track of the playlist and this available in all platform unlike Spotify which doesn't allow this kind stupid simple stuff in the Mobile App
|
||||
Just tap on any Playlist's Cover Image & from the track list, tap the little Play button next to the track from which you want the playlist to start playing.
|
||||
|
||||

|
||||
|
||||
|
||||
## Sing along with any Song with **Synced Lyrics** just like Karaoke
|
||||
|
||||
Yes, Spotube support Synced Lyrics too (most of the popular English songs). So, if you wanna sing along with your favorite Music Track but don't know the Lyrics Spotube got you covered & now even you can sing along. Here's how, just play any playlist/album/track & press on the Lyrics Tab in the Sidebar (desktop/tablet) or Bottom Bar (mobile) & the Synced Lyrics will automatically start playing with the Audio Track
|
||||
|
||||

|
||||
|
||||
|
||||
## Discover Weekly Playlist
|
||||
|
||||
Spotube categorizes playlists by Genre & always shows the most trending playlists in the home screen. In Spotube the Music isn't personalized & it shows you the playlists with best music. So the experience is more vast & what you experience is genuinely what you love & want to listen to not forced or manipulated to
|
||||
|
||||
But there's one question, **where the heck is my Discover Weekly playlist**?
|
||||
It's there. But doesn't show up in the home screen because it's a special kind of playlist that Spotify doesn't allow third-party clients to access unless the User specifically searches for it. You can find your Discover Weekly playlist by simply searching on Spotube's search page like below
|
||||
|
||||

|
||||
0
website/posts/images/.gitkeep
Normal file
41
website/posts/spotube-basics.md
Normal file
@ -0,0 +1,41 @@
|
||||
---
|
||||
title: Spotube Basics
|
||||
author: Kingkor Roy Tirtho
|
||||
date: 2024-02-10
|
||||
published: true
|
||||
---
|
||||
|
||||
Spotube is an open-source Spotify client that allows users to stream music from Spotify. To use Spotube, you need to sign in with your Spotify account. Here's a step-by-step guide on how to sign in to Spotube.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, make sure you have the following:
|
||||
|
||||
- A Spotify account
|
||||
- The Spotube application installed on your device
|
||||
|
||||
## Steps
|
||||
|
||||
1. Open the Spotube application on your device.
|
||||
2. Click on the "Sign in with Spotify" button.
|
||||
3. You will be redirected to the Spotify login page. Enter your Spotify username and password and click on the "Log In" button.
|
||||
4. You will be asked to grant Spotube permission to access your Spotify account. Click on the "Agree" button to grant permission.
|
||||
5. You will be redirected back to the Spotube application, and you should now be signed in to your Spotify account.
|
||||
|
||||
That's it! You are now signed in to Spotube and can start streaming music from Spotify.
|
||||
|
||||
| Title | Author | Date | Published |
|
||||
| -------------- | ------------------ | ---------- | --------- |
|
||||
| Spotube Basics | Kingkor Roy Tirtho | 2024-02-10 | true |
|
||||
|
||||
```bash
|
||||
$ git clone
|
||||
```
|
||||
|
||||
```javascript
|
||||
const a = 1;
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
Signing in to Spotube is a simple process that requires you to have a Spotify account and the Spotube application installed on your device. By following the steps outlined in this tutorial, you should be able to sign in to Spotube and start streaming music from Spotify.
|
||||
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
@ -1 +0,0 @@
|
||||
google-site-verification: google7d445d7199e703dc.html
|
||||
|
Before Width: | Height: | Size: 336 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 16 KiB |
@ -1 +0,0 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
|
Before Width: | Height: | Size: 323 KiB |
22
website/src/app.d.ts
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
// and what to do when importing types
|
||||
declare namespace App {
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Error {}
|
||||
// interface Platform {}
|
||||
interface Platform {
|
||||
env: {
|
||||
COUNTER: DurableObjectNamespace;
|
||||
};
|
||||
context: {
|
||||
waitUntil(promise: Promise<any>): void;
|
||||
};
|
||||
caches: CacheStorage & { default: Cache };
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@fortawesome/pro-solid-svg-icons/index.es' {
|
||||
export * from '@fortawesome/pro-solid-svg-icons';
|
||||
}
|
||||
23
website/src/app.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
|
||||
<!-- favicon 32x32 -->
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="%sveltekit.assets%/favicon-32x32.png" />
|
||||
<!-- favicon 16x16 -->
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="%sveltekit.assets%/favicon-16x16.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<!-- Apple icons -->
|
||||
<link rel="apple-touch-icon" href="%sveltekit.assets%/apple-touch-icon.png" />
|
||||
<!-- Android Chrome -->
|
||||
<link rel="manifest" href="%sveltekit.assets%/manifest.json" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
<body data-sveltekit-preload-data="hover" data-theme="wintry">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
11
website/src/app.postcss
Normal file
@ -0,0 +1,11 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@tailwind variants;
|
||||
|
||||
/* vintage theme */
|
||||
@font-face {
|
||||
font-family: 'Abril Fatface';
|
||||
src: url('/fonts/AbrilFatface.ttf');
|
||||
font-display: swap;
|
||||
}
|
||||
25
website/src/lib/components/downloads/download-items.svelte
Normal file
@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import type { IconDefinition } from '@fortawesome/free-brands-svg-icons';
|
||||
import Fa from 'svelte-fa';
|
||||
|
||||
export let links: Record<string, [string, IconDefinition[], string]>;
|
||||
</script>
|
||||
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{#each Object.entries(links) as link}
|
||||
<a
|
||||
href={link[1][0]}
|
||||
class="flex flex-col btn variant-ghost-primary rounded-xl p-0 overflow-hidden"
|
||||
>
|
||||
<div class="relative bg-primary-500 p-4 flex gap-4 justify-center rounded-t-xl w-full">
|
||||
{#each link[1][1] as icon}
|
||||
<Fa {icon} />
|
||||
{/each}
|
||||
<p class="chip variant-ghost-warning text-warning-400 absolute right-2 uppercase">
|
||||
{link[1][2]}
|
||||
</p>
|
||||
</div>
|
||||
<p class="p-4">{link[0]}</p>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
3
website/src/lib/components/markdown/layout.svelte
Normal file
@ -0,0 +1,3 @@
|
||||
<article class="prose lg:prose-lg dark:prose-invert max-w-3xl">
|
||||
<slot />
|
||||
</article>
|
||||
32
website/src/lib/components/navbar/darkmode-toggle.svelte
Normal file
@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { SlideToggle } from '@skeletonlabs/skeleton';
|
||||
import { persisted } from '$lib/persisted-store';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export const isDark = persisted('dark-mode', false);
|
||||
|
||||
$: {
|
||||
if (browser) {
|
||||
$isDark
|
||||
? document.documentElement.classList.add('dark')
|
||||
: document.documentElement.classList.remove('dark');
|
||||
}
|
||||
}
|
||||
|
||||
export let label: string | undefined;
|
||||
</script>
|
||||
|
||||
<div class="inline-flex gap-2">
|
||||
{#if label}
|
||||
<label class="ps-4">{label}</label>
|
||||
{/if}
|
||||
<SlideToggle
|
||||
active="bg-primary-backdrop-token"
|
||||
size="sm"
|
||||
name="dark-mode"
|
||||
checked={$isDark}
|
||||
on:change={() => {
|
||||
isDark.update((prev) => !prev);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||