mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-16 17:05:17 +00:00
commit
34554f0ced
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -53,7 +53,7 @@ body:
|
||||
description: Where did you install Spotube from?
|
||||
multiple: true
|
||||
options:
|
||||
- "Website (spotube.netlify.app)"
|
||||
- "Website (spotube.netlify.app) or (spotube.krtirtho.dev)"
|
||||
- "GitHub Releases (Binary)"
|
||||
- "GitHub Actions (Nightly Binary)"
|
||||
- "Play Store (Android)"
|
||||
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -5,6 +5,7 @@
|
||||
"danceability",
|
||||
"instrumentalness",
|
||||
"Mpris",
|
||||
"riverpod",
|
||||
"speechiness",
|
||||
"Spotube",
|
||||
"winget"
|
||||
|
24
CHANGELOG.md
24
CHANGELOG.md
@ -2,6 +2,30 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
## [3.0.1](https://github.com/KRTirtho/spotube/compare/v3.0.0...v3.1.0) (2023-08-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Force High Refresh Rate on some Android devices ([#607](https://github.com/KRTirtho/spotube/issues/607)) ([6dff099](https://github.com/KRTirtho/spotube/commit/6dff0996bdfee603acf242b1316f8793d625267c))
|
||||
* **translations:** add spanish translations ([#585](https://github.com/KRTirtho/spotube/issues/585)) ([042d7a4](https://github.com/KRTirtho/spotube/commit/042d7a4a10c78dd93a56a2f32d18a0fb74dbe697))
|
||||
* **translations:** add Simplified Chinese translation. ([#556](https://github.com/KRTirtho/spotube/issues/556)) ([26dbd52](https://github.com/KRTirtho/spotube/commit/26dbd523737d868114a47e82acd412cdae622b7c))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* alternative track source textfield safe area ([b8c6d7e](https://github.com/KRTirtho/spotube/commit/b8c6d7eb6ae1c54bdc83a455850dfca0f27bd881))
|
||||
* avoid sponsor block for first few seconds to not break the stream ([d8cf2ae](https://github.com/KRTirtho/spotube/commit/d8cf2ae1315dc3848fe1ac12286faafe90fdbed7))
|
||||
* cache segments casting error ([dfd60bd](https://github.com/KRTirtho/spotube/commit/dfd60bd4cc0fe8fe90e0cbfd26331df505cde2aa))
|
||||
* duration is always zero in PlayerView ([4885dca](https://github.com/KRTirtho/spotube/commit/4885dca04f06658391d1063e6c5a009547391a6f))
|
||||
* flags not showing up and html in descriptions ([5a563ef](https://github.com/KRTirtho/spotube/commit/5a563ef4289423ceb5c44ba13f3cfda34b2d16dd))
|
||||
* **linux:** crash when no secret service provider found ([#608](https://github.com/KRTirtho/spotube/issues/608)) ([888a4b1](https://github.com/KRTirtho/spotube/commit/888a4b1162c25371d7f6e88fae3a2473cabf1434))
|
||||
* login dialog stays after login, mention sp_gaid in tutorial ([b492840](https://github.com/KRTirtho/spotube/commit/b4928405122ae5e5d4d4560f316f2a546a2fabe4))
|
||||
* **album_sync**: negative index exception in update palette ([#561](https://github.com/KRTirtho/spotube/issues/561)) ([0089d47](https://github.com/KRTirtho/spotube/commit/0089d471ae6d595e058061e3ac44caecdba12f61))
|
||||
* remove adaptive widgets ([#520](https://github.com/KRTirtho/spotube/issues/520)) ([e4cbdd3](https://github.com/KRTirtho/spotube/commit/e4cbdd37479a572198c1ca27fcbbba0232275513))
|
||||
* shuffle not working ([#562](https://github.com/KRTirtho/spotube/issues/562)) ([dc76634](https://github.com/KRTirtho/spotube/commit/dc76634a6e4ccdca0f09d63a2db82cce53d950d7))
|
||||
* track not skipping to next even when source is available ([0b7affd](https://github.com/KRTirtho/spotube/commit/0b7affdc058c028982266d5c93215697301846bd))
|
||||
|
||||
## [3.0.0](https://github.com/KRTirtho/spotube/compare/v2.7.1...v3.0.0) (2023-07-02)
|
||||
|
||||
|
||||
|
@ -131,7 +131,7 @@ Do the following:
|
||||
```
|
||||
- Fedora
|
||||
```bash
|
||||
dnf install mpv mpv-devel libappindicator-gtk3 libappindicator-gtk3-devel libsecret libsecret-devel jsoncpp jsoncpp-devel libnotify libnotify-devel
|
||||
dnf install mpv mpv-devel libappindicator-gtk3 libappindicator-gtk3-devel libsecret libsecret-devel jsoncpp jsoncpp-devel libnotify libnotify-devel NetworkManager
|
||||
```
|
||||
- Clone the Repo
|
||||
- Create a `.env` in root of the project following the `.env.example` template
|
||||
|
2
LICENSE
2
LICENSE
@ -9,4 +9,4 @@ Redistribution and use in source and binary forms, with or without modification,
|
||||
3. All advertising materials mentioning features or use of this software must display the following acknowledgement:
|
||||
This product includes software developed by Kingkor Roy Tirtho.
|
||||
4. Neither the name of the Software nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
THIS SOFTWARE IS PROVIDED BY KINGKOR ROY TIRTHO AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL KINGKOR ROY TIRTHO AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
THIS SOFTWARE IS PROVIDED BY KINGKOR ROY TIRTHO AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL KINGKOR ROY TIRTHO AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
@ -69,7 +69,7 @@ This handy table lists all methods you can use to install Spotube:
|
||||
<td>Android</td>
|
||||
<td>
|
||||
<a href="https://play.google.com/store/apps/details?id=oss.krtirtho.spotube">
|
||||
<img width="220" alt="Download from Play store" src="https://github.com/steverichey/google-play-badge-svg/raw/master/img/en_get.svg">
|
||||
<img width="220" alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png">
|
||||
</a>
|
||||
<br>
|
||||
<a href="https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-android-all-arch.apk">
|
||||
@ -168,15 +168,15 @@ You can compile Spotube's source code by [following these instructions](CONTRIBU
|
||||
|
||||
- [Kingkor Roy Tirtho](https://github.com/KRTirtho) - The Founder, Maintainer and Lead Developer
|
||||
- [Owen Connor](https://github.com/owencz1998) - The Cool Discord Moderator
|
||||
- [Piotr Rogowski](https://github.com/karniv00l) - The MacOS Developer
|
||||
- [RaptaG](https://github.com/RaptaG) - The GitHub Moderator and Community Manager
|
||||
- [Piotr Rogowski](https://github.com/karniv00l) - The MacOS Developer
|
||||
- [Rusty Apple](https://github.com/RustyApple) - The Mysterious Unknown Guy
|
||||
|
||||
## 💼 License
|
||||
|
||||
Spotube is open source and licensed under the [BSD-4-Clause](/LICENSE) License.
|
||||
|
||||
If you are concerned, feel free to [read the reason of choosing this license](https://dev.to/krtirtho/choosing-open-source-license-wisely-1m3p).
|
||||
If you are concerned, you can [read the reason of choosing this license](https://dev.to/krtirtho/choosing-open-source-license-wisely-1m3p).
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
|
@ -10,6 +10,7 @@ pkgbase = spotube-bin
|
||||
depends = libsecret
|
||||
depends = jsoncpp
|
||||
depends = libnotify
|
||||
depends = networkmanager
|
||||
source = https://github.com/KRTirtho/spotube/releases/download/v2.3.0/Spotube-linux-x86_64.tar.xz
|
||||
md5sums = 8cd6a7385c5c75d203dccd762f1d63ec
|
||||
|
||||
|
@ -8,7 +8,7 @@ arch=(x86_64)
|
||||
url="https://github.com/KRTirtho/spotube/"
|
||||
license=('BSD-4-Clause')
|
||||
groups=()
|
||||
depends=('mpv' 'libappindicator-gtk3' 'libsecret' 'jsoncpp' 'libnotify')
|
||||
depends=('mpv' 'libappindicator-gtk3' 'libsecret' 'jsoncpp' 'libnotify' 'networkmanager')
|
||||
makedepends=()
|
||||
checkdepends=()
|
||||
optdepends=()
|
||||
|
@ -9,7 +9,7 @@ class ISOLanguageName {
|
||||
}
|
||||
|
||||
// Uncomment the languages as we add support for them
|
||||
// Currently supported: bn,en,fr,hi
|
||||
// Currently supported: bn,en,fr,hi,zh
|
||||
abstract class LanguageLocals {
|
||||
static final Map isoLangs = {
|
||||
// "ab": const ISOLanguageName(
|
||||
@ -128,10 +128,10 @@ abstract class LanguageLocals {
|
||||
// name: "Chichewa",
|
||||
// nativeName: "chiCheŵa",
|
||||
// ),
|
||||
// "zh": const ISOLanguageName(
|
||||
// name: "Chinese",
|
||||
// nativeName: "汉语",
|
||||
// ),
|
||||
"zh": const ISOLanguageName(
|
||||
name: "Simplified Chinese",
|
||||
nativeName: "简体中文",
|
||||
),
|
||||
// "cv": const ISOLanguageName(
|
||||
// name: "Chuvash",
|
||||
// nativeName: "чӑваш чӗлхи",
|
||||
@ -600,10 +600,10 @@ abstract class LanguageLocals {
|
||||
// name: "Southern Sotho",
|
||||
// nativeName: "Sesotho",
|
||||
// ),
|
||||
// "es": const ISOLanguageName(
|
||||
// name: "Spanish; Castilian",
|
||||
// nativeName: "español, castellano",
|
||||
// ),
|
||||
"es": const ISOLanguageName(
|
||||
name: "Spanish",
|
||||
nativeName: "español",
|
||||
),
|
||||
// "su": const ISOLanguageName(
|
||||
// name: "Sundanese",
|
||||
// nativeName: "Basa Sunda",
|
||||
|
@ -38,8 +38,8 @@ class TokenLoginForm extends HookConsumerWidget {
|
||||
TextField(
|
||||
controller: keyCodeController,
|
||||
decoration: InputDecoration(
|
||||
hintText: context.l10n.spotify_cookie("\"sp_key\""),
|
||||
labelText: context.l10n.cookie_name_cookie("sp_key"),
|
||||
hintText: context.l10n.spotify_cookie("\"sp_key (or sp_gaid)\""),
|
||||
labelText: context.l10n.cookie_name_cookie("sp_key (or sp_gaid)"),
|
||||
),
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
),
|
||||
|
@ -40,7 +40,7 @@ class RecommendationAttributeDials extends HookWidget {
|
||||
children: [
|
||||
Text(context.l10n.min, style: labelStyle),
|
||||
Expanded(
|
||||
child: Slider.adaptive(
|
||||
child: Slider(
|
||||
value: values.min / base,
|
||||
min: 0,
|
||||
max: 1,
|
||||
@ -58,7 +58,7 @@ class RecommendationAttributeDials extends HookWidget {
|
||||
children: [
|
||||
Text(context.l10n.target, style: labelStyle),
|
||||
Expanded(
|
||||
child: Slider.adaptive(
|
||||
child: Slider(
|
||||
value: values.target / base,
|
||||
min: 0,
|
||||
max: 1,
|
||||
@ -76,7 +76,7 @@ class RecommendationAttributeDials extends HookWidget {
|
||||
children: [
|
||||
Text(context.l10n.max, style: labelStyle),
|
||||
Expanded(
|
||||
child: Slider.adaptive(
|
||||
child: Slider(
|
||||
value: values.max / base,
|
||||
min: 0,
|
||||
max: 1,
|
||||
|
@ -128,7 +128,7 @@ class PlayerActions extends HookConsumerWidget {
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
|
@ -48,7 +48,6 @@ class PlayerControls extends HookConsumerWidget {
|
||||
|
||||
final playing =
|
||||
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
||||
final buffering = useStream(audioPlayer.bufferingStream).data ?? true;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final isDominantColorDark = ThemeData.estimateBrightnessForColor(
|
||||
@ -89,215 +88,208 @@ class PlayerControls extends HookConsumerWidget {
|
||||
iconSize: compact ? 18 : 24,
|
||||
);
|
||||
|
||||
return RepaintBoundary(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
if (focusNode.canRequestFocus) {
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
},
|
||||
child: FocusableActionDetector(
|
||||
focusNode: focusNode,
|
||||
shortcuts: shortcuts,
|
||||
actions: actions,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 600),
|
||||
child: Column(
|
||||
children: [
|
||||
if (!compact)
|
||||
HookBuilder(
|
||||
builder: (context) {
|
||||
final (
|
||||
:bufferProgress,
|
||||
:duration,
|
||||
:position,
|
||||
:progressStatic
|
||||
) = useProgress(ref);
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
if (focusNode.canRequestFocus) {
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
},
|
||||
child: FocusableActionDetector(
|
||||
focusNode: focusNode,
|
||||
shortcuts: shortcuts,
|
||||
actions: actions,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 600),
|
||||
child: Column(
|
||||
children: [
|
||||
if (!compact)
|
||||
HookBuilder(
|
||||
builder: (context) {
|
||||
final (
|
||||
:bufferProgress,
|
||||
:duration,
|
||||
:position,
|
||||
:progressStatic
|
||||
) = useProgress(ref);
|
||||
|
||||
final totalMinutes = PrimitiveUtils.zeroPadNumStr(
|
||||
duration.inMinutes.remainder(60),
|
||||
);
|
||||
final totalSeconds = PrimitiveUtils.zeroPadNumStr(
|
||||
duration.inSeconds.remainder(60),
|
||||
);
|
||||
final currentMinutes = PrimitiveUtils.zeroPadNumStr(
|
||||
position.inMinutes.remainder(60),
|
||||
);
|
||||
final currentSeconds = PrimitiveUtils.zeroPadNumStr(
|
||||
position.inSeconds.remainder(60),
|
||||
);
|
||||
final totalMinutes = PrimitiveUtils.zeroPadNumStr(
|
||||
duration.inMinutes.remainder(60),
|
||||
);
|
||||
final totalSeconds = PrimitiveUtils.zeroPadNumStr(
|
||||
duration.inSeconds.remainder(60),
|
||||
);
|
||||
final currentMinutes = PrimitiveUtils.zeroPadNumStr(
|
||||
position.inMinutes.remainder(60),
|
||||
);
|
||||
final currentSeconds = PrimitiveUtils.zeroPadNumStr(
|
||||
position.inSeconds.remainder(60),
|
||||
);
|
||||
|
||||
final progress = useState<num>(
|
||||
useMemoized(() => progressStatic, []),
|
||||
);
|
||||
final progress = useState<num>(
|
||||
useMemoized(() => progressStatic, []),
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
progress.value = progressStatic;
|
||||
return null;
|
||||
}, [progressStatic]);
|
||||
useEffect(() {
|
||||
progress.value = progressStatic;
|
||||
return null;
|
||||
}, [progressStatic]);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Tooltip(
|
||||
message: context.l10n.slide_to_seek,
|
||||
child: Slider.adaptive(
|
||||
// cannot divide by zero
|
||||
// there's an edge case for value being bigger
|
||||
// than total duration. Keeping it resolved
|
||||
value: progress.value.toDouble(),
|
||||
secondaryTrackValue: bufferProgress,
|
||||
onChanged:
|
||||
playlist.isFetching == true || buffering
|
||||
? null
|
||||
: (v) {
|
||||
progress.value = v;
|
||||
},
|
||||
onChangeEnd: (value) async {
|
||||
await audioPlayer.seek(
|
||||
Duration(
|
||||
seconds:
|
||||
(value * duration.inSeconds).toInt(),
|
||||
),
|
||||
);
|
||||
},
|
||||
activeColor: sliderColor,
|
||||
secondaryActiveColor:
|
||||
sliderColor.withOpacity(0.2),
|
||||
inactiveColor: sliderColor.withOpacity(0.15),
|
||||
return Column(
|
||||
children: [
|
||||
Tooltip(
|
||||
message: context.l10n.slide_to_seek,
|
||||
child: Slider(
|
||||
// cannot divide by zero
|
||||
// there's an edge case for value being bigger
|
||||
// than total duration. Keeping it resolved
|
||||
value: progress.value.toDouble(),
|
||||
secondaryTrackValue: bufferProgress,
|
||||
onChanged: playlist.isFetching == true
|
||||
? null
|
||||
: (v) {
|
||||
progress.value = v;
|
||||
},
|
||||
onChangeEnd: (value) async {
|
||||
await audioPlayer.seek(
|
||||
Duration(
|
||||
seconds: (value * duration.inSeconds).toInt(),
|
||||
),
|
||||
);
|
||||
},
|
||||
activeColor: sliderColor,
|
||||
secondaryActiveColor: sliderColor.withOpacity(0.2),
|
||||
inactiveColor: sliderColor.withOpacity(0.15),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
),
|
||||
child: DefaultTextStyle(
|
||||
style: theme.textTheme.bodySmall!.copyWith(
|
||||
color: palette?.dominantColor?.bodyTextColor,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("$currentMinutes:$currentSeconds"),
|
||||
Text("$totalMinutes:$totalSeconds"),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
),
|
||||
child: DefaultTextStyle(
|
||||
style: theme.textTheme.bodySmall!.copyWith(
|
||||
color: palette?.dominantColor?.bodyTextColor,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("$currentMinutes:$currentSeconds"),
|
||||
Text("$totalMinutes:$totalSeconds"),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
StreamBuilder<bool>(
|
||||
stream: audioPlayer.shuffledStream,
|
||||
builder: (context, snapshot) {
|
||||
final shuffled = snapshot.data ?? false;
|
||||
return IconButton(
|
||||
tooltip: shuffled
|
||||
? context.l10n.unshuffle_playlist
|
||||
: context.l10n.shuffle_playlist,
|
||||
icon: const Icon(SpotubeIcons.shuffle),
|
||||
style: shuffled ? activeButtonStyle : buttonStyle,
|
||||
onPressed: playlist.isFetching == true || buffering
|
||||
? null
|
||||
: () {
|
||||
if (shuffled) {
|
||||
audioPlayer.setShuffle(false);
|
||||
} else {
|
||||
audioPlayer.setShuffle(true);
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
IconButton(
|
||||
tooltip: context.l10n.previous_track,
|
||||
icon: const Icon(SpotubeIcons.skipBack),
|
||||
style: buttonStyle,
|
||||
onPressed: playlist.isFetching == true || buffering
|
||||
? null
|
||||
: playlistNotifier.previous,
|
||||
),
|
||||
IconButton(
|
||||
tooltip: playing
|
||||
? context.l10n.pause_playback
|
||||
: context.l10n.resume_playback,
|
||||
icon: playlist.isFetching == true
|
||||
? SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: accentColor?.titleTextColor ??
|
||||
theme.colorScheme.onPrimary,
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
playing ? SpotubeIcons.pause : SpotubeIcons.play,
|
||||
),
|
||||
style: resumePauseStyle,
|
||||
onPressed: playlist.isFetching == true
|
||||
? null
|
||||
: Actions.handler<PlayPauseIntent>(
|
||||
context,
|
||||
PlayPauseIntent(ref),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: context.l10n.next_track,
|
||||
icon: const Icon(SpotubeIcons.skipForward),
|
||||
style: buttonStyle,
|
||||
onPressed: playlist.isFetching == true || buffering
|
||||
? null
|
||||
: playlistNotifier.next,
|
||||
),
|
||||
StreamBuilder<PlaybackLoopMode>(
|
||||
stream: audioPlayer.loopModeStream,
|
||||
builder: (context, snapshot) {
|
||||
final loopMode =
|
||||
snapshot.data ?? PlaybackLoopMode.none;
|
||||
return IconButton(
|
||||
tooltip: loopMode == PlaybackLoopMode.one
|
||||
? context.l10n.loop_track
|
||||
: loopMode == PlaybackLoopMode.all
|
||||
? context.l10n.repeat_playlist
|
||||
: null,
|
||||
icon: Icon(
|
||||
loopMode == PlaybackLoopMode.one
|
||||
? SpotubeIcons.repeatOne
|
||||
: SpotubeIcons.repeat,
|
||||
),
|
||||
style: loopMode == PlaybackLoopMode.one ||
|
||||
loopMode == PlaybackLoopMode.all
|
||||
? activeButtonStyle
|
||||
: buttonStyle,
|
||||
onPressed: playlist.isFetching == true || buffering
|
||||
? null
|
||||
: () async {
|
||||
switch (await audioPlayer.loopMode) {
|
||||
case PlaybackLoopMode.all:
|
||||
audioPlayer
|
||||
.setLoopMode(PlaybackLoopMode.one);
|
||||
break;
|
||||
case PlaybackLoopMode.one:
|
||||
audioPlayer
|
||||
.setLoopMode(PlaybackLoopMode.none);
|
||||
break;
|
||||
case PlaybackLoopMode.none:
|
||||
audioPlayer
|
||||
.setLoopMode(PlaybackLoopMode.all);
|
||||
break;
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 5)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
StreamBuilder<bool>(
|
||||
stream: audioPlayer.shuffledStream,
|
||||
builder: (context, snapshot) {
|
||||
final shuffled = snapshot.data ?? false;
|
||||
return IconButton(
|
||||
tooltip: shuffled
|
||||
? context.l10n.unshuffle_playlist
|
||||
: context.l10n.shuffle_playlist,
|
||||
icon: const Icon(SpotubeIcons.shuffle),
|
||||
style: shuffled ? activeButtonStyle : buttonStyle,
|
||||
onPressed: playlist.isFetching == true
|
||||
? null
|
||||
: () {
|
||||
if (shuffled) {
|
||||
audioPlayer.setShuffle(false);
|
||||
} else {
|
||||
audioPlayer.setShuffle(true);
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
IconButton(
|
||||
tooltip: context.l10n.previous_track,
|
||||
icon: const Icon(SpotubeIcons.skipBack),
|
||||
style: buttonStyle,
|
||||
onPressed: playlist.isFetching == true
|
||||
? null
|
||||
: playlistNotifier.previous,
|
||||
),
|
||||
IconButton(
|
||||
tooltip: playing
|
||||
? context.l10n.pause_playback
|
||||
: context.l10n.resume_playback,
|
||||
icon: playlist.isFetching == true
|
||||
? SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: accentColor?.titleTextColor ??
|
||||
theme.colorScheme.onPrimary,
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
playing ? SpotubeIcons.pause : SpotubeIcons.play,
|
||||
),
|
||||
style: resumePauseStyle,
|
||||
onPressed: playlist.isFetching == true
|
||||
? null
|
||||
: Actions.handler<PlayPauseIntent>(
|
||||
context,
|
||||
PlayPauseIntent(ref),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: context.l10n.next_track,
|
||||
icon: const Icon(SpotubeIcons.skipForward),
|
||||
style: buttonStyle,
|
||||
onPressed: playlist.isFetching == true
|
||||
? null
|
||||
: playlistNotifier.next,
|
||||
),
|
||||
StreamBuilder<PlaybackLoopMode>(
|
||||
stream: audioPlayer.loopModeStream,
|
||||
builder: (context, snapshot) {
|
||||
final loopMode = snapshot.data ?? PlaybackLoopMode.none;
|
||||
return IconButton(
|
||||
tooltip: loopMode == PlaybackLoopMode.one
|
||||
? context.l10n.loop_track
|
||||
: loopMode == PlaybackLoopMode.all
|
||||
? context.l10n.repeat_playlist
|
||||
: null,
|
||||
icon: Icon(
|
||||
loopMode == PlaybackLoopMode.one
|
||||
? SpotubeIcons.repeatOne
|
||||
: SpotubeIcons.repeat,
|
||||
),
|
||||
style: loopMode == PlaybackLoopMode.one ||
|
||||
loopMode == PlaybackLoopMode.all
|
||||
? activeButtonStyle
|
||||
: buttonStyle,
|
||||
onPressed: playlist.isFetching == true
|
||||
? null
|
||||
: () async {
|
||||
switch (await audioPlayer.loopMode) {
|
||||
case PlaybackLoopMode.all:
|
||||
audioPlayer
|
||||
.setLoopMode(PlaybackLoopMode.one);
|
||||
break;
|
||||
case PlaybackLoopMode.one:
|
||||
audioPlayer
|
||||
.setLoopMode(PlaybackLoopMode.none);
|
||||
break;
|
||||
case PlaybackLoopMode.none:
|
||||
audioPlayer
|
||||
.setLoopMode(PlaybackLoopMode.all);
|
||||
break;
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -62,99 +62,95 @@ class PlayerOverlay extends HookConsumerWidget {
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
opacity: canShow ? 1 : 0,
|
||||
child: RepaintBoundary(
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
HookBuilder(
|
||||
builder: (context) {
|
||||
final progress = useProgress(ref);
|
||||
// animated
|
||||
return TweenAnimationBuilder<double>(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
tween: Tween<double>(
|
||||
begin: 0,
|
||||
end: progress.progressStatic,
|
||||
),
|
||||
builder: (context, value, child) {
|
||||
return LinearProgressIndicator(
|
||||
value: value,
|
||||
minHeight: 2,
|
||||
backgroundColor: Colors.transparent,
|
||||
valueColor: AlwaysStoppedAnimation(
|
||||
theme.colorScheme.primary,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () =>
|
||||
GoRouter.of(context).push("/player"),
|
||||
child: PlayerTrackDetails(
|
||||
albumArt: albumArt,
|
||||
color: textColor,
|
||||
),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
HookBuilder(
|
||||
builder: (context) {
|
||||
final progress = useProgress(ref);
|
||||
// animated
|
||||
return TweenAnimationBuilder<double>(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
tween: Tween<double>(
|
||||
begin: 0,
|
||||
end: progress.progressStatic,
|
||||
),
|
||||
builder: (context, value, child) {
|
||||
return LinearProgressIndicator(
|
||||
value: value,
|
||||
minHeight: 2,
|
||||
backgroundColor: Colors.transparent,
|
||||
valueColor: AlwaysStoppedAnimation(
|
||||
theme.colorScheme.primary,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () =>
|
||||
GoRouter.of(context).push("/player"),
|
||||
child: PlayerTrackDetails(
|
||||
albumArt: albumArt,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
SpotubeIcons.skipBack,
|
||||
color: textColor,
|
||||
),
|
||||
onPressed: playlistNotifier.previous,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
SpotubeIcons.skipBack,
|
||||
color: textColor,
|
||||
),
|
||||
Consumer(
|
||||
builder: (context, ref, _) {
|
||||
return IconButton(
|
||||
icon: playlist.isFetching
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child:
|
||||
CircularProgressIndicator(),
|
||||
)
|
||||
: Icon(
|
||||
playing
|
||||
? SpotubeIcons.pause
|
||||
: SpotubeIcons.play,
|
||||
color: textColor,
|
||||
),
|
||||
onPressed:
|
||||
Actions.handler<PlayPauseIntent>(
|
||||
context,
|
||||
PlayPauseIntent(ref),
|
||||
),
|
||||
);
|
||||
},
|
||||
onPressed: playlistNotifier.previous,
|
||||
),
|
||||
Consumer(
|
||||
builder: (context, ref, _) {
|
||||
return IconButton(
|
||||
icon: playlist.isFetching
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: Icon(
|
||||
playing
|
||||
? SpotubeIcons.pause
|
||||
: SpotubeIcons.play,
|
||||
color: textColor,
|
||||
),
|
||||
onPressed: Actions.handler<PlayPauseIntent>(
|
||||
context,
|
||||
PlayPauseIntent(ref),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
SpotubeIcons.skipForward,
|
||||
color: textColor,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
SpotubeIcons.skipForward,
|
||||
color: textColor,
|
||||
),
|
||||
onPressed: playlistNotifier.next,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: playlistNotifier.next,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -122,112 +122,114 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
||||
]);
|
||||
|
||||
var mediaQuery = MediaQuery.of(context);
|
||||
return BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: 12.0,
|
||||
sigmaY: 12.0,
|
||||
),
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
height: isSearching.value && mediaQuery.smAndDown
|
||||
? mediaQuery.size.height
|
||||
: mediaQuery.size.height * .6,
|
||||
margin: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: borderRadius,
|
||||
color: theme.scaffoldBackgroundColor.withOpacity(.3),
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: !isSearching.value
|
||||
? Text(
|
||||
context.l10n.alternative_track_sources,
|
||||
style: theme.textTheme.headlineSmall,
|
||||
)
|
||||
: TextField(
|
||||
autofocus: true,
|
||||
controller: searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: context.l10n.search,
|
||||
hintStyle: theme.textTheme.headlineSmall,
|
||||
border: InputBorder.none,
|
||||
),
|
||||
style: theme.textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
automaticallyImplyLeading: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
actions: [
|
||||
if (!isSearching.value)
|
||||
IconButton(
|
||||
icon: const Icon(SpotubeIcons.search, size: 18),
|
||||
onPressed: () {
|
||||
isSearching.value = true;
|
||||
},
|
||||
)
|
||||
else ...[
|
||||
if (preferences.youtubeApiType == YoutubeApiType.piped)
|
||||
PopupMenuButton(
|
||||
icon: const Icon(SpotubeIcons.filter, size: 18),
|
||||
onSelected: (SearchMode mode) {
|
||||
searchMode.value = mode;
|
||||
},
|
||||
initialValue: searchMode.value,
|
||||
itemBuilder: (context) => SearchMode.values
|
||||
.map(
|
||||
(e) => PopupMenuItem(
|
||||
value: e,
|
||||
child: Text(e.label),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(SpotubeIcons.close, size: 18),
|
||||
onPressed: () {
|
||||
isSearching.value = false;
|
||||
},
|
||||
),
|
||||
]
|
||||
],
|
||||
return SafeArea(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: 12.0,
|
||||
sigmaY: 12.0,
|
||||
),
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
height: isSearching.value && mediaQuery.smAndDown
|
||||
? mediaQuery.size.height
|
||||
: mediaQuery.size.height * .6,
|
||||
margin: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: borderRadius,
|
||||
color: theme.scaffoldBackgroundColor.withOpacity(.3),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
transitionBuilder: (child, animation) =>
|
||||
FadeTransition(opacity: animation, child: child),
|
||||
child: switch (isSearching.value) {
|
||||
false => ListView.builder(
|
||||
itemCount: siblings.length,
|
||||
itemBuilder: (context, index) =>
|
||||
itemBuilder(siblings[index]),
|
||||
),
|
||||
true => FutureBuilder(
|
||||
future: searchRequest,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Text(snapshot.error.toString()),
|
||||
);
|
||||
} else if (!snapshot.hasData) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: snapshot.data!.length,
|
||||
itemBuilder: (context, index) =>
|
||||
itemBuilder(snapshot.data![index]),
|
||||
);
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: !isSearching.value
|
||||
? Text(
|
||||
context.l10n.alternative_track_sources,
|
||||
style: theme.textTheme.headlineSmall,
|
||||
)
|
||||
: TextField(
|
||||
autofocus: true,
|
||||
controller: searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: context.l10n.search,
|
||||
hintStyle: theme.textTheme.headlineSmall,
|
||||
border: InputBorder.none,
|
||||
),
|
||||
style: theme.textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
automaticallyImplyLeading: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
actions: [
|
||||
if (!isSearching.value)
|
||||
IconButton(
|
||||
icon: const Icon(SpotubeIcons.search, size: 18),
|
||||
onPressed: () {
|
||||
isSearching.value = true;
|
||||
},
|
||||
)
|
||||
else ...[
|
||||
if (preferences.youtubeApiType == YoutubeApiType.piped)
|
||||
PopupMenuButton(
|
||||
icon: const Icon(SpotubeIcons.filter, size: 18),
|
||||
onSelected: (SearchMode mode) {
|
||||
searchMode.value = mode;
|
||||
},
|
||||
initialValue: searchMode.value,
|
||||
itemBuilder: (context) => SearchMode.values
|
||||
.map(
|
||||
(e) => PopupMenuItem(
|
||||
value: e,
|
||||
child: Text(e.label),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(SpotubeIcons.close, size: 18),
|
||||
onPressed: () {
|
||||
isSearching.value = false;
|
||||
},
|
||||
),
|
||||
},
|
||||
]
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
transitionBuilder: (child, animation) =>
|
||||
FadeTransition(opacity: animation, child: child),
|
||||
child: switch (isSearching.value) {
|
||||
false => ListView.builder(
|
||||
itemCount: siblings.length,
|
||||
itemBuilder: (context, index) =>
|
||||
itemBuilder(siblings[index]),
|
||||
),
|
||||
true => FutureBuilder(
|
||||
future: searchRequest,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Text(snapshot.error.toString()),
|
||||
);
|
||||
} else if (!snapshot.hasData) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: snapshot.data!.length,
|
||||
itemBuilder: (context, index) =>
|
||||
itemBuilder(snapshot.data![index]),
|
||||
);
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -29,7 +29,7 @@ class VolumeSlider extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Slider.adaptive(
|
||||
child: Slider(
|
||||
min: 0,
|
||||
max: 1,
|
||||
value: volume,
|
||||
|
@ -72,7 +72,9 @@ class PlaylistCard extends HookConsumerWidget {
|
||||
playlistNotifier.addCollection(playlist.id!);
|
||||
tracks.value = fetchedTracks;
|
||||
} finally {
|
||||
updating.value = false;
|
||||
if (context.mounted) {
|
||||
updating.value = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
onAddToQueuePressed: () async {
|
||||
|
@ -1,11 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
|
||||
Future<bool> showPromptDialog({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
required String message,
|
||||
String okText = "Ok",
|
||||
String cancelText = "Cancel",
|
||||
String? cancelText = "Cancel",
|
||||
}) async {
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
@ -14,12 +15,15 @@ Future<bool> showPromptDialog({
|
||||
title: Text(title),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
OutlinedButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text(cancelText),
|
||||
),
|
||||
if (cancelText != null)
|
||||
OutlinedButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text(
|
||||
cancelText == "Cancel" ? context.l10n.cancel : cancelText,
|
||||
),
|
||||
),
|
||||
FilledButton(
|
||||
child: Text(okText),
|
||||
child: Text(okText == "Ok" ? context.l10n.ok : okText),
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
),
|
||||
],
|
||||
|
@ -9,6 +9,15 @@ import 'package:spotube/hooks/use_breakpoint_value.dart';
|
||||
import 'package:spotube/hooks/use_brightness_value.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
final htmlTagRegexp = RegExp(r"<[^>]*>", caseSensitive: true);
|
||||
|
||||
String? useDescription(String? description) {
|
||||
return useMemoized(() {
|
||||
if (description == null) return null;
|
||||
return description.replaceAll(htmlTagRegexp, '');
|
||||
}, [description]);
|
||||
}
|
||||
|
||||
class PlaybuttonCard extends HookWidget {
|
||||
final void Function()? onTap;
|
||||
final void Function()? onPlaybuttonPressed;
|
||||
@ -40,19 +49,17 @@ class PlaybuttonCard extends HookWidget {
|
||||
final radius = BorderRadius.circular(15);
|
||||
|
||||
final double size = useBreakpointValue<double>(
|
||||
xs: 130,
|
||||
sm: 130,
|
||||
md: 150,
|
||||
others: 170,
|
||||
) ??
|
||||
170;
|
||||
xs: 130,
|
||||
sm: 130,
|
||||
md: 150,
|
||||
others: 170,
|
||||
);
|
||||
|
||||
final end = useBreakpointValue<double>(
|
||||
xs: 15,
|
||||
sm: 15,
|
||||
others: 20,
|
||||
) ??
|
||||
20;
|
||||
xs: 15,
|
||||
sm: 15,
|
||||
others: 20,
|
||||
);
|
||||
|
||||
final textsHeight = useState(
|
||||
(textsKey.currentContext?.findRenderObject() as RenderBox?)
|
||||
@ -61,6 +68,8 @@ class PlaybuttonCard extends HookWidget {
|
||||
110.00,
|
||||
);
|
||||
|
||||
final cleanDescription = useDescription(description);
|
||||
|
||||
useEffect(() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
textsHeight.value =
|
||||
@ -123,11 +132,11 @@ class PlaybuttonCard extends HookWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (description != null)
|
||||
if (cleanDescription != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: AutoSizeText(
|
||||
description!,
|
||||
cleanDescription,
|
||||
maxLines: 2,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color:
|
||||
|
@ -9,6 +9,7 @@ import 'package:spotube/collections/assets.gen.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/album/album_card.dart';
|
||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||
import 'package:spotube/components/shared/playbutton_card.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
|
||||
@ -42,6 +43,8 @@ class TrackCollectionHeading<T> extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final cleanDescription = useDescription(description);
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constrains) {
|
||||
return DecoratedBox(
|
||||
@ -111,13 +114,13 @@ class TrackCollectionHeading<T> extends HookConsumerWidget {
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
if (description != null)
|
||||
if (cleanDescription != null)
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: constrains.mdAndDown ? 400 : 300,
|
||||
),
|
||||
child: Text(
|
||||
description!,
|
||||
cleanDescription,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.fade,
|
||||
|
@ -92,7 +92,7 @@ class TrackTile extends HookConsumerWidget {
|
||||
else if (constrains.smAndDown)
|
||||
const SizedBox(width: 16),
|
||||
if (onChanged != null)
|
||||
Checkbox.adaptive(
|
||||
Checkbox(
|
||||
value: selected,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
|
@ -115,7 +115,7 @@ class TracksTableView extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
child: showCheck.value
|
||||
? Checkbox.adaptive(
|
||||
? Checkbox(
|
||||
value: selected.value.length == sortedTracks.length,
|
||||
onChanged: (checked) {
|
||||
if (!showCheck.value) showCheck.value = true;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import 'package:async/async.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
|
||||
({
|
||||
@ -9,42 +9,63 @@ import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
Duration duration,
|
||||
double bufferProgress
|
||||
}) useProgress(WidgetRef ref) {
|
||||
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||
|
||||
final bufferProgress =
|
||||
useStream(audioPlayer.bufferedPositionStream).data?.inSeconds ?? 0;
|
||||
|
||||
// Duration future is needed for getting the duration of the song
|
||||
// as stream can be null when no event occurs (Mostly needed for android)
|
||||
final durationFuture = useFuture(audioPlayer.duration);
|
||||
final duration = useStream(audioPlayer.durationStream).data ??
|
||||
durationFuture.data ??
|
||||
Duration.zero;
|
||||
final duration = useState(Duration.zero);
|
||||
final position = useState(Duration.zero);
|
||||
|
||||
final positionFuture = useFuture(audioPlayer.position);
|
||||
final position = useState<Duration>(positionFuture.data ?? Duration.zero);
|
||||
|
||||
final sliderMax = duration.inSeconds;
|
||||
final sliderMax = duration.value.inSeconds;
|
||||
final sliderValue = position.value.inSeconds;
|
||||
|
||||
useEffect(() {
|
||||
final durationOperation =
|
||||
CancelableOperation.fromFuture(audioPlayer.duration);
|
||||
durationOperation.then((value) {
|
||||
if (value != null) {
|
||||
duration.value = value;
|
||||
}
|
||||
});
|
||||
|
||||
final durationSubscription = audioPlayer.durationStream.listen((event) {
|
||||
duration.value = event;
|
||||
});
|
||||
|
||||
final positionOperation =
|
||||
CancelableOperation.fromFuture(audioPlayer.position);
|
||||
|
||||
positionOperation.then((value) {
|
||||
if (value != null) {
|
||||
position.value = value;
|
||||
}
|
||||
});
|
||||
|
||||
// audioPlayer.positionStream is fired every 200ms and only 1s delay is
|
||||
// enough. Thus only update the position if the difference is more than 1s
|
||||
// Reduces CPU usage
|
||||
var lastPosition = position.value;
|
||||
return audioPlayer.positionStream.listen((event) {
|
||||
|
||||
final positionSubscription = audioPlayer.positionStream.listen((event) {
|
||||
if (event.inMilliseconds > 1000 &&
|
||||
event.inMilliseconds - lastPosition.inMilliseconds < 1000) return;
|
||||
|
||||
lastPosition = event;
|
||||
position.value = event;
|
||||
}).cancel;
|
||||
});
|
||||
|
||||
return () {
|
||||
positionOperation.cancel();
|
||||
positionSubscription.cancel();
|
||||
durationOperation.cancel();
|
||||
durationSubscription.cancel();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
progressStatic:
|
||||
sliderMax == 0 || sliderValue > sliderMax ? 0 : sliderValue / sliderMax,
|
||||
position: position.value,
|
||||
duration: duration,
|
||||
duration: duration.value,
|
||||
bufferProgress: sliderMax == 0 || bufferProgress > sliderMax
|
||||
? 0
|
||||
: bufferProgress / sliderMax,
|
||||
|
@ -62,7 +62,7 @@ void useUpdateChecker(WidgetRef ref) {
|
||||
barrierColor: Colors.black26,
|
||||
builder: (context) {
|
||||
const url =
|
||||
"https://spotube.netlify.app/other-downloads/stable-downloads";
|
||||
"https://spotube.krtirtho.dev/other-downloads/stable-downloads";
|
||||
return AlertDialog(
|
||||
title: const Text("Spotube has an update"),
|
||||
actions: [
|
||||
|
@ -176,14 +176,15 @@
|
||||
"step_2": "ধাপ 2",
|
||||
"step_2_steps": "১. একবার আপনি লগ ইন করলে, ব্রাউজার ডেভটুল খুলতে F12 বা মাউসের রাইট ক্লিক > \"Inspect to open Browser DevTools\" টিপুন।\n২. তারপর \"Application\" ট্যাবে যান (Chrome, Edge, Brave etc..) অথবা \"Storage\" Tab (Firefox, Palemoon etc..)\n৩. \"Cookies \" বিভাগে যান তারপর \"https://accounts.spotify.com\" উপবিভাগে যান",
|
||||
"step_3": "ধাপ 3",
|
||||
"step_3_steps": "\"sp_dc\" এবং \"sp_key\" কুকিজের মান কপি করুন",
|
||||
"step_3_steps": "\"sp_dc\" এবং \"sp_key\" (অথবা sp_gaid) কুকিজের মান কপি করুন",
|
||||
"success_emoji": "আমরা সফল🥳",
|
||||
"success_message": "এখন আপনি সফলভাবে আপনার Spotify অ্যাকাউন্ট দিয়ে লগ ইন করেছেন। সাধুভাত আপনাকে",
|
||||
"step_4": "ধাপ 4",
|
||||
"step_4_steps": "কপি করা \"sp_dc\" এবং \"sp_key\" এর মান সংশ্লিষ্ট ফিল্ডে পেস্ট করুন",
|
||||
"step_4_steps": "কপি করা \"sp_dc\" এবং \"sp_key\" (অথবা sp_gaid) এর মান সংশ্লিষ্ট ফিল্ডে পেস্ট করুন",
|
||||
"something_went_wrong": "কিছু ভুল হয়েছে",
|
||||
"piped_instance": "Piped সার্ভার এড্রেস",
|
||||
"piped_description": "গান ম্যাচ করার জন্য ব্যবহৃত পাইপড সার্ভার\n এগুলোর মধ্যে কিছু ভাল কাজ নাও করতে পারে৷ তাই নিজ দায়িত্বে ব্যবহার করুন",
|
||||
"piped_description": "গান ম্যাচ করার জন্য ব্যবহৃত পাইপড সার্ভার",
|
||||
"piped_warning": "এগুলোর মধ্যে কিছু ভাল কাজ নাও করতে পারে৷ তাই নিজ দায়িত্বে ব্যবহার করুন",
|
||||
"generate_playlist": "প্লেলিস্ট তৈরি করুন",
|
||||
"track_exists": "ট্র্যাক {track} ইতিমধ্যে বিদ্যমান",
|
||||
"replace_downloaded_tracks": "সমস্ত ডাউনলোড করা ট্র্যাক প্রতিস্থাপন করুন",
|
||||
@ -249,5 +250,8 @@
|
||||
"developers": "ডেভেলপার",
|
||||
"not_logged_in": "আপনি লগইন করা নেই",
|
||||
"search_mode": "অনুসন্ধান মোড",
|
||||
"youtube_api_type": "API প্রকার"
|
||||
"youtube_api_type": "API প্রকার",
|
||||
"ok": "ঠিক আছে",
|
||||
"failed_to_encrypt": "এনক্রিপ্ট করা ব্যর্থ হয়েছে",
|
||||
"encryption_failed_warning": "Spotube আপনার তথ্যগুলি নিরাপদভাবে স্টোর করতে এনক্রিপশন ব্যবহার করে। কিন্তু এটি ব্যর্থ হয়েছে। তাই এটি অনিরাপদ স্টোরে ফলফল হবে\nযদি আপনি Linux ব্যবহার করেন, তবে দয়া করে নিশ্চিত হউন যে আপনার কোনও সিক্রেট-সার্ভিস gnome-keyring, kde-wallet, keepassxc ইত্যাদি ইনস্টল করা আছে"
|
||||
}
|
@ -176,14 +176,15 @@
|
||||
"step_2": "Schritt 2",
|
||||
"step_2_steps": "1. Wenn du angemeldet bist, drücke F12 oder klicke mit der rechten Maustaste > Inspektion, um die Browser-Entwicklertools zu öffnen.\n2. Gehe dann zum \"Anwendungs\"-Tab (Chrome, Edge, Brave usw.) oder zum \"Storage\"-Tab (Firefox, Palemoon usw.)\n3. Gehe zum Abschnitt \"Cookies\" und dann zum Unterabschnitt \"https://accounts.spotify.com\"",
|
||||
"step_3": "Schritt 3",
|
||||
"step_3_steps": "Kopiere die Werte der Cookies \"sp_dc\" und \"sp_key\"",
|
||||
"step_3_steps": "Kopiere die Werte der Cookies \"sp_dc\" und \"sp_key\" (oder sp_gaid)",
|
||||
"success_emoji": "Erfolg🥳",
|
||||
"success_message": "Jetzt bist du erfolgreich mit deinem Spotify-Konto angemeldet. Gut gemacht, Kumpel!",
|
||||
"step_4": "Schritt 4",
|
||||
"step_4_steps": "Füge die kopierten Werte von \"sp_dc\" und \"sp_key\" in die entsprechenden Felder ein",
|
||||
"step_4_steps": "Füge die kopierten Werte von \"sp_dc\" und \"sp_key\" (oder sp_gaid) in die entsprechenden Felder ein",
|
||||
"something_went_wrong": "Etwas ist schiefgelaufen",
|
||||
"piped_instance": "Piped-Serverinstanz",
|
||||
"piped_description": "Die Piped-Serverinstanz, die zur Titelzuordnung verwendet werden soll\nEinige von ihnen funktionieren möglicherweise nicht gut. Verwende sie also auf eigenes Risiko",
|
||||
"piped_description": "Die Piped-Serverinstanz, die zur Titelzuordnung verwendet werden soll",
|
||||
"piped_warning": "Einige von ihnen funktionieren möglicherweise nicht gut. Verwende sie also auf eigenes Risiko",
|
||||
"generate_playlist": "Playlist generieren",
|
||||
"track_exists": "Track {track} existiert bereits",
|
||||
"replace_downloaded_tracks": "Alle heruntergeladenen Titel ersetzen",
|
||||
@ -249,5 +250,8 @@
|
||||
"developers": "Entwickler",
|
||||
"not_logged_in": "Sie sind nicht angemeldet",
|
||||
"search_mode": "Suchmodus",
|
||||
"youtube_api_type": "API-Typ"
|
||||
"youtube_api_type": "API-Typ",
|
||||
"ok": "OK",
|
||||
"failed_to_encrypt": "Verschlüsselung fehlgeschlagen",
|
||||
"encryption_failed_warning": "Spotube verwendet Verschlüsselung, um Ihre Daten sicher zu speichern. Dies ist jedoch fehlgeschlagen. Daher wird es auf unsichere Speicherung zurückgreifen\nWenn Sie Linux verwenden, stellen Sie bitte sicher, dass Sie Secret-Services wie gnome-keyring, kde-wallet und keepassxc installiert haben"
|
||||
}
|
@ -176,14 +176,15 @@
|
||||
"step_2": "Step 2",
|
||||
"step_2_steps": "1. Once you're logged in, press F12 or Mouse Right Click > Inspect to Open the Browser devtools.\n2. Then go the \"Application\" Tab (Chrome, Edge, Brave etc..) or \"Storage\" Tab (Firefox, Palemoon etc..)\n3. Go to the \"Cookies\" section then the \"https://accounts.spotify.com\" subsection",
|
||||
"step_3": "Step 3",
|
||||
"step_3_steps": "Copy the values of \"sp_dc\" and \"sp_key\" Cookies",
|
||||
"step_3_steps": "Copy the values of \"sp_dc\" and \"sp_key\" (or sp_gaid) Cookies",
|
||||
"success_emoji": "Success🥳",
|
||||
"success_message": "Now you're successfully Logged In with your Spotify account. Good Job, mate!",
|
||||
"step_4": "Step 4",
|
||||
"step_4_steps": "Paste the copied \"sp_dc\" and \"sp_key\" values in the respective fields",
|
||||
"step_4_steps": "Paste the copied \"sp_dc\" and \"sp_key\" (or sp_gaid) values in the respective fields",
|
||||
"something_went_wrong": "Something went wrong",
|
||||
"piped_instance": "Piped Server Instance",
|
||||
"piped_description": "The Piped server instance to use for track matching\nSome of them might not work well. So use at your own risk",
|
||||
"piped_description": "The Piped server instance to use for track matching",
|
||||
"piped_warning": "Some of them might not work well. So use at your own risk",
|
||||
"generate_playlist": "Generate Playlist",
|
||||
"track_exists": "Track {track} already exists",
|
||||
"replace_downloaded_tracks": "Replace all downloaded tracks",
|
||||
@ -249,5 +250,8 @@
|
||||
"developers": "Developers",
|
||||
"not_logged_in": "You're not logged in",
|
||||
"search_mode": "Search Mode",
|
||||
"youtube_api_type": "API Type"
|
||||
}
|
||||
"youtube_api_type": "API Type",
|
||||
"ok": "Ok",
|
||||
"failed_to_encrypt": "Failed to encrypt",
|
||||
"encryption_failed_warning": "Spotube uses encryption to securely store your data. But failed to do so. So it'll fallback to insecure storage\nIf you're using linux, please make sure you've any secret-service (gnome-keyring, kde-wallet, keepassxc etc) installed"
|
||||
}
|
257
lib/l10n/app_es.arb
Normal file
257
lib/l10n/app_es.arb
Normal file
@ -0,0 +1,257 @@
|
||||
{
|
||||
"guest": "Invitado",
|
||||
"browse": "Explorar",
|
||||
"search": "Buscar",
|
||||
"library": "Biblioteca",
|
||||
"lyrics": "Letras",
|
||||
"settings": "Configuración",
|
||||
"genre_categories_filter": "Filtrar categorías o géneros...",
|
||||
"genre": "Género",
|
||||
"personalized": "Personalizado",
|
||||
"featured": "Destacado",
|
||||
"new_releases": "Nuevos Lanzamientos",
|
||||
"songs": "Canciones",
|
||||
"playing_track": "Reproduciendo {track}",
|
||||
"queue_clear_alert": "Esto eliminará la lista actual. Se eliminarán {track_length} canciones.\n¿Deseas continuar?",
|
||||
"load_more": "Cargar más",
|
||||
"playlists": "Listas de reproducción",
|
||||
"artists": "Artistas",
|
||||
"albums": "Álbumes",
|
||||
"tracks": "Canciones",
|
||||
"downloads": "Descargas",
|
||||
"filter_playlists": "Filtrar tus listas de reproducción...",
|
||||
"liked_tracks": "Canciones Favoritas",
|
||||
"liked_tracks_description": "Todas tus canciones favoritas",
|
||||
"create_playlist": "Crear Lista de reproducción",
|
||||
"create_a_playlist": "Crear una lista de reproducción",
|
||||
"create": "Crear",
|
||||
"cancel": "Cancelar",
|
||||
"playlist_name": "Nombre de la lista",
|
||||
"name_of_playlist": "Nombre de la lista",
|
||||
"description": "Descripción",
|
||||
"public": "Pública",
|
||||
"collaborative": "Colaborativa",
|
||||
"search_local_tracks": "Buscar canciones locales...",
|
||||
"play": "Reproducir",
|
||||
"delete": "Eliminar",
|
||||
"none": "Ninguno",
|
||||
"sort_a_z": "Ordenar de la A a la Z",
|
||||
"sort_z_a": "Ordenar de la Z a la A",
|
||||
"sort_artist": "Ordenar por Artista",
|
||||
"sort_album": "Ordenar por Álbum",
|
||||
"sort_tracks": "Ordenar Canciones",
|
||||
"currently_downloading": "Descargando en curso ({tracks_length})",
|
||||
"cancel_all": "Cancelar todo",
|
||||
"filter_artist": "Filtrar artistas...",
|
||||
"followers": "{followers} Seguidores",
|
||||
"add_artist_to_blacklist": "Agregar artista a la lista negra",
|
||||
"top_tracks": "Mejores Canciones",
|
||||
"fans_also_like": "A los fans también les gusta",
|
||||
"loading": "Cargando...",
|
||||
"artist": "Artista",
|
||||
"blacklisted": "En la lista negra",
|
||||
"following": "Siguiendo",
|
||||
"follow": "Seguir",
|
||||
"artist_url_copied": "URL del artista copiada al portapapeles",
|
||||
"added_to_queue": "Agregadas {tracks} canciones a la lista",
|
||||
"filter_albums": "Filtrar álbumes...",
|
||||
"synced": "Sincronizado",
|
||||
"plain": "Normal",
|
||||
"shuffle": "Aleatorio",
|
||||
"search_tracks": "Buscar canciones...",
|
||||
"released": "Lanzado",
|
||||
"error": "Error {error}",
|
||||
"title": "Título",
|
||||
"time": "Duración",
|
||||
"more_actions": "Más acciones",
|
||||
"download_count": "Descargas ({count})",
|
||||
"add_count_to_playlist": "Agregar ({count}) a la lista",
|
||||
"add_count_to_queue": "Agregar ({count}) a la lista",
|
||||
"play_count_next": "Reproducir ({count}) a continuación",
|
||||
"album": "Álbum",
|
||||
"copied_to_clipboard": "{data} copiado al portapapeles",
|
||||
"add_to_following_playlists": "Agregar {track} a las listas de reproducción siguientes",
|
||||
"add": "Agregar",
|
||||
"added_track_to_queue": "{track} agregada a la lista",
|
||||
"add_to_queue": "Agregar a la lista",
|
||||
"track_will_play_next": "{track} se reproducirá a continuación",
|
||||
"play_next": "Reproducir a continuación",
|
||||
"removed_track_from_queue": "{track} eliminada de la lista",
|
||||
"remove_from_queue": "Eliminar de la lista",
|
||||
"remove_from_favorites": "Eliminar de favoritos",
|
||||
"save_as_favorite": "Guardar como favorito",
|
||||
"add_to_playlist": "Agregar a la lista",
|
||||
"remove_from_playlist": "Eliminar de la lista",
|
||||
"add_to_blacklist": "Agregar a la lista negra",
|
||||
"remove_from_blacklist": "Eliminar de la lista negra",
|
||||
"share": "Compartir",
|
||||
"mini_player": "Reproductor Mini",
|
||||
"slide_to_seek": "Desliza para buscar adelante o atrás",
|
||||
"shuffle_playlist": "Reproducir lista en orden aleatorio",
|
||||
"unshuffle_playlist": "Desactivar reproducción aleatoria",
|
||||
"previous_track": "Pista anterior",
|
||||
"next_track": "Pista siguiente",
|
||||
"pause_playback": "Pausar reproducción",
|
||||
"resume_playback": "Reanudar reproducción",
|
||||
"loop_track": "Repetir pista",
|
||||
"repeat_playlist": "Repetir lista",
|
||||
"queue": "Lista",
|
||||
"alternative_track_sources": "Fuentes alternativas de canciones",
|
||||
"download_track": "Descargar canción",
|
||||
"tracks_in_queue": "{tracks} canciones en la lista",
|
||||
"clear_all": "Limpiar todo",
|
||||
"show_hide_ui_on_hover": "Mostrar/Ocultar interfaz al pasar el cursor",
|
||||
"always_on_top": "Siempre visible",
|
||||
"exit_mini_player": "Salir del reproductor mini",
|
||||
"download_location": "Ubicación de descargas",
|
||||
"account": "Cuenta",
|
||||
"login_with_spotify": "Iniciar sesión con tu cuenta de Spotify",
|
||||
"connect_with_spotify": "Conectar con Spotify",
|
||||
"logout": "Cerrar sesión",
|
||||
"logout_of_this_account": "Cerrar sesión de esta cuenta",
|
||||
"language_region": "Idioma y Región",
|
||||
"language": "Idioma",
|
||||
"system_default": "Predeterminado del sistema",
|
||||
"market_place_region": "Región de la tienda",
|
||||
"recommendation_country": "País de recomendación",
|
||||
"appearance": "Apariencia",
|
||||
"layout_mode": "Modo de diseño",
|
||||
"override_layout_settings": "Anular la configuración del modo de diseño responsive",
|
||||
"adaptive": "Adaptable",
|
||||
"compact": "Compacto",
|
||||
"extended": "Extendido",
|
||||
"theme": "Tema",
|
||||
"dark": "Oscuro",
|
||||
"light": "Claro",
|
||||
"system": "Sistema",
|
||||
"accent_color": "Color de acento",
|
||||
"sync_album_color": "Sincronizar color del álbum",
|
||||
"sync_album_color_description": "Usa el color dominante del arte del álbum como color de acento",
|
||||
"playback": "Reproducción",
|
||||
"audio_quality": "Calidad de audio",
|
||||
"high": "Alta",
|
||||
"low": "Baja",
|
||||
"pre_download_play": "Pre-descargar y reproducir",
|
||||
"pre_download_play_description": "En lugar de transmitir audio, descarga bytes y reproduce en su lugar (recomendado para usuarios con mayor ancho de banda)",
|
||||
"skip_non_music": "Omitir segmentos que no son música (SponsorBlock)",
|
||||
"blacklist_description": "Canciones y artistas en la lista negra",
|
||||
"wait_for_download_to_finish": "Por favor, espera a que termine la descarga actual",
|
||||
"download_lyrics": "Descargar letras junto con las canciones",
|
||||
"desktop": "Escritorio",
|
||||
"close_behavior": "Comportamiento al cerrar",
|
||||
"close": "Cerrar",
|
||||
"minimize_to_tray": "Minimizar en la bandeja del sistema",
|
||||
"show_tray_icon": "Mostrar icono en la bandeja del sistema",
|
||||
"about": "Acerca de",
|
||||
"u_love_spotube": "Sabemos que te encanta Spotube",
|
||||
"check_for_updates": "Buscar actualizaciones",
|
||||
"about_spotube": "Acerca de Spotube",
|
||||
"blacklist": "Lista negra",
|
||||
"please_sponsor": "Por favor, apoya/dona",
|
||||
"spotube_description": "Spotube, un cliente ligero, multiplataforma y gratuito de Spotify",
|
||||
"version": "Versión",
|
||||
"build_number": "Número de compilación",
|
||||
"founder": "Fundador",
|
||||
"repository": "Repositorio",
|
||||
"bug_issues": "Errores y problemas",
|
||||
"made_with": "Hecho con ❤️ en Bangladesh🇧🇩",
|
||||
"kingkor_roy_tirtho": "Kingkor Roy Tirtho",
|
||||
"copyright": "© 2021-{current_year} Kingkor Roy Tirtho",
|
||||
"license": "Licencia",
|
||||
"add_spotify_credentials": "Agrega tus credenciales de Spotify para comenzar",
|
||||
"credentials_will_not_be_shared_disclaimer": "No te preocupes, tus credenciales no serán recopiladas ni compartidas con nadie",
|
||||
"know_how_to_login": "¿No sabes cómo hacerlo?",
|
||||
"follow_step_by_step_guide": "Sigue la guía paso a paso",
|
||||
"spotify_cookie": "Cookie de Spotify {name}",
|
||||
"cookie_name_cookie": "Cookie {name}",
|
||||
"fill_in_all_fields": "Por favor, completa todos los campos",
|
||||
"submit": "Enviar",
|
||||
"exit": "Salir",
|
||||
"previous": "Anterior",
|
||||
"next": "Siguiente",
|
||||
"done": "Listo",
|
||||
"step_1": "Paso 1",
|
||||
"first_go_to": "Primero, ve a",
|
||||
"login_if_not_logged_in": "e inicia sesión/registra tu cuenta si no lo has hecho aún",
|
||||
"step_2": "Paso 2",
|
||||
"step_2_steps": "1. Una vez que hayas iniciado sesión, presiona F12 o haz clic derecho con el ratón > Inspeccionar para abrir las herramientas de desarrollo del navegador.\n2. Luego ve a la pestaña \"Application\" (Chrome, Edge, Brave, etc.) o \"Storage\" (Firefox, Palemoon, etc.)\n3. Ve a la sección \"Cookies\" y luego la subsección \"https://accounts.spotify.com\"",
|
||||
"step_3": "Paso 3",
|
||||
"step_3_steps": "Copia los valores de las Cookies \"sp_dc\" y \"sp_key\" (o sp_gaid)",
|
||||
"success_emoji": "¡Éxito! 🥳",
|
||||
"success_message": "Ahora has iniciado sesión con éxito en tu cuenta de Spotify. ¡Buen trabajo!",
|
||||
"step_4": "Paso 4",
|
||||
"step_4_steps": "Pega los valores copiados de \"sp_dc\" y \"sp_key\" (o sp_gaid) en los campos respectivos",
|
||||
"something_went_wrong": "Algo salió mal",
|
||||
"piped_instance": "Instancia del servidor Piped",
|
||||
"piped_description": "La instancia del servidor Piped a utilizar para la coincidencia de pistas",
|
||||
"piped_warning": "Algunas pueden no funcionar bien, úsalas bajo tu propio riesgo",
|
||||
"generate_playlist": "Generar Lista de reproducción",
|
||||
"track_exists": "La canción {track} ya existe",
|
||||
"replace_downloaded_tracks": "Reemplazar todas las canciones descargadas",
|
||||
"skip_download_tracks": "Omitir la descarga de todas las canciones descargadas",
|
||||
"do_you_want_to_replace": "¿Deseas reemplazar la canción existente?",
|
||||
"replace": "Reemplazar",
|
||||
"skip": "Omitir",
|
||||
"select_up_to_count_type": "Seleccionar hasta {count} {type}",
|
||||
"select_genres": "Seleccionar Géneros",
|
||||
"add_genres": "Agregar Géneros",
|
||||
"country": "País",
|
||||
"number_of_tracks_generate": "Número de canciones a generar",
|
||||
"acousticness": "Acousticness",
|
||||
"danceability": "Danceability",
|
||||
"energy": "Energía",
|
||||
"instrumentalness": "Instrumentalidad",
|
||||
"liveness": "En vivo",
|
||||
"loudness": "Volumen",
|
||||
"speechiness": "Habla",
|
||||
"valence": "Valencia",
|
||||
"popularity": "Popularidad",
|
||||
"key": "Tono",
|
||||
"duration": "Duración (s)",
|
||||
"tempo": "Tempo (BPM)",
|
||||
"mode": "Modo",
|
||||
"time_signature": "Compás",
|
||||
"short": "Corto",
|
||||
"medium": "Medio",
|
||||
"long": "Largo",
|
||||
"min": "Mín.",
|
||||
"max": "Máx.",
|
||||
"target": "Objetivo",
|
||||
"moderate": "Moderado",
|
||||
"deselect_all": "Deseleccionar todo",
|
||||
"select_all": "Seleccionar todo",
|
||||
"are_you_sure": "¿Estás seguro?",
|
||||
"generating_playlist": "Generando tu lista de reproducción personalizada...",
|
||||
"selected_count_tracks": "Seleccionadas {count} canciones",
|
||||
"download_warning": "Si descargas todas las canciones de golpe, estás claramente pirateando música y causando daño a la sociedad creativa de la música. Espero que seas consciente de esto y siempre intentes respetar y apoyar el arduo trabajo de los artistas",
|
||||
"download_ip_ban_warning": "Por cierto, tu IP puede ser bloqueada en YouTube debido a solicitudes de descarga excesivas. El bloqueo de IP significa que no podrás usar YouTube (incluso si has iniciado sesión) durante al menos 2-3 meses desde esa dirección IP. Y Spotube no se hace responsable si esto ocurre alguna vez",
|
||||
"by_clicking_accept_terms": "Al hacer clic en 'Aceptar', aceptas los siguientes términos:",
|
||||
"download_agreement_1": "Sé que estoy pirateando música. Soy malo",
|
||||
"download_agreement_2": "Apoyaré al artista donde pueda y solo lo hago porque no tengo dinero para comprar su arte",
|
||||
"download_agreement_3": "Soy completamente consciente de que mi IP puede ser bloqueada en YouTube y no responsabilizo a Spotube ni a sus dueños/contribuyentes por cualquier incidente causado por mi acción actual",
|
||||
"decline": "Rechazar",
|
||||
"accept": "Aceptar",
|
||||
"details": "Detalles",
|
||||
"youtube": "YouTube",
|
||||
"channel": "Canal",
|
||||
"likes": "Me gusta",
|
||||
"dislikes": "No me gusta",
|
||||
"views": "Vistas",
|
||||
"streamUrl": "URL del streaming",
|
||||
"stop": "Detener",
|
||||
"sort_newest": "Ordenar por más recientes",
|
||||
"sort_oldest": "Ordenar por más antiguos",
|
||||
"sleep_timer": "Temporizador de apagado",
|
||||
"mins": "{minutes} minutos",
|
||||
"hours": "{hours} horas",
|
||||
"hour": "{hours} hora",
|
||||
"custom_hours": "Horas personalizadas",
|
||||
"logs": "Registros",
|
||||
"developers": "Desarrolladores",
|
||||
"not_logged_in": "No has iniciado sesión",
|
||||
"search_mode": "Modo de búsqueda",
|
||||
"youtube_api_type": "Tipo de API de YouTube",
|
||||
"ok": "OK",
|
||||
"failed_to_encrypt": "Error al cifrar",
|
||||
"encryption_failed_warning": "Spotube utiliza el cifrado para almacenar sus datos de forma segura. Pero ha fallado. Por lo tanto, volverá a un almacenamiento no seguro\nSi está utilizando Linux, asegúrese de tener instalados servicios secretos como gnome-keyring, kde-wallet y keepassxc"
|
||||
}
|
@ -176,14 +176,15 @@
|
||||
"step_2": "Étape 2",
|
||||
"step_2_steps": "1. Une fois connecté, appuyez sur F12 ou clic droit de la souris > Inspecter pour ouvrir les outils de développement du navigateur.\n2. Ensuite, allez dans l'onglet \"Application\" (Chrome, Edge, Brave, etc.) ou l'onglet \"Stockage\" (Firefox, Palemoon, etc.)\n3. Allez dans la section \"Cookies\", puis dans la sous-section \"https://accounts.spotify.com\"",
|
||||
"step_3": "Étape 3",
|
||||
"step_3_steps": "Copiez les valeurs des cookies \"sp_dc\" et \"sp_key\"",
|
||||
"step_3_steps": "Copiez les valeurs des cookies \"sp_dc\" et \"sp_key\" (ou sp_gaid)",
|
||||
"success_emoji": "Succès🥳",
|
||||
"success_message": "Vous êtes maintenant connecté avec succès à votre compte Spotify. Bon travail, mon ami!",
|
||||
"step_4": "Étape 4",
|
||||
"step_4_steps": "Collez les valeurs copiées de \"sp_dc\" et \"sp_key\" dans les champs respectifs",
|
||||
"step_4_steps": "Collez les valeurs copiées de \"sp_dc\" et \"sp_key\" (ou sp_gaid) dans les champs respectifs",
|
||||
"something_went_wrong": "Quelque chose s'est mal passé",
|
||||
"piped_instance": "Instance pipée",
|
||||
"piped_description": "L'instance de serveur Piped à utiliser pour la correspondance des pistes\nCertaines d'entre elles peuvent ne pas fonctionner correctement. Alors utilisez à vos risques et périls",
|
||||
"piped_description": "L'instance de serveur Piped à utiliser pour la correspondance des pistes",
|
||||
"piped_warning": "Certaines d'entre elles peuvent ne pas fonctionner correctement. Alors utilisez à vos risques et périls",
|
||||
"generate_playlist": "Générer une playlist",
|
||||
"track_exists": "La piste {track} existe déjà",
|
||||
"replace_downloaded_tracks": "Remplacer toutes les pistes téléchargées",
|
||||
@ -249,5 +250,8 @@
|
||||
"developers": "Développeurs",
|
||||
"not_logged_in": "Vous n'êtes pas connecté(e)",
|
||||
"search_mode": "Mode de recherche",
|
||||
"youtube_api_type": "Type d'API"
|
||||
"youtube_api_type": "Type d'API",
|
||||
"ok": "OK",
|
||||
"failed_to_encrypt": "Échec de la cryptage",
|
||||
"encryption_failed_warning": "Spotube utilise le cryptage pour stocker vos données en toute sécurité. Mais cela a échoué. Il basculera donc vers un stockage non sécurisé\nSi vous utilisez Linux, assurez-vous d'avoir installé des services secrets tels que gnome-keyring, kde-wallet et keepassxc"
|
||||
}
|
@ -176,14 +176,15 @@
|
||||
"step_2": "2 चरण",
|
||||
"step_2_steps": "1. जब आप लॉगिन हो जाएँ, तो F12 दबाएं या माउस राइट क्लिक> निरीक्षण करें ताकि ब्राउज़र डेवटूल्स खुलें।\n2. फिर ब्राउज़र के \"एप्लिकेशन\" टैब (Chrome, Edge, Brave आदि) या \"स्टोरेज\" टैब (Firefox, Palemoon आदि) में जाएं\n3. \"कुकीज़\" अनुभाग में जाएं फिर \"https: //accounts.spotify.com\" उप-अनुभाग में जाएं",
|
||||
"step_3": "स्टेप 3",
|
||||
"step_3_steps": "\"sp_dc\" और \"sp_key\" कुकीज़ के मान कॉपी करें",
|
||||
"step_3_steps": "\"sp_dc\" और \"sp_key\" (या sp_gaid) कुकीज़ के मान कॉपी करें",
|
||||
"success_emoji": "सफलता🥳",
|
||||
"success_message": "अब आप अपने स्पॉटिफाई अकाउंट से सफलतापूर्वक लॉगइन हो गए हैं। अच्छा काम किया!",
|
||||
"step_4": "स्टेप 4",
|
||||
"step_4_steps": "कॉपी की गई \"sp_dc\" और \"sp_key\" मानों को संबंधित फील्ड में पेस्ट करें",
|
||||
"step_4_steps": "कॉपी की गई \"sp_dc\" और \"sp_key\" (या sp_gaid) मानों को संबंधित फील्ड में पेस्ट करें",
|
||||
"something_went_wrong": "कुछ गलत हो गया",
|
||||
"piped_instance": "पाइप्ड सर्वर",
|
||||
"piped_description": "पाइप किए गए सर्वर\n गानों का मिलान करने के लिए उपयोग किए जाते हैं, हो सकता है कि उनमें से कुछ के साथ ठीक से काम न करें इसलिए अपने जोखिम पर उपयोग करें",
|
||||
"piped_description": "पाइप किए गए सर्वर",
|
||||
"piped_warning": "गानों का मिलान करने के लिए उपयोग किए जाते हैं, हो सकता है कि उनमें से कुछ के साथ ठीक से काम न करें इसलिए अपने जोखिम पर उपयोग करें",
|
||||
"generate_playlist": "प्लेलिस्ट बनाएं",
|
||||
"track_exists": "ट्रैक {track} पहले से मौजूद है",
|
||||
"replace_downloaded_tracks": "सभी डाउनलोड किए गए ट्रैक्स को बदलें",
|
||||
@ -249,5 +250,8 @@
|
||||
"developers": "डेवलपर्स",
|
||||
"not_logged_in": "आप लॉग इन नहीं हैं",
|
||||
"search_mode": "खोज मोड",
|
||||
"youtube_api_type": "API प्रकार"
|
||||
"youtube_api_type": "API प्रकार",
|
||||
"ok": "ठीक है",
|
||||
"failed_to_encrypt": "एन्क्रिप्ट करने में विफल रहा",
|
||||
"encryption_failed_warning": "Spotube आपके डेटा को सुरक्षित रूप से स्टोर करने के लिए एन्क्रिप्शन का उपयोग करता है। लेकिन इसमें विफल रहा। इसलिए, यह असुरक्षित स्टोरेज पर फॉलबैक करेगा\nयदि आप Linux का उपयोग कर रहे हैं, तो कृपया सुनिश्चित करें कि आपके पास gnome-keyring, kde-wallet, keepassxc आदि जैसी कोई सीक्रेट-सर्विस इंस्टॉल की गई है"
|
||||
}
|
@ -176,14 +176,15 @@
|
||||
"step_2": "ステップ 2",
|
||||
"step_2_steps": "1. ログインしたら、F12を押すか、マウス右クリック > 調査(検証)でブラウザの開発者ツール (devtools) を開きます。\n2. アプリケーション (Application) タブ (Chrome, Edge, Brave など) またはストレージタブ (Firefox, Palemoon など)\n3. Cookies 欄を選択し、https://accounts.spotify.com の枝を選びます",
|
||||
"step_3": "ステップ 3",
|
||||
"step_3_steps": "sp_dc と sp_key の値 (Value) をコピーします",
|
||||
"step_3_steps": "sp_dc と sp_key (または or sp_gaid) の値 (Value) をコピーします",
|
||||
"success_emoji": "成功🥳",
|
||||
"success_message": "アカウントへのログインに成功しました。よくできました!",
|
||||
"step_4": "ステップ 4",
|
||||
"step_4_steps": "コピーした sp_dc と sp_keyの値をそれぞれの入力欄に貼り付けます",
|
||||
"step_4_steps": "コピーした sp_dc と sp_key (または or sp_gaid) の値をそれぞれの入力欄に貼り付けます",
|
||||
"something_went_wrong": "何か誤りがあります",
|
||||
"piped_instance": "Piped サーバーのインスタンス",
|
||||
"piped_description": "曲の一致に使う Piped サーバーのインスタンス\nそれらの一部ではうまく動作しないこともあります。自己責任で使用してください",
|
||||
"piped_description": "曲の一致に使う Piped サーバーのインスタンス",
|
||||
"piped_warning": "それらの一部ではうまく動作しないこともあります。自己責任で使用してください",
|
||||
"generate_playlist": "再生リストの生成",
|
||||
"track_exists": "曲 {track} は既に存在します",
|
||||
"replace_downloaded_tracks": "すべてのダウンロード済みの曲を置換",
|
||||
@ -249,5 +250,8 @@
|
||||
"developers": "開発",
|
||||
"not_logged_in": "ログインしていません",
|
||||
"search_mode": "検索モード",
|
||||
"youtube_api_type": "APIの種類"
|
||||
"youtube_api_type": "APIの種類",
|
||||
"ok": "分かりました",
|
||||
"failed_to_encrypt": "暗号化に失敗しました",
|
||||
"encryption_failed_warning": "Spotubeはデータを安全に保存するために暗号化を使用しています。しかし、失敗しました。したがって、安全でないストレージにフォールバックします\nLinuxを使用している場合は、gnome-keyring、kde-wallet、keepassxcなどのシークレットサービスがインストールされていることを確認してください"
|
||||
}
|
257
lib/l10n/app_zh.arb
Normal file
257
lib/l10n/app_zh.arb
Normal file
@ -0,0 +1,257 @@
|
||||
{
|
||||
"guest": "访客",
|
||||
"browse": "浏览",
|
||||
"search": "搜索",
|
||||
"library": "音乐库",
|
||||
"lyrics": "歌词",
|
||||
"settings": "设置",
|
||||
"genre_categories_filter": "筛选类别...",
|
||||
"genre": "探索歌单",
|
||||
"personalized": "为你打造",
|
||||
"featured": "推荐",
|
||||
"new_releases": "新歌热播",
|
||||
"songs": "歌曲",
|
||||
"playing_track": "播放 {track}",
|
||||
"queue_clear_alert": "这将清空当前的播放队列。{track_length} 首歌曲将被移除\n你确定要继续吗?",
|
||||
"load_more": "加载更多",
|
||||
"playlists": "歌单",
|
||||
"artists": "艺人",
|
||||
"albums": "专辑",
|
||||
"tracks": "歌曲",
|
||||
"downloads": "下载",
|
||||
"filter_playlists": "筛选歌单...",
|
||||
"liked_tracks": "已点赞的歌曲",
|
||||
"liked_tracks_description": "你点赞过的所有歌曲",
|
||||
"create_playlist": "创建歌单",
|
||||
"create_a_playlist": "创建一个歌单",
|
||||
"create": "创建",
|
||||
"cancel": "取消",
|
||||
"playlist_name": "歌单名称",
|
||||
"name_of_playlist": "歌单的名称",
|
||||
"description": "描述",
|
||||
"public": "公开",
|
||||
"collaborative": "共享协作",
|
||||
"search_local_tracks": "搜索本地歌曲...",
|
||||
"play": "播放",
|
||||
"delete": "删除",
|
||||
"none": "无",
|
||||
"sort_a_z": "按字母正序",
|
||||
"sort_z_a": "按字母倒序",
|
||||
"sort_artist": "按艺人",
|
||||
"sort_album": "按专辑",
|
||||
"sort_tracks": "排序方式",
|
||||
"currently_downloading": "正在下载 ({tracks_length})",
|
||||
"cancel_all": "取消全部",
|
||||
"filter_artist": "筛选艺人...",
|
||||
"followers": "{followers} 名关注者",
|
||||
"add_artist_to_blacklist": "屏蔽该艺人",
|
||||
"top_tracks": "热门歌曲",
|
||||
"fans_also_like": "粉丝也喜欢",
|
||||
"loading": "加载中...",
|
||||
"artist": "艺人",
|
||||
"blacklisted": "已屏蔽",
|
||||
"following": "关注中",
|
||||
"follow": "关注",
|
||||
"artist_url_copied": "艺人的分享链接已复制至剪贴板",
|
||||
"added_to_queue": "已添加 {tracks} 首歌曲到播放队列",
|
||||
"filter_albums": "筛选专辑...",
|
||||
"synced": "同步",
|
||||
"plain": "无同步",
|
||||
"shuffle": "随机播放",
|
||||
"search_tracks": "搜索歌曲...",
|
||||
"released": "发行时间",
|
||||
"error": "错误 {error}",
|
||||
"title": "标题",
|
||||
"time": "时长",
|
||||
"more_actions": "更多操作",
|
||||
"download_count": "下载 ({count}) 首歌曲",
|
||||
"add_count_to_playlist": "添加 ({count}) 首歌曲到歌单中",
|
||||
"add_count_to_queue": "添加 ({count}) 首歌曲到播放队列中",
|
||||
"play_count_next": "接下来播放 ({count}) 首歌曲",
|
||||
"album": "专辑",
|
||||
"copied_to_clipboard": "已将 {data} 复制至剪贴板",
|
||||
"add_to_following_playlists": "添加 {track} 到以下播放列表",
|
||||
"add": "添加",
|
||||
"added_track_to_queue": "添加 {track} 到播放队列",
|
||||
"add_to_queue": "添加到播放队列",
|
||||
"track_will_play_next": "{track} 将在下一首播放",
|
||||
"play_next": "下一首播放",
|
||||
"removed_track_from_queue": "将 {track} 从播放队列中移除",
|
||||
"remove_from_queue": "从播放队列移除",
|
||||
"remove_from_favorites": "取消点赞",
|
||||
"save_as_favorite": "点赞",
|
||||
"add_to_playlist": "添加到歌单",
|
||||
"remove_from_playlist": "从歌单中移除",
|
||||
"add_to_blacklist": "添加到屏蔽列表",
|
||||
"remove_from_blacklist": "从屏蔽列表中移除",
|
||||
"share": "分享",
|
||||
"mini_player": "小窗模式",
|
||||
"slide_to_seek": "滑动以前进或后退",
|
||||
"shuffle_playlist": "随机播放歌单",
|
||||
"unshuffle_playlist": "取消随机播放歌单",
|
||||
"previous_track": "上一首歌曲",
|
||||
"next_track": "下一首歌曲",
|
||||
"pause_playback": "暂停播放",
|
||||
"resume_playback": "恢复播放",
|
||||
"loop_track": "单曲循环",
|
||||
"repeat_playlist": "歌单循环",
|
||||
"queue": "播放队列",
|
||||
"alternative_track_sources": "其它音源",
|
||||
"download_track": "下载歌曲",
|
||||
"tracks_in_queue": "{tracks} 首歌曲在播放队列中",
|
||||
"clear_all": "清除全部",
|
||||
"show_hide_ui_on_hover": "悬停时显示/隐藏控制栏",
|
||||
"always_on_top": "置顶",
|
||||
"exit_mini_player": "退出小窗模式",
|
||||
"download_location": "下载路径",
|
||||
"account": "账户",
|
||||
"login_with_spotify": "使用 Spotify 登录",
|
||||
"connect_with_spotify": "与 Spotify 账户连接",
|
||||
"logout": "退出",
|
||||
"logout_of_this_account": "退出该账户",
|
||||
"language_region": "语言和地区",
|
||||
"language": "语言",
|
||||
"system_default": "系统默认",
|
||||
"market_place_region": "市场地区",
|
||||
"recommendation_country": "选择国家与地区以获取对应推荐",
|
||||
"appearance": "外观",
|
||||
"layout_mode": "布局类型",
|
||||
"override_layout_settings": "将覆盖响应式布局设置",
|
||||
"adaptive": "自适应",
|
||||
"compact": "紧凑",
|
||||
"extended": "宽广",
|
||||
"theme": "主题",
|
||||
"dark": "深色",
|
||||
"light": "浅色",
|
||||
"system": "系统",
|
||||
"accent_color": "主色调",
|
||||
"sync_album_color": "匹配封面颜色",
|
||||
"sync_album_color_description": "选取专辑封面主题色作为主色调",
|
||||
"playback": "播放",
|
||||
"audio_quality": "音质",
|
||||
"high": "高",
|
||||
"low": "低",
|
||||
"pre_download_play": "先下后播",
|
||||
"pre_download_play_description": "先下载歌曲后再播放而非流式播放(推荐带宽较高用户使用)",
|
||||
"skip_non_music": "跳过非音乐片段(屏蔽赞助商)",
|
||||
"blacklist_description": "已屏蔽的歌曲与艺人",
|
||||
"wait_for_download_to_finish": "请等待当前下载任务完成",
|
||||
"download_lyrics": "下载歌曲时同时下载歌词",
|
||||
"desktop": "桌面端设置",
|
||||
"close_behavior": "点击关闭按钮行为",
|
||||
"close": "关闭",
|
||||
"minimize_to_tray": "最小化到托盘",
|
||||
"show_tray_icon": "显示托盘图标",
|
||||
"about": "关于",
|
||||
"u_love_spotube": "我们明白你喜欢 Spotube",
|
||||
"check_for_updates": "检查更新",
|
||||
"about_spotube": "关于 Spotube",
|
||||
"blacklist": "屏蔽列表",
|
||||
"please_sponsor": "请赞助/捐赠",
|
||||
"spotube_description": "Spotube,一个轻量、跨平台且完全免费的 Spotify 客户端。",
|
||||
"version": "版本",
|
||||
"build_number": "构建代码",
|
||||
"founder": "发起人",
|
||||
"repository": "源码",
|
||||
"bug_issues": "缺陷和问题报告",
|
||||
"made_with": "于孟加拉🇧🇩用 ❤️ 发电",
|
||||
"kingkor_roy_tirtho": "Kingkor Roy Tirtho",
|
||||
"copyright": "© 2021-{current_year} Kingkor Roy Tirtho",
|
||||
"license": "许可证",
|
||||
"add_spotify_credentials": "添加你的 Spotify 登录信息以开始使用",
|
||||
"credentials_will_not_be_shared_disclaimer": "不用担心,软件不会收集或分享任何个人数据给第三方",
|
||||
"know_how_to_login": "不知道该怎么做?",
|
||||
"follow_step_by_step_guide": "请按照以下指南进行",
|
||||
"spotify_cookie": "Spotify {name} Cookie",
|
||||
"cookie_name_cookie": "{name} Cookie",
|
||||
"fill_in_all_fields": "请填写所有栏目",
|
||||
"submit": "提交",
|
||||
"exit": "退出",
|
||||
"previous": "上一步",
|
||||
"next": "下一步",
|
||||
"done": "完成",
|
||||
"step_1": "步骤 1",
|
||||
"first_go_to": "首先,前往",
|
||||
"login_if_not_logged_in": "如果尚未登录,请登录或者注册一个账户",
|
||||
"step_2": "步骤 2",
|
||||
"step_2_steps": "1. 一旦你已经完成登录, 按 F12 键或者鼠标右击网页空白区域 > 选择“检查”以打开浏览器开发者工具(DevTools)\n2. 然后选择 \"应用(Application)\" 标签页(Chrome, Edge, Brave 等基于 Chromium 的浏览器) 或 \"存储(Storage)\" 标签页 (Firefox, Palemoon 等基于 Firefox 的浏览器))\n3. 选择 \"Cookies\" 栏目然后选择 \"https://accounts.spotify.com\" 子栏目",
|
||||
"step_3": "步骤 3",
|
||||
"step_3_steps": "复制名称为 \"sp_dc\" 和 \"sp_key\" (或 sp_gaid) 的值(Cookie Value)",
|
||||
"success_emoji": "成功🥳",
|
||||
"success_message": "你已经成功使用 Spotify 登录。干得漂亮!",
|
||||
"step_4": "步骤 4",
|
||||
"step_4_steps": "将 \"sp_dc\" 与 \"sp_key\" (或 sp_gaid) 的值分别复制后粘贴到对应的区域",
|
||||
"something_went_wrong": "某些地方出现了问题",
|
||||
"piped_instance": "管道服务器实例",
|
||||
"piped_description": "管道服务器实例用于匹配歌曲",
|
||||
"piped_warning": "它们中的一部分可能并不能正常工作。使用时请自行承担风险",
|
||||
"generate_playlist": "生成歌单",
|
||||
"track_exists": "歌曲 {track} 已存在",
|
||||
"replace_downloaded_tracks": "替换已下载的歌曲",
|
||||
"skip_download_tracks": "下载时跳过已下载的歌曲",
|
||||
"do_you_want_to_replace": "你确定要替换已下载的歌曲吗??",
|
||||
"replace": "替换",
|
||||
"skip": "跳过",
|
||||
"select_up_to_count_type": "选择多达 {count} 种的类型 {type}",
|
||||
"select_genres": "选择曲风",
|
||||
"add_genres": "添加曲风",
|
||||
"country": "国家和地区",
|
||||
"number_of_tracks_generate": "生成歌曲的数目",
|
||||
"acousticness": "原声程度",
|
||||
"danceability": "律动感",
|
||||
"energy": "冲击感",
|
||||
"instrumentalness": "歌唱部分占比",
|
||||
"liveness": "现场感",
|
||||
"loudness": "响度",
|
||||
"speechiness": "朗诵比例",
|
||||
"valence": "心理感受",
|
||||
"popularity": "流行度",
|
||||
"key": "曲调",
|
||||
"duration": "歌曲时长 (s)",
|
||||
"tempo": "分钟节拍数 (BPM)",
|
||||
"mode": "旋律重复度",
|
||||
"time_signature": "音符时值",
|
||||
"short": "短",
|
||||
"medium": "中",
|
||||
"long": "长",
|
||||
"min": "最低",
|
||||
"max": "最高",
|
||||
"target": "目标",
|
||||
"moderate": "中",
|
||||
"deselect_all": "取消全选",
|
||||
"select_all": "全选",
|
||||
"are_you_sure": "你确定吗?",
|
||||
"generating_playlist": "正在生成你的自定义歌单...",
|
||||
"selected_count_tracks": "已选择 {count} 首歌曲",
|
||||
"download_warning": "如果你大量下载这些歌曲,你显然在侵犯音乐的版权并对音乐创作社区造成了伤害。我希望你能意识到这一点。永远要尊重并支持艺术家们的辛勤工作",
|
||||
"download_ip_ban_warning": "小心,如果出现超出正常的下载请求那你的 IP 可能会被 YouTube 封禁,这意味着你的设备将在长达 2-3 个月的时间内无法使用该 IP 访问 YouTube(即使你没登录)。Spotube 对此不承担任何责任",
|
||||
"by_clicking_accept_terms": "点击 '同意' 代表着你同意以下的条款",
|
||||
"download_agreement_1": "我明白侵犯音乐版权是一件不好的事情",
|
||||
"download_agreement_2": "我将尽可能支持艺术家的工作。我现在之所以做不到是因为缺乏资金来购买正版",
|
||||
"download_agreement_3": "我完全了解我的 IP 存在被 YouTube的风险。我同意 Spotube 的所有者与贡献者们无须对我目前的行为所导致的任何后果负责",
|
||||
"decline": "拒绝",
|
||||
"accept": "同意",
|
||||
"details": "详情",
|
||||
"youtube": "YouTube",
|
||||
"channel": "频道",
|
||||
"likes": "赞",
|
||||
"dislikes": "踩",
|
||||
"views": "浏览次数",
|
||||
"streamUrl": "播放流 URL",
|
||||
"stop": "停止",
|
||||
"sort_newest": "按添加日期正序",
|
||||
"sort_oldest": "按添加日期倒序",
|
||||
"sleep_timer": "睡眠定时器",
|
||||
"mins": "{minutes} 分",
|
||||
"hours": "{hours} 时",
|
||||
"hour": "{hours} 时",
|
||||
"custom_hours": "自定义时间",
|
||||
"logs": "日志",
|
||||
"developers": "开发者",
|
||||
"not_logged_in": "你尚未登录",
|
||||
"search_mode": "搜索模式",
|
||||
"youtube_api_type": "API 类型",
|
||||
"ok": "确定",
|
||||
"failed_to_encrypt": "加密失败",
|
||||
"encryption_failed_warning": "Spotube使用加密来安全地存储您的数据。但是失败了。因此,它将回退到不安全的存储\n如果您使用Linux,请确保已安装gnome-keyring、kde-wallet和keepassxc等秘密服务"
|
||||
}
|
@ -3,15 +3,18 @@
|
||||
/// Kingkor Roy Tirtho => English, Bengali
|
||||
/// ChatGPT (GPT 3.5) XD => Hindi, French
|
||||
/// maboroshin@github => Japanese
|
||||
/// iceyear@github => Simplified Chinese
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class L10n {
|
||||
static final all = [
|
||||
const Locale('en'),
|
||||
const Locale('bn', 'BD'),
|
||||
const Locale('de', 'GE'),
|
||||
const Locale('es', 'ES'),
|
||||
const Locale('fr', 'FR'),
|
||||
const Locale('hi', 'IN'),
|
||||
const Locale('de', 'GE'),
|
||||
const Locale('ja', 'JP'),
|
||||
const Locale('zh', 'CN'),
|
||||
];
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ import 'package:path_provider/path_provider.dart';
|
||||
import 'package:spotube/hooks/use_init_sys_tray.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||
|
||||
Future<void> main(List<String> rawArgs) async {
|
||||
final parser = ArgParser();
|
||||
@ -85,6 +86,11 @@ Future<void> main(List<String> rawArgs) async {
|
||||
|
||||
MediaKit.ensureInitialized();
|
||||
|
||||
// force High Refresh Rate on some Android devices (like One Plus)
|
||||
if (DesktopTools.platform.isAndroid) {
|
||||
await FlutterDisplayMode.setHighRefreshRate();
|
||||
}
|
||||
|
||||
await DesktopTools.ensureInitialized(
|
||||
DesktopWindowOptions(
|
||||
hideTitleBar: true,
|
||||
@ -116,7 +122,7 @@ Future<void> main(List<String> rawArgs) async {
|
||||
MatchedTrack.boxName,
|
||||
path: hiveCacheDir,
|
||||
);
|
||||
await Hive.openLazyBox<List<SkipSegment>>(
|
||||
await Hive.openLazyBox(
|
||||
SkipSegment.boxName,
|
||||
path: hiveCacheDir,
|
||||
);
|
||||
@ -150,7 +156,7 @@ Future<void> main(List<String> rawArgs) async {
|
||||
runApp(
|
||||
DevicePreview(
|
||||
availableLocales: L10n.all,
|
||||
enabled: !kReleaseMode && DesktopTools.platform.isDesktop,
|
||||
enabled: false,
|
||||
data: const DevicePreviewData(
|
||||
isEnabled: false,
|
||||
orientation: Orientation.portrait,
|
||||
|
@ -12,8 +12,7 @@ class SkipSegment {
|
||||
|
||||
static String version = 'v1';
|
||||
static final boxName = "oss.krtirtho.spotube.skip_segments.$version";
|
||||
static LazyBox<List<SkipSegment>> get box =>
|
||||
Hive.lazyBox<List<SkipSegment>>(boxName);
|
||||
static LazyBox get box => Hive.lazyBox(boxName);
|
||||
|
||||
SkipSegment.fromJson(Map<String, dynamic> json)
|
||||
: start = json['start'],
|
||||
|
@ -285,7 +285,7 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Slider.adaptive(
|
||||
child: Slider(
|
||||
value: value.toDouble(),
|
||||
min: 10,
|
||||
max: 100,
|
||||
|
@ -5,12 +5,14 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotube/components/shared/dialogs/replace_downloaded_dialog.dart';
|
||||
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/hooks/use_update_checker.dart';
|
||||
import 'package:spotube/provider/download_manager_provider.dart';
|
||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
||||
|
||||
const rootPaths = {
|
||||
0: "/",
|
||||
@ -33,6 +35,17 @@ class RootApp extends HookConsumerWidget {
|
||||
final showingDialogCompleter = useRef(Completer()..complete());
|
||||
final downloader = ref.watch(downloadManagerProvider.notifier);
|
||||
|
||||
useEffect(() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
|
||||
if (sharedPreferences.getBool(kIsUsingEncryption) == false &&
|
||||
context.mounted) {
|
||||
await PersistedStateNotifier.showNoEncryptionDialog(context);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() {
|
||||
downloader.onFileExists = (track) async {
|
||||
if (!isMounted()) return false;
|
||||
|
@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:piped_client/piped_client.dart';
|
||||
import 'package:spotube/collections/env.dart';
|
||||
@ -74,37 +75,48 @@ class SettingsPage extends HookConsumerWidget {
|
||||
heading: context.l10n.account,
|
||||
children: [
|
||||
if (auth == null)
|
||||
AdaptiveListTile(
|
||||
leading: Icon(
|
||||
SpotubeIcons.login,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
title: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: AutoSizeText(
|
||||
context.l10n.login_with_spotify,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
LayoutBuilder(builder: (context, constrains) {
|
||||
return ListTile(
|
||||
leading: Icon(
|
||||
SpotubeIcons.login,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
trailing: (context, update) => FilledButton(
|
||||
onPressed: () {
|
||||
GoRouter.of(context).push("/login");
|
||||
},
|
||||
style: ButtonStyle(
|
||||
shape: MaterialStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(25.0),
|
||||
title: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: AutoSizeText(
|
||||
context.l10n.login_with_spotify,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
context.l10n.connect_with_spotify.toUpperCase(),
|
||||
),
|
||||
),
|
||||
)
|
||||
onTap: constrains.mdAndUp
|
||||
? null
|
||||
: () {
|
||||
GoRouter.of(context).push("/login");
|
||||
},
|
||||
trailing: constrains.smAndDown
|
||||
? null
|
||||
: FilledButton(
|
||||
onPressed: () {
|
||||
GoRouter.of(context).push("/login");
|
||||
},
|
||||
style: ButtonStyle(
|
||||
shape: MaterialStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(25.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
context.l10n.connect_with_spotify
|
||||
.toUpperCase(),
|
||||
),
|
||||
),
|
||||
);
|
||||
})
|
||||
else
|
||||
Builder(builder: (context) {
|
||||
return ListTile(
|
||||
@ -322,8 +334,23 @@ class SettingsPage extends HookConsumerWidget {
|
||||
const Icon(SpotubeIcons.piped),
|
||||
title:
|
||||
Text(context.l10n.piped_instance),
|
||||
subtitle: Text(
|
||||
context.l10n.piped_description),
|
||||
subtitle: RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: context
|
||||
.l10n.piped_description),
|
||||
const TextSpan(text: "\n"),
|
||||
TextSpan(
|
||||
text:
|
||||
context.l10n.piped_warning,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
value: preferences.pipedInstance,
|
||||
showValueWhenUnfolded: false,
|
||||
options: data
|
||||
@ -331,9 +358,26 @@ class SettingsPage extends HookConsumerWidget {
|
||||
.map(
|
||||
(e) => DropdownMenuItem(
|
||||
value: e.apiUrl,
|
||||
child: Text(
|
||||
"${e.name}\n"
|
||||
"${e.locations.map(countryCodeToEmoji).join(" ")}",
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text:
|
||||
"${e.name.trim()}\n",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge,
|
||||
),
|
||||
TextSpan(
|
||||
text: e.locations
|
||||
.map(
|
||||
countryCodeToEmoji)
|
||||
.join(""),
|
||||
style: GoogleFonts
|
||||
.notoColorEmoji(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -1,12 +0,0 @@
|
||||
import 'package:dbus/dbus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
final Provider<DBusClient?> dbusClientProvider = Provider<DBusClient?>((ref) {
|
||||
if (kIsLinux) {
|
||||
return DBusClient.session();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
final dbus = DBusClient.session();
|
@ -94,12 +94,13 @@ mixin NextFetcher on StateNotifier<ProxyPlaylist> {
|
||||
}
|
||||
|
||||
List<Track> mapSourcesToTracks(List<String> sources) {
|
||||
final tracks = state.tracks;
|
||||
|
||||
return sources
|
||||
.map((source) {
|
||||
final track = tracks.firstWhereOrNull(
|
||||
(track) => makeAppropriateSource(track) == source,
|
||||
final track = state.tracks.firstWhereOrNull(
|
||||
(track) {
|
||||
final newSource = makeAppropriateSource(track);
|
||||
return newSource == source;
|
||||
},
|
||||
);
|
||||
return track;
|
||||
})
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:catcher/catcher.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
@ -68,8 +69,8 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
||||
() async {
|
||||
notificationService = await AudioServices.create(ref, this);
|
||||
|
||||
(String, List<SkipSegment>)? currentSegments;
|
||||
bool isFetchingSegments = false;
|
||||
({String source, List<SkipSegment> segments})? currentSegments;
|
||||
|
||||
audioPlayer.activeSourceChangedStream.listen((newActiveSource) async {
|
||||
final newActiveTrack =
|
||||
mapSourcesToTracks([newActiveSource]).firstOrNull;
|
||||
@ -86,10 +87,6 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
||||
.indexWhere((element) => element.id == newActiveTrack.id),
|
||||
);
|
||||
|
||||
isFetchingSegments = true;
|
||||
|
||||
isFetchingSegments = false;
|
||||
|
||||
updatePalette();
|
||||
});
|
||||
|
||||
@ -110,33 +107,21 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
||||
|
||||
bool isPreSearching = false;
|
||||
|
||||
listenTo60Percent(percent) async {
|
||||
listenTo2Percent(int percent) async {
|
||||
if (isPreSearching ||
|
||||
audioPlayer.currentSource == null ||
|
||||
audioPlayer.nextSource == null) return;
|
||||
audioPlayer.nextSource == null ||
|
||||
isPlayable(audioPlayer.nextSource!)) return;
|
||||
|
||||
try {
|
||||
isPreSearching = true;
|
||||
|
||||
// TODO: Make repeat mode sensitive changes later
|
||||
final oldTrack =
|
||||
mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull;
|
||||
final track = await ensureSourcePlayable(audioPlayer.nextSource!);
|
||||
|
||||
if (track != null) {
|
||||
state = state.copyWith(tracks: mergeTracks([track], state.tracks));
|
||||
if (currentSegments == null ||
|
||||
(oldTrack?.id != null &&
|
||||
currentSegments!.$1 != oldTrack!.id!) &&
|
||||
!isFetchingSegments) {
|
||||
isFetchingSegments = true;
|
||||
currentSegments = (
|
||||
audioPlayer.currentSource!,
|
||||
await getAndCacheSkipSegments(
|
||||
track.ytTrack.id,
|
||||
),
|
||||
);
|
||||
isFetchingSegments = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (oldTrack != null && track != null) {
|
||||
@ -147,48 +132,39 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
||||
}
|
||||
} finally {
|
||||
isPreSearching = false;
|
||||
|
||||
/// Sometimes fetching can take a lot of time, so we need to check
|
||||
/// if next source is playable or not at 99% progress. If not, then
|
||||
/// it'll be paused automatically
|
||||
///
|
||||
/// After fetching the nextSource and replacing it, we need to check
|
||||
/// if the player is paused or not. If it is paused, then we need to
|
||||
/// resume it to skip to next track
|
||||
if (audioPlayer.isPaused) {
|
||||
await audioPlayer.resume();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
audioPlayer.percentCompletedStream(60).listen(listenTo60Percent);
|
||||
audioPlayer.percentCompletedStream(2).listen(listenTo2Percent);
|
||||
|
||||
// player stops at 99% if nextSource is still not playable
|
||||
audioPlayer.percentCompletedStream(99).listen((_) async {
|
||||
if (audioPlayer.nextSource == null ||
|
||||
isPlayable(audioPlayer.nextSource!)) return;
|
||||
await audioPlayer.pause();
|
||||
});
|
||||
bool isFetchingSegments = false;
|
||||
|
||||
audioPlayer.positionStream.listen((position) async {
|
||||
if (preferences.searchMode == SearchMode.youtubeMusic ||
|
||||
// skipping in very first second breaks stream
|
||||
if ((preferences.youtubeApiType == YoutubeApiType.piped &&
|
||||
preferences.searchMode == SearchMode.youtubeMusic) ||
|
||||
!preferences.skipNonMusic) return;
|
||||
|
||||
final notSameSegmentId =
|
||||
currentSegments?.source != audioPlayer.currentSource;
|
||||
|
||||
if (currentSegments == null ||
|
||||
currentSegments!.$1 != state.activeTrack!.id! &&
|
||||
!isFetchingSegments) {
|
||||
(notSameSegmentId && !isFetchingSegments)) {
|
||||
isFetchingSegments = true;
|
||||
currentSegments = (
|
||||
audioPlayer.currentSource!,
|
||||
await getAndCacheSkipSegments(
|
||||
(state.activeTrack as SpotubeTrack).ytTrack.id,
|
||||
),
|
||||
);
|
||||
isFetchingSegments = false;
|
||||
try {
|
||||
currentSegments = (
|
||||
source: audioPlayer.currentSource!,
|
||||
segments: await getAndCacheSkipSegments(
|
||||
(state.activeTrack as SpotubeTrack).ytTrack.id,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
isFetchingSegments = false;
|
||||
}
|
||||
}
|
||||
|
||||
final (_, segments) = currentSegments!;
|
||||
if (segments.isEmpty) return;
|
||||
final (source: _, :segments) = currentSegments!;
|
||||
if (segments.isEmpty || position < const Duration(seconds: 3)) return;
|
||||
|
||||
for (final segment in segments) {
|
||||
if ((position.inSeconds >= segment.start &&
|
||||
@ -368,6 +344,10 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
||||
}
|
||||
|
||||
Future<void> addTracksAtFirst(Iterable<Track> tracks) async {
|
||||
if (state.tracks.length == 1) {
|
||||
return addTracks(tracks);
|
||||
}
|
||||
|
||||
tracks = blacklist.filter(tracks).toList() as List<Track>;
|
||||
final destIndex = state.active != null ? state.active! + 1 : 0;
|
||||
final newTracks = state.tracks.toList()..insertAll(destIndex, tracks);
|
||||
@ -488,14 +468,12 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
||||
return;
|
||||
}
|
||||
return Future.microtask(() async {
|
||||
final activeTrack = state.tracks.elementAtOrNull(state.active ?? 0);
|
||||
|
||||
if (activeTrack == null) return;
|
||||
if (state.activeTrack == null) return;
|
||||
|
||||
final palette = await PaletteGenerator.fromImageProvider(
|
||||
UniversalImage.imageProvider(
|
||||
TypeConversionUtils.image_X_UrlString(
|
||||
activeTrack.album?.images,
|
||||
state.activeTrack?.album?.images,
|
||||
placeholder: ImagePlaceholder.albumArt,
|
||||
),
|
||||
height: 50,
|
||||
@ -508,12 +486,15 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
||||
|
||||
Future<List<SkipSegment>> getAndCacheSkipSegments(String id) async {
|
||||
if (!preferences.skipNonMusic ||
|
||||
preferences.searchMode != SearchMode.youtube) return [];
|
||||
(preferences.youtubeApiType == YoutubeApiType.piped &&
|
||||
preferences.searchMode == SearchMode.youtubeMusic)) return [];
|
||||
|
||||
try {
|
||||
final cached = await SkipSegment.box.get(id);
|
||||
if (cached != null && cached.isNotEmpty) {
|
||||
return List.castFrom<dynamic, SkipSegment>(cached);
|
||||
return List.castFrom<dynamic, SkipSegment>(
|
||||
(cached as List).map((json) => SkipSegment.fromJson(json)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
final res = await get(Uri(
|
||||
@ -535,6 +516,11 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
||||
));
|
||||
|
||||
if (res.body == "Not Found") {
|
||||
Catcher.reportCheckedError(
|
||||
"[SponsorBlock] no skip segments found for $id\n"
|
||||
"${res.request?.url}",
|
||||
StackTrace.current,
|
||||
);
|
||||
return List.castFrom<dynamic, SkipSegment>([]);
|
||||
}
|
||||
|
||||
@ -553,7 +539,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
||||
|
||||
await SkipSegment.box.put(
|
||||
id,
|
||||
segments,
|
||||
segments.map((e) => e.toJson()).toList(),
|
||||
);
|
||||
return List.castFrom<dynamic, SkipSegment>(segments);
|
||||
} catch (e, stack) {
|
||||
|
@ -77,13 +77,13 @@ class UserPreferences extends PersistedChangeNotifier {
|
||||
this.checkUpdate = true,
|
||||
this.audioQuality = AudioQuality.high,
|
||||
this.downloadLocation = "",
|
||||
this.closeBehavior = CloseBehavior.minimizeToTray,
|
||||
this.closeBehavior = CloseBehavior.close,
|
||||
this.showSystemTrayIcon = true,
|
||||
this.locale = const Locale("system", "system"),
|
||||
this.pipedInstance = "https://pipedapi.kavin.rocks",
|
||||
this.searchMode = SearchMode.youtube,
|
||||
this.searchMode = SearchMode.youtubeMusic,
|
||||
this.skipNonMusic = true,
|
||||
this.youtubeApiType = YoutubeApiType.youtube,
|
||||
this.youtubeApiType = YoutubeApiType.piped,
|
||||
}) : super() {
|
||||
if (downloadLocation.isEmpty) {
|
||||
_getDefaultDownloadDirectory().then(
|
||||
@ -251,7 +251,7 @@ class UserPreferences extends PersistedChangeNotifier {
|
||||
|
||||
youtubeApiType = YoutubeApiType.values.firstWhere(
|
||||
(type) => type.name == map["youtubeApiType"],
|
||||
orElse: () => YoutubeApiType.youtube,
|
||||
orElse: () => YoutubeApiType.piped,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -128,6 +128,7 @@ class MkPlayerWithState extends Player {
|
||||
_playlist = null;
|
||||
_tempMedias = null;
|
||||
_playerStateStream.add(AudioPlaybackState.stopped);
|
||||
_shuffleStream.add(false);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -242,6 +243,12 @@ class MkPlayerWithState extends Player {
|
||||
play: true,
|
||||
);
|
||||
}
|
||||
|
||||
// replace in the _tempMedias if it's not null
|
||||
if (shuffled && _tempMedias != null) {
|
||||
final tempIndex = _tempMedias!.indexOf(media);
|
||||
_tempMedias![tempIndex] = Media(newUrl, extras: media.extras);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -263,7 +270,8 @@ class MkPlayerWithState extends Player {
|
||||
FutureOr<void> insert(int index, Media media) {
|
||||
if (_playlist == null ||
|
||||
index < 0 ||
|
||||
index > _playlist!.medias.length - 1) {
|
||||
(_playlist!.medias.length > 1 &&
|
||||
index > _playlist!.medias.length - 1)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,6 @@ import 'dart:io';
|
||||
import 'package:dbus/dbus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:spotube/provider/dbus_provider.dart';
|
||||
import 'package:spotube/models/spotube_track.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
@ -12,6 +11,8 @@ import 'package:spotube/services/audio_player/loop_mode.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
final dbus = DBusClient.session();
|
||||
|
||||
class _MprisMediaPlayer2 extends DBusObject {
|
||||
/// Creates a new object to expose on [path].
|
||||
_MprisMediaPlayer2() : super(DBusObjectPath('/org/mpris/MediaPlayer2')) {
|
||||
|
@ -1,10 +1,14 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotube/collections/routes.dart';
|
||||
import 'package:spotube/components/shared/dialogs/prompt_dialog.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:spotube/utils/primitive_utils.dart';
|
||||
|
||||
@ -15,6 +19,8 @@ const secureStorage = FlutterSecureStorage(
|
||||
);
|
||||
|
||||
const kKeyBoxName = "spotube_box_name";
|
||||
const kNoEncryptionWarningShownKey = "showedNoEncryptionWarning";
|
||||
const kIsUsingEncryption = "isUsingEncryption";
|
||||
String getBoxKey(String boxName) => "spotube_box_$boxName";
|
||||
|
||||
abstract class PersistedStateNotifier<T> extends StateNotifier<T> {
|
||||
@ -34,12 +40,36 @@ abstract class PersistedStateNotifier<T> extends StateNotifier<T> {
|
||||
static late LazyBox _box;
|
||||
static late LazyBox _encryptedBox;
|
||||
|
||||
static Future<void> showNoEncryptionDialog(BuildContext context) async {
|
||||
final localStorage = await SharedPreferences.getInstance();
|
||||
final wasShownAlready =
|
||||
localStorage.getBool(kNoEncryptionWarningShownKey) == true;
|
||||
|
||||
if (wasShownAlready || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
await showPromptDialog(
|
||||
context: context,
|
||||
title: context.l10n.failed_to_encrypt,
|
||||
message: context.l10n.encryption_failed_warning,
|
||||
cancelText: null,
|
||||
);
|
||||
await localStorage.setBool(kNoEncryptionWarningShownKey, true);
|
||||
}
|
||||
|
||||
static Future<String?> read(String key) async {
|
||||
final localStorage = await SharedPreferences.getInstance();
|
||||
if (kIsMacOS || kIsIOS) {
|
||||
return localStorage.getString(key);
|
||||
} else {
|
||||
return secureStorage.read(key: key);
|
||||
try {
|
||||
await localStorage.setBool(kIsUsingEncryption, true);
|
||||
return await secureStorage.read(key: key);
|
||||
} catch (e) {
|
||||
await localStorage.setBool(kIsUsingEncryption, false);
|
||||
return localStorage.getString(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,7 +79,13 @@ abstract class PersistedStateNotifier<T> extends StateNotifier<T> {
|
||||
await localStorage.setString(key, value);
|
||||
return;
|
||||
} else {
|
||||
return secureStorage.write(key: key, value: value);
|
||||
try {
|
||||
await localStorage.setBool(kIsUsingEncryption, true);
|
||||
await secureStorage.write(key: key, value: value);
|
||||
} catch (e) {
|
||||
await localStorage.setBool(kIsUsingEncryption, false);
|
||||
await localStorage.setString(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ dependencies:
|
||||
- libsecret-1-0
|
||||
- libnotify-bin
|
||||
- libjsoncpp25
|
||||
- network-manager
|
||||
|
||||
essential: false
|
||||
icon: assets/spotube-logo.png
|
||||
|
@ -12,6 +12,7 @@ requires:
|
||||
- jsoncpp
|
||||
- libsecret
|
||||
- libnotify
|
||||
- NetworkManager
|
||||
|
||||
display_name: Spotube
|
||||
|
||||
|
16
pubspec.lock
16
pubspec.lock
@ -632,6 +632,14 @@ packages:
|
||||
url: "https://github.com/KRTirtho/flutter_desktop_tools.git"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
flutter_displaymode:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_displaymode
|
||||
sha256: "42c5e9abd13d28ed74f701b60529d7f8416947e58256e6659c5550db719c57ef"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
flutter_distributor:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -873,6 +881,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.6"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: google_fonts
|
||||
sha256: "6b6f10f0ce3c42f6552d1c70d2c28d764cf22bb487f50f66cca31dcd5194f4d6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.4"
|
||||
gotrue:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -3,7 +3,10 @@ description: Open source Spotify client that doesn't require Premium nor uses El
|
||||
|
||||
publish_to: "none"
|
||||
|
||||
version: 3.0.0+19
|
||||
version: 3.0.1+20
|
||||
|
||||
homepage: https://spotube.krtirtho.dev
|
||||
repository: https://github.com/KRTirtho/spotube
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
@ -97,6 +100,8 @@ dependencies:
|
||||
duration: ^3.0.12
|
||||
disable_battery_optimization: ^1.1.0+1
|
||||
youtube_explode_dart: ^1.12.4
|
||||
flutter_displaymode: ^0.6.0
|
||||
google_fonts: ^4.0.4
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.3.2
|
||||
|
@ -1,182 +1,166 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Varibles
|
||||
fname="$(basename $0)"
|
||||
installDir='/usr/share/spotube'
|
||||
desktopFile='/usr/share/applications/spotube.desktop'
|
||||
appdata='/usr/share/appdata/spotube.appdata.xml'
|
||||
icon='/usr/share/icons/spotube/spotube-logo.png'
|
||||
symlink='/usr/bin/spotube'
|
||||
temp='/tmp/spotube-installer'
|
||||
latestVer="$(wget -qO- "https://api.github.com/repos/KRTirtho/spotube/releases/latest" \ | grep -Po '"tag_name": "\K.*?(?=")')"
|
||||
|
||||
INSTLLATION_DIR=/usr/share/spotube
|
||||
DESKTOP_FILE_PATH=/usr/share/applications/spotube.desktop
|
||||
APP_DATA_PATH=/usr/share/appdata/spotube.appdata.xml
|
||||
ICON_PATH=/usr/share/icons/spotube/spotube-logo.png
|
||||
BIN_SYMLINK_PATH=/usr/bin/spotube
|
||||
# Root check - From CAAIS (https://codeberg.org/RaptaG/CAAIS), under GPL-3.0
|
||||
function rootCheck() {
|
||||
if [ "${EUID}" -ne 0 ]; then
|
||||
echo "Error: Root permissions are required for ${fname} to work."
|
||||
echo "Please run './${fname}' for more information."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
TEMP_DIR=/tmp/spotube-installer
|
||||
|
||||
# get latest version from github api
|
||||
VERSION=$(curl --silent "https://api.github.com/repos/KRTirtho/spotube/releases/latest" \
|
||||
| grep -Po '"tag_name": "\K.*?(?=")')
|
||||
|
||||
function spotube_help(){
|
||||
# available flags are -v or --version to specify what version to download
|
||||
echo "Usage: ./install.sh [flags]"
|
||||
echo "Flags:"
|
||||
echo " -v, --version <version> Specify what version to download. Default: $VERSION"
|
||||
echo " -h, --help Show this help message"
|
||||
echo " -r, --remove Remove spotube from your system"
|
||||
# Flags
|
||||
function help(){
|
||||
echo "Usage: sudo ./${fname} [flags]"
|
||||
echo 'Flags:'
|
||||
echo ' -i, --install <version> Install any Spotube version (if not specified, the latest is installed).'
|
||||
echo ' -h, --help This help menu'
|
||||
echo ' -r, --remove Removes Spotube from your system'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# a function to check if a given command exists or not and returns bool
|
||||
# Checks whether a given command exists or not and returns bool
|
||||
function command_exists() {
|
||||
command -v "$@" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
function install_deps(){
|
||||
local DEBIAN_DEPS="curl tar mpv libappindicator3-1 gir1.2-appindicator3-0.1 libsecret-1-0 libnotify-bin libjsoncpp25"
|
||||
local RPM_DEPS="curl tar mpv libappindicator jsoncpp libsecret libnotify"
|
||||
local ARCH_DEPS="curl tar mpv libappindicator-gtk3 libsecret jsoncpp libnotify"
|
||||
local debianDeps='mpv libappindicator3-1 gir1.2-appindicator3-0.1 libsecret-1-0 libnotify-bin libjsoncpp25'
|
||||
local rpmDeps='mpv libappindicator jsoncpp libsecret libnotify'
|
||||
local archDeps='mpv libappindicator-gtk3 libsecret jsoncpp libnotify'
|
||||
|
||||
if command_exists apt; then
|
||||
sudo apt install -y $DEBIAN_DEPS
|
||||
apt install -y ${debianDeps}
|
||||
elif command_exists dnf; then
|
||||
sudo dnf install -y $RPM_DEPS
|
||||
dnf install -y ${debianDeps}
|
||||
elif command_exists yum; then
|
||||
sudo yum install -y $RPM_DEPS
|
||||
yum install -y ${rpmDeps}
|
||||
elif command_exists zypper; then
|
||||
sudo zypper install -y $RPM_DEPS
|
||||
zypper install -y ${rpmDeps}
|
||||
elif command_exists pacman; then
|
||||
sudo pacman -Sy $ARCH_DEPS
|
||||
else
|
||||
echo "Your package manager is not supported by this script. Please install the dependencies manually."
|
||||
echo "The dependencies are: curl, tar, mpv, appindicator, libsecret, jsoncpp, libnotify"
|
||||
fi
|
||||
pacman -Sy ${archDeps}
|
||||
else
|
||||
# Maybe one day
|
||||
# # Deps
|
||||
# # JsonCpp
|
||||
# wget https://github.com/open-source-parsers/jsoncpp/tarball/master -O jsoncpp.tar.gz
|
||||
# tar -xf jsoncpp.tar.gz && cd open-source-parsers-jsoncpp-*
|
||||
echo 'You have to install some dependancies manually in order for Spotube to work.'
|
||||
echo "The deps are the following: ${rpmDeps}"
|
||||
fi
|
||||
}
|
||||
|
||||
function download_extract_spotube(){
|
||||
local TAR_PATH=/tmp/spotube-$VERSION.tar.xz
|
||||
local DOWNLOAD_URL=https://github.com/KRTirtho/spotube/releases/download/v$VERSION/spotube-linux-$VERSION-x86_64.tar.xz
|
||||
local tarPath="/tmp/spotube-${ver}.tar.xz"
|
||||
local donwloadURL="https://github.com/KRTirtho/spotube/releases/download/v${ver}/spotube-linux-${ver}-x86_64.tar.xz"
|
||||
|
||||
# check if version is nightly
|
||||
|
||||
if [ "$VERSION" = "nightly" ]; then
|
||||
DOWNLOAD_URL=https://github.com/KRTirtho/spotube/releases/download/nightly/spotube-linux-nightly-x86_64.tar.xz
|
||||
if [ "${ver}" = "nightly" ]; then
|
||||
downloadURL"=https://github.com/KRTirtho/spotube/releases/download/nightly/spotube-linux-nightly-x86_64.tar.xz"
|
||||
fi
|
||||
|
||||
|
||||
rm -rf $TEMP_DIR
|
||||
mkdir -p $TEMP_DIR
|
||||
|
||||
rm -rf ${temp}
|
||||
mkdir -p ${temp}
|
||||
|
||||
# check if already exists downloaded file
|
||||
if [ -f $TAR_PATH ]; then
|
||||
echo "Found spotube-$VERSION.tar.xz in /tmp. Skipping download..."
|
||||
# Check if already exists downloaded file
|
||||
if [ -f ${tarPath} ]; then
|
||||
echo "Installation file detected. Skipping download..."
|
||||
else
|
||||
echo "Downloading spotube-$VERSION.tar.xz..."
|
||||
curl -L $DOWNLOAD_URL -o $TAR_PATH
|
||||
echo "Downloading spotube-${ver}.tar.xz..."
|
||||
wget -q ${downloadURL} -P ${tarPath}
|
||||
fi
|
||||
|
||||
# Extract the tarball
|
||||
tar -xf $TAR_PATH -C $TEMP_DIR
|
||||
tar -xf ${tarPath} -C ${temp}
|
||||
|
||||
# check if $TEMP_DIR empty or not
|
||||
|
||||
if [ ! "$(ls -A $TEMP_DIR)" ]; then
|
||||
echo "Failed to extract the tarball. Redownloading..."
|
||||
rm -f $TAR_PATH
|
||||
curl -L $DOWNLOAD_URL -o $TAR_PATH
|
||||
tar -xf $TAR_PATH -C $TEMP_DIR
|
||||
# Is $temp empty or not
|
||||
if [ ! "$(ls -A ${temp})" ]; then
|
||||
echo 'Failed to extract the tarball. Redownloading...'
|
||||
rm -f ${tarPath}
|
||||
wget -q ${downloadURL} -P ${tarPath}
|
||||
tar -xf ${tarPath} -C ${temp}
|
||||
fi
|
||||
|
||||
# checking one last time
|
||||
if [ ! "$(ls -A $TEMP_DIR)" ]; then
|
||||
echo "Failed to extract the tarball. Aborting installation..."
|
||||
# Once again
|
||||
if [ ! "$(ls -A ${temp})" ]; then
|
||||
echo 'Failed to extract the tarball. Installation aborted.'
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function install_spotube(){
|
||||
# check if exists and uninstall if user allows
|
||||
if [ -d ${installDir} ]; then
|
||||
echo -n "Spotube is already installed. Do you want to reinstall it? [y/N] "
|
||||
read reinstall
|
||||
|
||||
if [ -d $INSTLLATION_DIR ]; then
|
||||
echo "Spotube is already installed. Do you want to uninstall it and then install? [y/N]"
|
||||
read -r uninstall
|
||||
if [ "$uninstall" = "y" ] || [ "$uninstall" = "Y" ]; then
|
||||
uninstall_spotube
|
||||
else
|
||||
echo "Aborting installation..."
|
||||
exit 1
|
||||
fi
|
||||
case "${reinstall}" in
|
||||
[yY]*)
|
||||
uninstall_spotube ;;
|
||||
*)
|
||||
echo 'Aborting installation...'
|
||||
exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Move the files to /usr/share/spotube
|
||||
|
||||
sudo mkdir -p $INSTLLATION_DIR
|
||||
# Install Spotube from temp dir
|
||||
mkdir -p ${installDir}
|
||||
mv ${temp}/data ${installDir}
|
||||
mv ${temp}/lib ${installDir}
|
||||
mv ${temp}/spotube ${installDir}
|
||||
mv ${temp}/spotube.desktop ${desktopDir}
|
||||
mv ${temp}/com.github.KRTirtho.Spotube.appdata.xml ${appdata}
|
||||
mkdir -p /usr/share/icons/spotube
|
||||
mv ${temp}/spotube-logo.png ${icon}
|
||||
ln -s /usr/share/spotube/spotube ${symlink}
|
||||
|
||||
sudo mv $TEMP_DIR/data $INSTLLATION_DIR
|
||||
sudo mv $TEMP_DIR/lib $INSTLLATION_DIR
|
||||
sudo mv $TEMP_DIR/spotube $INSTLLATION_DIR
|
||||
|
||||
# Move the desktop file to /usr/share/applications
|
||||
|
||||
sudo mv $TEMP_DIR/spotube.desktop $DESKTOP_FILE_PATH
|
||||
|
||||
# Move the appdata file to /usr/share/appdata
|
||||
|
||||
sudo mv $TEMP_DIR/com.github.KRTirtho.Spotube.appdata.xml $APP_DATA_PATH
|
||||
|
||||
# Move the logo to /usr/share/icons/spotube
|
||||
|
||||
sudo mkdir -p /usr/share/icons/spotube
|
||||
|
||||
sudo mv $TEMP_DIR/spotube-logo.png $ICON_PATH
|
||||
|
||||
# Create a symlink to /usr/bin
|
||||
|
||||
sudo ln -s /usr/share/spotube/spotube $BIN_SYMLINK_PATH
|
||||
|
||||
# Clean up
|
||||
|
||||
rm -rf $TEMP_DIR
|
||||
|
||||
echo "Spotube $VERSION has been installed successfully!"
|
||||
rm -rf ${temp} # Remove temp dir
|
||||
echo "Spotube ${ver} has been installed successfully!"
|
||||
}
|
||||
|
||||
function uninstall_spotube(){
|
||||
# confirm
|
||||
echo -n "Are you sure you want to uninstall Spotube? [y/N] "
|
||||
read confirm
|
||||
|
||||
echo "Are you sure you want to uninstall Spotube?"
|
||||
echo
|
||||
echo "This will remove following files and directories:"
|
||||
echo " $INSTLLATION_DIR"
|
||||
echo " $DESKTOP_FILE_PATH"
|
||||
echo " $APP_DATA_PATH"
|
||||
echo " $ICON_PATH"
|
||||
echo " $BIN_SYMLINK_PATH"
|
||||
echo
|
||||
echo "[y/N]"
|
||||
|
||||
read -r CONFIRMATION
|
||||
|
||||
if [[ "$CONFIRMATION" != "y" ]]; then
|
||||
echo "Aborting uninstallation..."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# remove the files
|
||||
|
||||
|
||||
sudo rm -rf $INSTLLATION_DIR $DESKTOP_FILE_PATH $APP_DATA_PATH $ICON_PATH $BIN_SYMLINK_PATH
|
||||
case "${confirm}" in
|
||||
[yY]*)
|
||||
echo 'Unstalling Spotube..'
|
||||
rm -rf ${installDir} ${desktopDir} ${appdata} ${icon} ${symlink} ;;
|
||||
*)
|
||||
echo 'Aborting...'
|
||||
exit 0 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# parse arguments -v, --version, -r, --remove, -h, --help
|
||||
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case $1 in
|
||||
-v|--version) VERSION="$2"; shift ;;
|
||||
-r|--remove) uninstall_spotube; exit 0 ;;
|
||||
-h|--help) spotube_help; exit 0 ;;
|
||||
*) echo "Unknown parameter passed: $1"; spotube_help; exit 1 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
install_deps
|
||||
download_extract_spotube
|
||||
install_spotube
|
||||
case "$1" in
|
||||
-i | --install)
|
||||
if [ "$2" != "" ]; then
|
||||
ver="$2"
|
||||
else
|
||||
ver="${latestVer}"
|
||||
fi
|
||||
|
||||
rootCheck
|
||||
install_deps
|
||||
download_extract_spotube
|
||||
install_spotube
|
||||
exit 0 ;;
|
||||
-r | --remove)
|
||||
rootCheck
|
||||
uninstall_spotube
|
||||
exit 0 ;;
|
||||
-h | --help | "")
|
||||
help
|
||||
exit 0 ;;
|
||||
*)
|
||||
echo "Invalid flag '$1'"
|
||||
echo "Please run ./${fname} for more information."
|
||||
exit 1 ;;
|
||||
esac
|
||||
|
Loading…
Reference in New Issue
Block a user