diff --git a/.circleci/config.yml b/.circleci/config.yml
deleted file mode 100644
index a55310ce..00000000
--- a/.circleci/config.yml
+++ /dev/null
@@ -1,177 +0,0 @@
-version: 2.1
-
-orbs:
- gh: circleci/github-cli@2.2.0
-
-jobs:
- flutter_linux_arm:
- machine:
- image: ubuntu-2204:current
- resource_class: arm.medium
- parameters:
- version:
- type: string
- default: 3.1.1
- channel:
- type: enum
- enum:
- - release
- - nightly
- default: release
- github_run_number:
- type: string
- default: "0"
- dry_run:
- type: boolean
- default: true
- steps:
- - checkout
- - gh/setup
-
- - run:
- name: Get current date
- command: |
- echo "export CURRENT_DATE=$(date +%Y-%m-%d)" >> $BASH_ENV
-
- - run:
- name: Install dependencies
- command: |
- sudo apt-get update -y
- sudo apt-get install -y tar clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse libunwind-dev locate patchelf gir1.2-appindicator3-0.1 libappindicator3-1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev mpv libmpv-dev zip rpm
-
- - run:
- name: Install Flutter
- command: |
- git clone https://github.com/flutter/flutter.git
- cd flutter && git checkout stable && cd ..
- export PATH="$PATH:`pwd`/flutter/bin"
- flutter precache
- flutter doctor -v
-
- - run:
- name: Install AppImageTool
- command: |
- wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-aarch64.AppImage"
- chmod +x appimagetool
- mv appimagetool flutter/bin
-
- - persist_to_workspace:
- root: flutter
- paths:
- - .
-
- - when:
- condition:
- equal: [<< parameters.channel >>, nightly]
- steps:
- - run:
- name: Replace pubspec version and BUILD_VERSION Env (nightly)
- command: |
- curl -sS https://webi.sh/yq | sh
- yq -i '.version |= sub("\+\d+", "+<< parameters.channel >>.")' pubspec.yaml
- yq -i '.version += strenv(GITHUB_RUN_NUMBER)' pubspec.yaml
- echo 'export BUILD_VERSION="<< parameters.version >>+<< parameters.channel >>.<< parameters.github_run_number >>"' >> $BASH_ENV
-
- - when:
- condition:
- equal: [<< parameters.channel >>, release]
- steps:
- - run: echo 'export BUILD_VERSION="<< parameters.version >>"' >> $BASH_ENV
-
- - run:
- name: Generate .env file
- command: |
- echo "SPOTIFY_SECRETS=${SPOTIFY_SECRETS}" >> .env
-
- - run:
- name: Replace Version in files
- command: |
- sed -i 's|%{{APPDATA_RELEASE}}%||' linux/com.github.KRTirtho.Spotube.appdata.xml
- echo "build_arch: aarch64" >> linux/packaging/rpm/make_config.yaml
-
- - run:
- name: Build secrets
- command: |
- export PATH="$PATH:`pwd`/flutter/bin"
- flutter config --enable-linux-desktop
- flutter pub get
- dart run build_runner build --delete-conflicting-outputs --enable-experiment=records,patterns
-
- - run:
- name: Build Flutter app
- command: |
- export PATH="$PATH:`pwd`/flutter/bin"
- export PATH="$PATH":"$HOME/.pub-cache/bin"
- dart pub global activate flutter_distributor
- alias dpkg-deb="dpkg-deb --Zxz"
- flutter_distributor package --platform=linux --targets=deb
- flutter_distributor package --platform=linux --targets=appimage
- flutter_distributor package --platform=linux --targets=rpm
-
- - when:
- condition:
- equal: [<< parameters.channel >>, nightly]
- steps:
- - run: make tar VERSION=nightly ARCH=arm64 PKG_ARCH=aarch64
-
- - when:
- condition:
- equal: [<< parameters.channel >>, release]
- steps:
- - run: make tar VERSION=${BUILD_VERSION} ARCH=arm64 PKG_ARCH=aarch64
-
- - run:
- name: Move artifacts
- command: |
- mkdir bundle
- mv build/spotube-linux-*-aarch64.tar.xz bundle/
- mv dist/**/spotube-*-linux.deb bundle/Spotube-linux-aarch64.deb
- mv dist/**/spotube-*-linux.rpm bundle/Spotube-linux-aarch64.rpm
- mv dist/**/spotube-*-linux.AppImage bundle/Spotube-linux-aarch64.AppImage
- zip -r Spotube-linux-aarch64.zip bundle
-
- - store_artifacts:
- path: Spotube-linux-aarch64.zip
-
- - when:
- condition:
- and:
- - equal: [<< parameters.dry_run >>, false]
- - equal: [<< parameters.channel >>, release]
- steps:
- - run:
- name: Upload to release (release)
- command: gh release upload v<< parameters.version >> bundle/* --clobber
-
- - when:
- condition:
- and:
- - equal: [<< parameters.dry_run >>, false]
- - equal: [<< parameters.channel >>, nightly]
- steps:
- - run:
- name: Upload to release (nightly)
- command: gh release upload nightly bundle/* --clobber
-
-parameters:
- GHA_Actor:
- type: string
- default: ""
- GHA_Action:
- type: string
- default: ""
- GHA_Event:
- type: string
- default: ""
- GHA_Meta:
- type: string
- default: ""
-
-workflows:
- build_flutter_for_arm_workflow:
- when: << pipeline.parameters.GHA_Action >>
- jobs:
- - flutter_linux_arm:
- context:
- - org-global
- - GITHUB_CREDS
diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json
index 0b54542f..df8efa0e 100644
--- a/.fvm/fvm_config.json
+++ b/.fvm/fvm_config.json
@@ -1,4 +1,4 @@
{
- "flutterSdkVersion": "3.19.6",
+ "flutterSdkVersion": "3.22.1",
"flavors": {}
}
\ No newline at end of file
diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml
index 64cc8adc..db158029 100644
--- a/.github/workflows/pr-lint.yml
+++ b/.github/workflows/pr-lint.yml
@@ -4,7 +4,7 @@ on:
pull_request:
env:
- FLUTTER_VERSION: '3.19.6'
+ FLUTTER_VERSION: 3.22.2
jobs:
lint:
diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml
index e99aebab..5b74c9b5 100644
--- a/.github/workflows/spotube-release-binary.yml
+++ b/.github/workflows/spotube-release-binary.yml
@@ -20,7 +20,7 @@ on:
description: Dry run without uploading to release
env:
- FLUTTER_VERSION: 3.19.6
+ FLUTTER_VERSION: 3.22.1
permissions:
contents: write
diff --git a/.vscode/settings.json b/.vscode/settings.json
index de5fbd69..0ec6ca76 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -17,6 +17,7 @@
"songlink",
"speechiness",
"Spotube",
+ "titlebar",
"winget"
],
"editor.formatOnSave": true,
diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md
index e859f9e6..0cfff0ca 100644
--- a/CONTRIBUTION.md
+++ b/CONTRIBUTION.md
@@ -32,7 +32,7 @@ All types of contributions are encouraged and valued. See the [Table of Contents
This project and everyone participating in it is governed by the
[Spotube Code of Conduct](https://github.com/KRTirtho/spotube/blob/master/CODE_OF_CONDUCT.md).
By participating, you are expected to uphold this code. Please report unacceptable behavior
-to <>.
+to krtirtho@gmail.com.
## I Have a Question
diff --git a/lib/collections/intents.dart b/lib/collections/intents.dart
index 579aff18..6d6e643e 100644
--- a/lib/collections/intents.dart
+++ b/lib/collections/intents.dart
@@ -5,7 +5,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:spotube/collections/routes.dart';
-import 'package:spotube/components/player/player_controls.dart';
+import 'package:spotube/modules/player/player_controls.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/pages/home/home.dart';
import 'package:spotube/pages/library/library.dart';
diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart
index dc2e4b7c..b9e06c61 100644
--- a/lib/collections/routes.dart
+++ b/lib/collections/routes.dart
@@ -1,4 +1,3 @@
-import 'package:catcher_2/catcher_2.dart';
import 'package:flutter/foundation.dart' hide Category;
import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';
@@ -36,7 +35,7 @@ import 'package:spotube/pages/track/track.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:spotube/utils/platform.dart';
-import 'package:spotube/components/shared/spotube_page_route.dart';
+import 'package:spotube/components/spotube_page_route.dart';
import 'package:spotube/pages/artist/artist.dart';
import 'package:spotube/pages/library/library.dart';
import 'package:spotube/pages/desktop_login/login_tutorial.dart';
@@ -46,7 +45,7 @@ import 'package:spotube/pages/root/root_app.dart';
import 'package:spotube/pages/settings/settings.dart';
import 'package:spotube/pages/mobile_login/mobile_login.dart';
-final rootNavigatorKey = Catcher2.navigatorKey;
+final rootNavigatorKey = GlobalKey();
final shellRouteNavigatorKey = GlobalKey();
final routerProvider = Provider((ref) {
return GoRouter(
diff --git a/lib/components/shared/adaptive/adaptive_list_tile.dart b/lib/components/adaptive/adaptive_list_tile.dart
similarity index 100%
rename from lib/components/shared/adaptive/adaptive_list_tile.dart
rename to lib/components/adaptive/adaptive_list_tile.dart
diff --git a/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart b/lib/components/adaptive/adaptive_pop_sheet_list.dart
similarity index 97%
rename from lib/components/shared/adaptive/adaptive_pop_sheet_list.dart
rename to lib/components/adaptive/adaptive_pop_sheet_list.dart
index 21f56a22..1686801c 100644
--- a/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart
+++ b/lib/components/adaptive/adaptive_pop_sheet_list.dart
@@ -226,7 +226,10 @@ class _AdaptivePopSheetListItem extends StatelessWidget {
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
- child: IgnorePointer(child: item),
+ child: IconTheme.merge(
+ data: const IconThemeData(opacity: 1),
+ child: IgnorePointer(child: item),
+ ),
),
);
}
diff --git a/lib/components/shared/adaptive/adaptive_popup_menu_button.dart b/lib/components/adaptive/adaptive_popup_menu_button.dart
similarity index 100%
rename from lib/components/shared/adaptive/adaptive_popup_menu_button.dart
rename to lib/components/adaptive/adaptive_popup_menu_button.dart
diff --git a/lib/components/shared/adaptive/adaptive_select_tile.dart b/lib/components/adaptive/adaptive_select_tile.dart
similarity index 100%
rename from lib/components/shared/adaptive/adaptive_select_tile.dart
rename to lib/components/adaptive/adaptive_select_tile.dart
diff --git a/lib/components/shared/animated_gradient.dart b/lib/components/animated_gradient.dart
similarity index 100%
rename from lib/components/shared/animated_gradient.dart
rename to lib/components/animated_gradient.dart
diff --git a/lib/components/shared/bordered_text.dart b/lib/components/bordered_text.dart
similarity index 100%
rename from lib/components/shared/bordered_text.dart
rename to lib/components/bordered_text.dart
diff --git a/lib/components/shared/compact_search.dart b/lib/components/compact_search.dart
similarity index 100%
rename from lib/components/shared/compact_search.dart
rename to lib/components/compact_search.dart
diff --git a/lib/components/shared/dialogs/confirm_download_dialog.dart b/lib/components/dialogs/confirm_download_dialog.dart
similarity index 97%
rename from lib/components/shared/dialogs/confirm_download_dialog.dart
rename to lib/components/dialogs/confirm_download_dialog.dart
index 486310a7..897c64cb 100644
--- a/lib/components/shared/dialogs/confirm_download_dialog.dart
+++ b/lib/components/dialogs/confirm_download_dialog.dart
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
-import 'package:spotube/components/shared/image/universal_image.dart';
+import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
diff --git a/lib/components/shared/dialogs/piped_down_dialog.dart b/lib/components/dialogs/piped_down_dialog.dart
similarity index 100%
rename from lib/components/shared/dialogs/piped_down_dialog.dart
rename to lib/components/dialogs/piped_down_dialog.dart
diff --git a/lib/components/shared/dialogs/playlist_add_track_dialog.dart b/lib/components/dialogs/playlist_add_track_dialog.dart
similarity index 96%
rename from lib/components/shared/dialogs/playlist_add_track_dialog.dart
rename to lib/components/dialogs/playlist_add_track_dialog.dart
index 5d493a68..5af9c9e4 100644
--- a/lib/components/shared/dialogs/playlist_add_track_dialog.dart
+++ b/lib/components/dialogs/playlist_add_track_dialog.dart
@@ -4,8 +4,8 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
-import 'package:spotube/components/playlist/playlist_create_dialog.dart';
-import 'package:spotube/components/shared/image/universal_image.dart';
+import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
+import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/provider/spotify/spotify.dart';
diff --git a/lib/components/shared/dialogs/prompt_dialog.dart b/lib/components/dialogs/prompt_dialog.dart
similarity index 100%
rename from lib/components/shared/dialogs/prompt_dialog.dart
rename to lib/components/dialogs/prompt_dialog.dart
diff --git a/lib/components/shared/dialogs/replace_downloaded_dialog.dart b/lib/components/dialogs/replace_downloaded_dialog.dart
similarity index 100%
rename from lib/components/shared/dialogs/replace_downloaded_dialog.dart
rename to lib/components/dialogs/replace_downloaded_dialog.dart
diff --git a/lib/components/shared/dialogs/select_device_dialog.dart b/lib/components/dialogs/select_device_dialog.dart
similarity index 100%
rename from lib/components/shared/dialogs/select_device_dialog.dart
rename to lib/components/dialogs/select_device_dialog.dart
diff --git a/lib/components/shared/dialogs/track_details_dialog.dart b/lib/components/dialogs/track_details_dialog.dart
similarity index 96%
rename from lib/components/shared/dialogs/track_details_dialog.dart
rename to lib/components/dialogs/track_details_dialog.dart
index da2a140b..2495863c 100644
--- a/lib/components/shared/dialogs/track_details_dialog.dart
+++ b/lib/components/dialogs/track_details_dialog.dart
@@ -2,9 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
-import 'package:spotube/components/shared/links/artist_link.dart';
-import 'package:spotube/components/shared/links/hyper_link.dart';
-import 'package:spotube/components/shared/links/link_text.dart';
+import 'package:spotube/components/links/artist_link.dart';
+import 'package:spotube/components/links/hyper_link.dart';
+import 'package:spotube/components/links/link_text.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
diff --git a/lib/components/shared/expandable_search/expandable_search.dart b/lib/components/expandable_search/expandable_search.dart
similarity index 100%
rename from lib/components/shared/expandable_search/expandable_search.dart
rename to lib/components/expandable_search/expandable_search.dart
diff --git a/lib/components/shared/fallbacks/anonymous_fallback.dart b/lib/components/fallbacks/anonymous_fallback.dart
similarity index 100%
rename from lib/components/shared/fallbacks/anonymous_fallback.dart
rename to lib/components/fallbacks/anonymous_fallback.dart
diff --git a/lib/components/shared/fallbacks/not_found.dart b/lib/components/fallbacks/not_found.dart
similarity index 100%
rename from lib/components/shared/fallbacks/not_found.dart
rename to lib/components/fallbacks/not_found.dart
diff --git a/lib/components/shared/heart_button.dart b/lib/components/heart_button/heart_button.dart
similarity index 70%
rename from lib/components/shared/heart_button.dart
rename to lib/components/heart_button/heart_button.dart
index c296d7a9..8222b8e6 100644
--- a/lib/components/shared/heart_button.dart
+++ b/lib/components/heart_button/heart_button.dart
@@ -1,11 +1,10 @@
import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
+import 'package:spotube/components/heart_button/use_track_toggle_like.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart';
-import 'package:spotube/provider/scrobbler_provider.dart';
import 'package:spotube/provider/spotify/spotify.dart';
class HeartButton extends HookConsumerWidget {
@@ -55,38 +54,6 @@ class HeartButton extends HookConsumerWidget {
}
}
-typedef UseTrackToggleLike = ({
- bool isLiked,
- Future Function(Track track) toggleTrackLike,
-});
-
-UseTrackToggleLike useTrackToggleLike(Track track, WidgetRef ref) {
- final savedTracks = ref.watch(likedTracksProvider);
- final savedTracksNotifier = ref.watch(likedTracksProvider.notifier);
-
- final isLiked = useMemoized(
- () =>
- savedTracks.asData?.value.any((element) => element.id == track.id) ??
- false,
- [savedTracks.asData?.value, track.id],
- );
-
- final scrobblerNotifier = ref.read(scrobblerProvider.notifier);
-
- return (
- isLiked: isLiked,
- toggleTrackLike: (track) async {
- await savedTracksNotifier.toggleFavorite(track);
-
- if (!isLiked) {
- await scrobblerNotifier.love(track);
- } else {
- await scrobblerNotifier.unlove(track);
- }
- },
- );
-}
-
class TrackHeartButton extends HookConsumerWidget {
final Track track;
const TrackHeartButton({
diff --git a/lib/components/heart_button/use_track_toggle_like.dart b/lib/components/heart_button/use_track_toggle_like.dart
new file mode 100644
index 00000000..2a886feb
--- /dev/null
+++ b/lib/components/heart_button/use_track_toggle_like.dart
@@ -0,0 +1,37 @@
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:spotify/spotify.dart';
+import 'package:spotube/provider/scrobbler_provider.dart';
+import 'package:spotube/provider/spotify/spotify.dart';
+
+typedef UseTrackToggleLike = ({
+ bool isLiked,
+ Future Function(Track track) toggleTrackLike,
+});
+
+UseTrackToggleLike useTrackToggleLike(Track track, WidgetRef ref) {
+ final savedTracks = ref.watch(likedTracksProvider);
+ final savedTracksNotifier = ref.watch(likedTracksProvider.notifier);
+
+ final isLiked = useMemoized(
+ () =>
+ savedTracks.asData?.value.any((element) => element.id == track.id) ??
+ false,
+ [savedTracks.asData?.value, track.id],
+ );
+
+ final scrobblerNotifier = ref.read(scrobblerProvider.notifier);
+
+ return (
+ isLiked: isLiked,
+ toggleTrackLike: (track) async {
+ await savedTracksNotifier.toggleFavorite(track);
+
+ if (!isLiked) {
+ await scrobblerNotifier.love(track);
+ } else {
+ await scrobblerNotifier.unlove(track);
+ }
+ },
+ );
+}
diff --git a/lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart b/lib/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart
similarity index 95%
rename from lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart
rename to lib/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart
index 291950bb..16204952 100644
--- a/lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart
+++ b/lib/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart
@@ -5,9 +5,9 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/fake.dart';
-import 'package:spotube/components/album/album_card.dart';
-import 'package:spotube/components/artist/artist_card.dart';
-import 'package:spotube/components/playlist/playlist_card.dart';
+import 'package:spotube/modules/album/album_card.dart';
+import 'package:spotube/modules/artist/artist_card.dart';
+import 'package:spotube/modules/playlist/playlist_card.dart';
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
diff --git a/lib/components/shared/hover_builder.dart b/lib/components/hover_builder.dart
similarity index 100%
rename from lib/components/shared/hover_builder.dart
rename to lib/components/hover_builder.dart
diff --git a/lib/components/shared/image/universal_image.dart b/lib/components/image/universal_image.dart
similarity index 100%
rename from lib/components/shared/image/universal_image.dart
rename to lib/components/image/universal_image.dart
diff --git a/lib/components/shared/inter_scrollbar/inter_scrollbar.dart b/lib/components/inter_scrollbar/inter_scrollbar.dart
similarity index 100%
rename from lib/components/shared/inter_scrollbar/inter_scrollbar.dart
rename to lib/components/inter_scrollbar/inter_scrollbar.dart
diff --git a/lib/components/shared/links/anchor_button.dart b/lib/components/links/anchor_button.dart
similarity index 100%
rename from lib/components/shared/links/anchor_button.dart
rename to lib/components/links/anchor_button.dart
diff --git a/lib/components/shared/links/artist_link.dart b/lib/components/links/artist_link.dart
similarity index 96%
rename from lib/components/shared/links/artist_link.dart
rename to lib/components/links/artist_link.dart
index 5236a061..47ddecd8 100644
--- a/lib/components/shared/links/artist_link.dart
+++ b/lib/components/links/artist_link.dart
@@ -1,6 +1,6 @@
import 'package:flutter/widgets.dart';
import 'package:spotify/spotify.dart';
-import 'package:spotube/components/shared/links/anchor_button.dart';
+import 'package:spotube/components/links/anchor_button.dart';
import 'package:spotube/pages/artist/artist.dart';
import 'package:spotube/utils/service_utils.dart';
diff --git a/lib/components/shared/links/hyper_link.dart b/lib/components/links/hyper_link.dart
similarity index 92%
rename from lib/components/shared/links/hyper_link.dart
rename to lib/components/links/hyper_link.dart
index f84517b4..32d715e0 100644
--- a/lib/components/shared/links/hyper_link.dart
+++ b/lib/components/links/hyper_link.dart
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
-import 'package:spotube/components/shared/links/anchor_button.dart';
+import 'package:spotube/components/links/anchor_button.dart';
import 'package:url_launcher/url_launcher_string.dart';
class Hyperlink extends StatelessWidget {
diff --git a/lib/components/shared/links/link_text.dart b/lib/components/links/link_text.dart
similarity index 93%
rename from lib/components/shared/links/link_text.dart
rename to lib/components/links/link_text.dart
index db7b6358..0cab71d0 100644
--- a/lib/components/shared/links/link_text.dart
+++ b/lib/components/links/link_text.dart
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
-import 'package:spotube/components/shared/links/anchor_button.dart';
+import 'package:spotube/components/links/anchor_button.dart';
import 'package:spotube/utils/service_utils.dart';
class LinkText extends StatelessWidget {
diff --git a/lib/components/shared/panels/controller.dart b/lib/components/panels/controller.dart
similarity index 99%
rename from lib/components/shared/panels/controller.dart
rename to lib/components/panels/controller.dart
index 65c2444e..834e9ce6 100644
--- a/lib/components/shared/panels/controller.dart
+++ b/lib/components/panels/controller.dart
@@ -1,4 +1,4 @@
-part of './sliding_up_panel.dart';
+part of 'sliding_up_panel.dart';
class PanelController extends ChangeNotifier {
SlidingUpPanelState? _panelState;
diff --git a/lib/components/shared/panels/helpers.dart b/lib/components/panels/helpers.dart
similarity index 98%
rename from lib/components/shared/panels/helpers.dart
rename to lib/components/panels/helpers.dart
index 6d0dde31..d79fa97c 100644
--- a/lib/components/shared/panels/helpers.dart
+++ b/lib/components/panels/helpers.dart
@@ -1,4 +1,4 @@
-part of "./sliding_up_panel.dart";
+part of "sliding_up_panel.dart";
/// if you want to prevent the panel from being dragged using the widget,
/// wrap the widget with this
diff --git a/lib/components/shared/panels/sliding_up_panel.dart b/lib/components/panels/sliding_up_panel.dart
similarity index 100%
rename from lib/components/shared/panels/sliding_up_panel.dart
rename to lib/components/panels/sliding_up_panel.dart
diff --git a/lib/components/shared/playbutton_card.dart b/lib/components/playbutton_card.dart
similarity index 98%
rename from lib/components/shared/playbutton_card.dart
rename to lib/components/playbutton_card.dart
index 80a27eb0..ffd91cd2 100644
--- a/lib/components/shared/playbutton_card.dart
+++ b/lib/components/playbutton_card.dart
@@ -5,8 +5,8 @@ import 'package:gap/gap.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/spotube_icons.dart';
-import 'package:spotube/components/shared/hover_builder.dart';
-import 'package:spotube/components/shared/image/universal_image.dart';
+import 'package:spotube/components/hover_builder.dart';
+import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
import 'package:spotube/hooks/utils/use_brightness_value.dart';
diff --git a/lib/components/shared/page_window_title_bar.dart b/lib/components/shared/page_window_title_bar.dart
deleted file mode 100644
index c5fc11e7..00000000
--- a/lib/components/shared/page_window_title_bar.dart
+++ /dev/null
@@ -1,653 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
-import 'package:spotube/utils/platform.dart';
-import 'package:titlebar_buttons/titlebar_buttons.dart';
-import 'dart:math';
-import 'package:flutter/foundation.dart' show kIsWeb;
-import 'dart:io' show Platform;
-
-import 'package:window_manager/window_manager.dart';
-
-class PageWindowTitleBar extends StatefulHookConsumerWidget
- implements PreferredSizeWidget {
- final Widget? leading;
- final bool automaticallyImplyLeading;
- final List? actions;
- final Color? backgroundColor;
- final Color? foregroundColor;
- final IconThemeData? actionsIconTheme;
- final bool? centerTitle;
- final double? titleSpacing;
- final double toolbarOpacity;
- final double? leadingWidth;
- final TextStyle? toolbarTextStyle;
- final TextStyle? titleTextStyle;
- final double? titleWidth;
- final Widget? title;
-
- final bool _sliver;
-
- const PageWindowTitleBar({
- super.key,
- this.actions,
- this.title,
- this.toolbarOpacity = 1,
- this.backgroundColor,
- this.actionsIconTheme,
- this.automaticallyImplyLeading = false,
- this.centerTitle,
- this.foregroundColor,
- this.leading,
- this.leadingWidth,
- this.titleSpacing,
- this.titleTextStyle,
- this.titleWidth,
- this.toolbarTextStyle,
- }) : _sliver = false,
- pinned = false,
- floating = false,
- snap = false,
- stretch = false;
-
- final bool pinned;
- final bool floating;
- final bool snap;
- final bool stretch;
-
- const PageWindowTitleBar.sliver({
- super.key,
- this.actions,
- this.title,
- this.backgroundColor,
- this.actionsIconTheme,
- this.automaticallyImplyLeading = false,
- this.centerTitle,
- this.foregroundColor,
- this.leading,
- this.leadingWidth,
- this.titleSpacing,
- this.titleTextStyle,
- this.titleWidth,
- this.toolbarTextStyle,
- this.pinned = false,
- this.floating = false,
- this.snap = false,
- this.stretch = false,
- }) : _sliver = true,
- toolbarOpacity = 1;
-
- @override
- Size get preferredSize => const Size.fromHeight(kToolbarHeight);
-
- @override
- ConsumerState createState() => _PageWindowTitleBarState();
-}
-
-class _PageWindowTitleBarState extends ConsumerState {
- void onDrag(details) {
- final systemTitleBar =
- ref.read(userPreferencesProvider.select((s) => s.systemTitleBar));
- if (kIsDesktop && !systemTitleBar) {
- windowManager.startDragging();
- }
- }
-
- @override
- Widget build(BuildContext context) {
- final mediaQuery = MediaQuery.of(context);
-
- if (widget._sliver) {
- return SliverLayoutBuilder(
- builder: (context, constraints) {
- final hasFullscreen =
- mediaQuery.size.width == constraints.crossAxisExtent;
- final hasLeadingOrCanPop =
- widget.leading != null || Navigator.canPop(context);
-
- return SliverPadding(
- padding: EdgeInsets.only(
- left: kIsMacOS && hasFullscreen && hasLeadingOrCanPop ? 65 : 0,
- ),
- sliver: SliverAppBar(
- leading: widget.leading,
- automaticallyImplyLeading: widget.automaticallyImplyLeading,
- actions: [
- ...?widget.actions,
- WindowTitleBarButtons(foregroundColor: widget.foregroundColor),
- ],
- backgroundColor: widget.backgroundColor,
- foregroundColor: widget.foregroundColor,
- actionsIconTheme: widget.actionsIconTheme,
- centerTitle: widget.centerTitle,
- titleSpacing: widget.titleSpacing,
- leadingWidth: widget.leadingWidth,
- toolbarTextStyle: widget.toolbarTextStyle,
- titleTextStyle: widget.titleTextStyle,
- title: SizedBox(
- width: double.infinity, // workaround to force dragging
- child: widget.title ?? const Text(""),
- ),
- pinned: widget.pinned,
- floating: widget.floating,
- snap: widget.snap,
- stretch: widget.stretch,
- ),
- );
- },
- );
- }
-
- return LayoutBuilder(builder: (context, constrains) {
- final hasFullscreen = mediaQuery.size.width == constrains.maxWidth;
- final hasLeadingOrCanPop =
- widget.leading != null || Navigator.canPop(context);
-
- return GestureDetector(
- onHorizontalDragStart: onDrag,
- onVerticalDragStart: onDrag,
- child: Padding(
- padding: EdgeInsets.only(
- left: kIsMacOS && hasFullscreen && hasLeadingOrCanPop ? 65 : 0,
- ),
- child: AppBar(
- leading: widget.leading,
- automaticallyImplyLeading: widget.automaticallyImplyLeading,
- actions: [
- ...?widget.actions,
- WindowTitleBarButtons(foregroundColor: widget.foregroundColor),
- ],
- backgroundColor: widget.backgroundColor,
- foregroundColor: widget.foregroundColor,
- actionsIconTheme: widget.actionsIconTheme,
- centerTitle: widget.centerTitle,
- titleSpacing: widget.titleSpacing,
- toolbarOpacity: widget.toolbarOpacity,
- leadingWidth: widget.leadingWidth,
- toolbarTextStyle: widget.toolbarTextStyle,
- titleTextStyle: widget.titleTextStyle,
- title: SizedBox(
- width: double.infinity, // workaround to force dragging
- child: widget.title ?? const Text(""),
- ),
- scrolledUnderElevation: 0,
- shadowColor: Colors.transparent,
- forceMaterialTransparency: true,
- elevation: 0,
- ),
- ),
- );
- });
- }
-}
-
-class WindowTitleBarButtons extends HookConsumerWidget {
- final Color? foregroundColor;
- const WindowTitleBarButtons({
- super.key,
- this.foregroundColor,
- });
-
- @override
- Widget build(BuildContext context, ref) {
- final preferences = ref.watch(userPreferencesProvider);
- final isMaximized = useState(null);
- const type = ThemeType.auto;
-
- Future onClose() async {
- await windowManager.close();
- }
-
- useEffect(() {
- if (kIsDesktop) {
- windowManager.isMaximized().then((value) {
- isMaximized.value = value;
- });
- }
- return null;
- }, []);
-
- if (!kIsDesktop || kIsMacOS || preferences.systemTitleBar) {
- return const SizedBox.shrink();
- }
-
- if (kIsWindows) {
- final theme = Theme.of(context);
- final colors = WindowButtonColors(
- normal: Colors.transparent,
- iconNormal: foregroundColor ?? theme.colorScheme.onBackground,
- mouseOver: theme.colorScheme.onBackground.withOpacity(0.1),
- mouseDown: theme.colorScheme.onBackground.withOpacity(0.2),
- iconMouseOver: theme.colorScheme.onBackground,
- iconMouseDown: theme.colorScheme.onBackground,
- );
-
- final closeColors = WindowButtonColors(
- normal: Colors.transparent,
- iconNormal: foregroundColor ?? theme.colorScheme.onBackground,
- mouseOver: Colors.red,
- mouseDown: Colors.red[800]!,
- iconMouseOver: Colors.white,
- iconMouseDown: Colors.black,
- );
-
- return Padding(
- padding: const EdgeInsets.only(bottom: 25),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- MinimizeWindowButton(
- onPressed: windowManager.minimize,
- colors: colors,
- ),
- if (isMaximized.value != true)
- MaximizeWindowButton(
- colors: colors,
- onPressed: () {
- windowManager.maximize();
- isMaximized.value = true;
- },
- )
- else
- RestoreWindowButton(
- colors: colors,
- onPressed: () {
- windowManager.unmaximize();
- isMaximized.value = false;
- },
- ),
- CloseWindowButton(
- colors: closeColors,
- onPressed: onClose,
- ),
- ],
- ),
- );
- }
-
- return Padding(
- padding: const EdgeInsets.only(bottom: 20, left: 10),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- DecoratedMinimizeButton(
- type: type,
- onPressed: windowManager.minimize,
- ),
- DecoratedMaximizeButton(
- type: type,
- onPressed: () async {
- if (await windowManager.isMaximized()) {
- await windowManager.unmaximize();
- isMaximized.value = false;
- } else {
- await windowManager.maximize();
- isMaximized.value = true;
- }
- },
- ),
- DecoratedCloseButton(
- type: type,
- onPressed: onClose,
- ),
- ],
- ),
- );
- }
-}
-
-typedef WindowButtonIconBuilder = Widget Function(
- WindowButtonContext buttonContext);
-typedef WindowButtonBuilder = Widget Function(
- WindowButtonContext buttonContext, Widget icon);
-
-class WindowButtonContext {
- BuildContext context;
- MouseState mouseState;
- Color? backgroundColor;
- Color iconColor;
- WindowButtonContext(
- {required this.context,
- required this.mouseState,
- this.backgroundColor,
- required this.iconColor});
-}
-
-class WindowButtonColors {
- late Color normal;
- late Color mouseOver;
- late Color mouseDown;
- late Color iconNormal;
- late Color iconMouseOver;
- late Color iconMouseDown;
- WindowButtonColors(
- {Color? normal,
- Color? mouseOver,
- Color? mouseDown,
- Color? iconNormal,
- Color? iconMouseOver,
- Color? iconMouseDown}) {
- this.normal = normal ?? _defaultButtonColors.normal;
- this.mouseOver = mouseOver ?? _defaultButtonColors.mouseOver;
- this.mouseDown = mouseDown ?? _defaultButtonColors.mouseDown;
- this.iconNormal = iconNormal ?? _defaultButtonColors.iconNormal;
- this.iconMouseOver = iconMouseOver ?? _defaultButtonColors.iconMouseOver;
- this.iconMouseDown = iconMouseDown ?? _defaultButtonColors.iconMouseDown;
- }
-}
-
-final _defaultButtonColors = WindowButtonColors(
- normal: Colors.transparent,
- iconNormal: const Color(0xFF805306),
- mouseOver: const Color(0xFF404040),
- mouseDown: const Color(0xFF202020),
- iconMouseOver: const Color(0xFFFFFFFF),
- iconMouseDown: const Color(0xFFF0F0F0),
-);
-
-class WindowButton extends StatelessWidget {
- final WindowButtonBuilder? builder;
- final WindowButtonIconBuilder? iconBuilder;
- late final WindowButtonColors colors;
- final bool animate;
- final EdgeInsets? padding;
- final VoidCallback? onPressed;
-
- WindowButton(
- {super.key,
- WindowButtonColors? colors,
- this.builder,
- @required this.iconBuilder,
- this.padding,
- this.onPressed,
- this.animate = false}) {
- this.colors = colors ?? _defaultButtonColors;
- }
-
- Color getBackgroundColor(MouseState mouseState) {
- if (mouseState.isMouseDown) return colors.mouseDown;
- if (mouseState.isMouseOver) return colors.mouseOver;
- return colors.normal;
- }
-
- Color getIconColor(MouseState mouseState) {
- if (mouseState.isMouseDown) return colors.iconMouseDown;
- if (mouseState.isMouseOver) return colors.iconMouseOver;
- return colors.iconNormal;
- }
-
- @override
- Widget build(BuildContext context) {
- if (kIsWeb) {
- return Container();
- } else {
- // Don't show button on macOS
- if (Platform.isMacOS) {
- return Container();
- }
- }
-
- return MouseStateBuilder(
- builder: (context, mouseState) {
- WindowButtonContext buttonContext = WindowButtonContext(
- mouseState: mouseState,
- context: context,
- backgroundColor: getBackgroundColor(mouseState),
- iconColor: getIconColor(mouseState));
-
- var icon =
- (iconBuilder != null) ? iconBuilder!(buttonContext) : Container();
-
- var fadeOutColor =
- getBackgroundColor(MouseState()..isMouseOver = true).withOpacity(0);
- var padding = this.padding ?? const EdgeInsets.all(10);
- var animationMs =
- mouseState.isMouseOver ? (animate ? 100 : 0) : (animate ? 200 : 0);
- Widget iconWithPadding = Padding(padding: padding, child: icon);
- iconWithPadding = AnimatedContainer(
- curve: Curves.easeOut,
- duration: Duration(milliseconds: animationMs),
- color: buttonContext.backgroundColor ?? fadeOutColor,
- child: iconWithPadding);
- var button =
- (builder != null) ? builder!(buttonContext, icon) : iconWithPadding;
- return SizedBox(
- width: 45,
- height: 32,
- child: button,
- );
- },
- onPressed: () {
- if (onPressed != null) onPressed!();
- },
- );
- }
-}
-
-class MinimizeWindowButton extends WindowButton {
- MinimizeWindowButton(
- {super.key, super.colors, super.onPressed, bool? animate})
- : super(
- animate: animate ?? false,
- iconBuilder: (buttonContext) =>
- MinimizeIcon(color: buttonContext.iconColor),
- );
-}
-
-class MaximizeWindowButton extends WindowButton {
- MaximizeWindowButton(
- {super.key, super.colors, super.onPressed, bool? animate})
- : super(
- animate: animate ?? false,
- iconBuilder: (buttonContext) =>
- MaximizeIcon(color: buttonContext.iconColor),
- );
-}
-
-class RestoreWindowButton extends WindowButton {
- RestoreWindowButton({super.key, super.colors, super.onPressed, bool? animate})
- : super(
- animate: animate ?? false,
- iconBuilder: (buttonContext) =>
- RestoreIcon(color: buttonContext.iconColor),
- );
-}
-
-final _defaultCloseButtonColors = WindowButtonColors(
- mouseOver: const Color(0xFFD32F2F),
- mouseDown: const Color(0xFFB71C1C),
- iconNormal: const Color(0xFF805306),
- iconMouseOver: const Color(0xFFFFFFFF));
-
-class CloseWindowButton extends WindowButton {
- CloseWindowButton(
- {super.key, WindowButtonColors? colors, super.onPressed, bool? animate})
- : super(
- colors: colors ?? _defaultCloseButtonColors,
- animate: animate ?? false,
- iconBuilder: (buttonContext) =>
- CloseIcon(color: buttonContext.iconColor),
- );
-}
-
-// Switched to CustomPaint icons by https://github.com/esDotDev
-
-/// Close
-class CloseIcon extends StatelessWidget {
- final Color color;
- const CloseIcon({super.key, required this.color});
- @override
- Widget build(BuildContext context) => Align(
- alignment: Alignment.topLeft,
- child: Stack(children: [
- // Use rotated containers instead of a painter because it renders slightly crisper than a painter for some reason.
- Transform.rotate(
- angle: pi * .25,
- child:
- Center(child: Container(width: 14, height: 1, color: color))),
- Transform.rotate(
- angle: pi * -.25,
- child:
- Center(child: Container(width: 14, height: 1, color: color))),
- ]),
- );
-}
-
-/// Maximize
-class MaximizeIcon extends StatelessWidget {
- final Color color;
- const MaximizeIcon({super.key, required this.color});
- @override
- Widget build(BuildContext context) => _AlignedPaint(_MaximizePainter(color));
-}
-
-class _MaximizePainter extends _IconPainter {
- _MaximizePainter(super.color);
- @override
- void paint(Canvas canvas, Size size) {
- Paint p = getPaint(color);
- canvas.drawRect(Rect.fromLTRB(0, 0, size.width - 1, size.height - 1), p);
- }
-}
-
-/// Restore
-class RestoreIcon extends StatelessWidget {
- final Color color;
- const RestoreIcon({
- super.key,
- required this.color,
- });
- @override
- Widget build(BuildContext context) => _AlignedPaint(_RestorePainter(color));
-}
-
-class _RestorePainter extends _IconPainter {
- _RestorePainter(super.color);
- @override
- void paint(Canvas canvas, Size size) {
- Paint p = getPaint(color);
- canvas.drawRect(Rect.fromLTRB(0, 2, size.width - 2, size.height), p);
- canvas.drawLine(const Offset(2, 2), const Offset(2, 0), p);
- canvas.drawLine(const Offset(2, 0), Offset(size.width, 0), p);
- canvas.drawLine(
- Offset(size.width, 0), Offset(size.width, size.height - 2), p);
- canvas.drawLine(Offset(size.width, size.height - 2),
- Offset(size.width - 2, size.height - 2), p);
- }
-}
-
-/// Minimize
-class MinimizeIcon extends StatelessWidget {
- final Color color;
- const MinimizeIcon({super.key, required this.color});
- @override
- Widget build(BuildContext context) => _AlignedPaint(_MinimizePainter(color));
-}
-
-class _MinimizePainter extends _IconPainter {
- _MinimizePainter(super.color);
- @override
- void paint(Canvas canvas, Size size) {
- Paint p = getPaint(color);
- canvas.drawLine(
- Offset(0, size.height / 2), Offset(size.width, size.height / 2), p);
- }
-}
-
-/// Helpers
-abstract class _IconPainter extends CustomPainter {
- _IconPainter(this.color);
- final Color color;
-
- @override
- bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
-}
-
-class _AlignedPaint extends StatelessWidget {
- const _AlignedPaint(this.painter);
- final CustomPainter painter;
-
- @override
- Widget build(BuildContext context) {
- return Align(
- alignment: Alignment.center,
- child: CustomPaint(size: const Size(10, 10), painter: painter));
- }
-}
-
-Paint getPaint(Color color, [bool isAntiAlias = false]) => Paint()
- ..color = color
- ..style = PaintingStyle.stroke
- ..isAntiAlias = isAntiAlias
- ..strokeWidth = 1;
-
-typedef MouseStateBuilderCB = Widget Function(
- BuildContext context, MouseState mouseState);
-
-class MouseState {
- bool isMouseOver = false;
- bool isMouseDown = false;
- MouseState();
- @override
- String toString() {
- return "isMouseDown: $isMouseDown - isMouseOver: $isMouseOver";
- }
-}
-
-T? _ambiguate(T? value) => value;
-
-class MouseStateBuilder extends StatefulWidget {
- final MouseStateBuilderCB builder;
- final VoidCallback? onPressed;
- const MouseStateBuilder({super.key, required this.builder, this.onPressed});
- @override
- // ignore: library_private_types_in_public_api
- _MouseStateBuilderState createState() => _MouseStateBuilderState();
-}
-
-class _MouseStateBuilderState extends State {
- late MouseState _mouseState;
- _MouseStateBuilderState() {
- _mouseState = MouseState();
- }
-
- @override
- Widget build(BuildContext context) {
- return MouseRegion(
- onEnter: (event) {
- setState(() {
- _mouseState.isMouseOver = true;
- });
- },
- onExit: (event) {
- setState(() {
- _mouseState.isMouseOver = false;
- });
- },
- child: GestureDetector(
- onTapDown: (_) {
- setState(() {
- _mouseState.isMouseDown = true;
- });
- },
- onTapCancel: () {
- setState(() {
- _mouseState.isMouseDown = false;
- });
- },
- onTap: () {
- setState(() {
- _mouseState.isMouseDown = false;
- _mouseState.isMouseOver = false;
- });
- _ambiguate(WidgetsBinding.instance)!.addPostFrameCallback((_) {
- if (widget.onPressed != null) {
- widget.onPressed!();
- }
- });
- },
- onTapUp: (_) {},
- child: widget.builder(context, _mouseState)));
- }
-}
diff --git a/lib/components/shared/shimmers/shimmer_lyrics.dart b/lib/components/shimmers/shimmer_lyrics.dart
similarity index 100%
rename from lib/components/shared/shimmers/shimmer_lyrics.dart
rename to lib/components/shimmers/shimmer_lyrics.dart
diff --git a/lib/components/shared/sort_tracks_dropdown.dart b/lib/components/sort_tracks_dropdown.dart
similarity index 94%
rename from lib/components/shared/sort_tracks_dropdown.dart
rename to lib/components/sort_tracks_dropdown.dart
index be72d689..16727013 100644
--- a/lib/components/shared/sort_tracks_dropdown.dart
+++ b/lib/components/sort_tracks_dropdown.dart
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:spotube/collections/spotube_icons.dart';
-import 'package:spotube/components/library/user_local_tracks.dart';
-import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart';
+import 'package:spotube/modules/library/user_local_tracks.dart';
+import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart';
import 'package:spotube/extensions/context.dart';
class SortTracksDropdown extends StatelessWidget {
diff --git a/lib/components/shared/spotube_page_route.dart b/lib/components/spotube_page_route.dart
similarity index 100%
rename from lib/components/shared/spotube_page_route.dart
rename to lib/components/spotube_page_route.dart
diff --git a/lib/components/shared/themed_button_tab_bar.dart b/lib/components/themed_button_tab_bar.dart
similarity index 100%
rename from lib/components/shared/themed_button_tab_bar.dart
rename to lib/components/themed_button_tab_bar.dart
diff --git a/lib/components/titlebar/mouse_state.dart b/lib/components/titlebar/mouse_state.dart
new file mode 100644
index 00000000..9af2a8b0
--- /dev/null
+++ b/lib/components/titlebar/mouse_state.dart
@@ -0,0 +1,73 @@
+import 'package:flutter/material.dart';
+
+typedef MouseStateBuilderCB = Widget Function(
+ BuildContext context, MouseState mouseState);
+
+class MouseState {
+ bool isMouseOver = false;
+ bool isMouseDown = false;
+ MouseState();
+ @override
+ String toString() {
+ return "isMouseDown: $isMouseDown - isMouseOver: $isMouseOver";
+ }
+}
+
+T? _ambiguate(T? value) => value;
+
+class MouseStateBuilder extends StatefulWidget {
+ final MouseStateBuilderCB builder;
+ final VoidCallback? onPressed;
+ const MouseStateBuilder({super.key, required this.builder, this.onPressed});
+ @override
+ // ignore: library_private_types_in_public_api
+ _MouseStateBuilderState createState() => _MouseStateBuilderState();
+}
+
+class _MouseStateBuilderState extends State {
+ late MouseState _mouseState;
+ _MouseStateBuilderState() {
+ _mouseState = MouseState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return MouseRegion(
+ onEnter: (event) {
+ setState(() {
+ _mouseState.isMouseOver = true;
+ });
+ },
+ onExit: (event) {
+ setState(() {
+ _mouseState.isMouseOver = false;
+ });
+ },
+ child: GestureDetector(
+ onTapDown: (_) {
+ setState(() {
+ _mouseState.isMouseDown = true;
+ });
+ },
+ onTapCancel: () {
+ setState(() {
+ _mouseState.isMouseDown = false;
+ });
+ },
+ onTap: () {
+ setState(() {
+ _mouseState.isMouseDown = false;
+ _mouseState.isMouseOver = false;
+ });
+ _ambiguate(WidgetsBinding.instance)!.addPostFrameCallback((_) {
+ if (widget.onPressed != null) {
+ widget.onPressed!();
+ }
+ });
+ },
+ onTapUp: (_) {},
+ child: widget.builder(context, _mouseState),
+ ),
+ );
+ }
+}
diff --git a/lib/components/titlebar/titlebar.dart b/lib/components/titlebar/titlebar.dart
new file mode 100644
index 00000000..76a5ec8a
--- /dev/null
+++ b/lib/components/titlebar/titlebar.dart
@@ -0,0 +1,179 @@
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:spotube/components/titlebar/titlebar_buttons.dart';
+import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
+import 'package:spotube/utils/platform.dart';
+
+import 'package:window_manager/window_manager.dart';
+
+class PageWindowTitleBar extends StatefulHookConsumerWidget
+ implements PreferredSizeWidget {
+ final Widget? leading;
+ final bool automaticallyImplyLeading;
+ final List? actions;
+ final Color? backgroundColor;
+ final Color? foregroundColor;
+ final IconThemeData? actionsIconTheme;
+ final bool? centerTitle;
+ final double? titleSpacing;
+ final double toolbarOpacity;
+ final double? leadingWidth;
+ final TextStyle? toolbarTextStyle;
+ final TextStyle? titleTextStyle;
+ final double? titleWidth;
+ final Widget? title;
+
+ final bool _sliver;
+
+ const PageWindowTitleBar({
+ super.key,
+ this.actions,
+ this.title,
+ this.toolbarOpacity = 1,
+ this.backgroundColor,
+ this.actionsIconTheme,
+ this.automaticallyImplyLeading = false,
+ this.centerTitle,
+ this.foregroundColor,
+ this.leading,
+ this.leadingWidth,
+ this.titleSpacing,
+ this.titleTextStyle,
+ this.titleWidth,
+ this.toolbarTextStyle,
+ }) : _sliver = false,
+ pinned = false,
+ floating = false,
+ snap = false,
+ stretch = false;
+
+ final bool pinned;
+ final bool floating;
+ final bool snap;
+ final bool stretch;
+
+ const PageWindowTitleBar.sliver({
+ super.key,
+ this.actions,
+ this.title,
+ this.backgroundColor,
+ this.actionsIconTheme,
+ this.automaticallyImplyLeading = false,
+ this.centerTitle,
+ this.foregroundColor,
+ this.leading,
+ this.leadingWidth,
+ this.titleSpacing,
+ this.titleTextStyle,
+ this.titleWidth,
+ this.toolbarTextStyle,
+ this.pinned = false,
+ this.floating = false,
+ this.snap = false,
+ this.stretch = false,
+ }) : _sliver = true,
+ toolbarOpacity = 1;
+
+ @override
+ Size get preferredSize => const Size.fromHeight(kToolbarHeight);
+
+ @override
+ ConsumerState createState() => _PageWindowTitleBarState();
+}
+
+class _PageWindowTitleBarState extends ConsumerState {
+ void onDrag(details) {
+ final systemTitleBar =
+ ref.read(userPreferencesProvider.select((s) => s.systemTitleBar));
+ if (kIsDesktop && !systemTitleBar) {
+ windowManager.startDragging();
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final mediaQuery = MediaQuery.of(context);
+
+ if (widget._sliver) {
+ return SliverLayoutBuilder(
+ builder: (context, constraints) {
+ final hasFullscreen =
+ mediaQuery.size.width == constraints.crossAxisExtent;
+ final hasLeadingOrCanPop =
+ widget.leading != null || Navigator.canPop(context);
+
+ return SliverPadding(
+ padding: EdgeInsets.only(
+ left: kIsMacOS && hasFullscreen && hasLeadingOrCanPop ? 65 : 0,
+ ),
+ sliver: SliverAppBar(
+ leading: widget.leading,
+ automaticallyImplyLeading: widget.automaticallyImplyLeading,
+ actions: [
+ ...?widget.actions,
+ WindowTitleBarButtons(foregroundColor: widget.foregroundColor),
+ ],
+ backgroundColor: widget.backgroundColor,
+ foregroundColor: widget.foregroundColor,
+ actionsIconTheme: widget.actionsIconTheme,
+ centerTitle: widget.centerTitle,
+ titleSpacing: widget.titleSpacing,
+ leadingWidth: widget.leadingWidth,
+ toolbarTextStyle: widget.toolbarTextStyle,
+ titleTextStyle: widget.titleTextStyle,
+ title: SizedBox(
+ width: double.infinity, // workaround to force dragging
+ child: widget.title ?? const Text(""),
+ ),
+ pinned: widget.pinned,
+ floating: widget.floating,
+ snap: widget.snap,
+ stretch: widget.stretch,
+ ),
+ );
+ },
+ );
+ }
+
+ return LayoutBuilder(builder: (context, constrains) {
+ final hasFullscreen = mediaQuery.size.width == constrains.maxWidth;
+ final hasLeadingOrCanPop =
+ widget.leading != null || Navigator.canPop(context);
+
+ return GestureDetector(
+ onHorizontalDragStart: onDrag,
+ onVerticalDragStart: onDrag,
+ child: Padding(
+ padding: EdgeInsets.only(
+ left: kIsMacOS && hasFullscreen && hasLeadingOrCanPop ? 65 : 0,
+ ),
+ child: AppBar(
+ leading: widget.leading,
+ automaticallyImplyLeading: widget.automaticallyImplyLeading,
+ actions: [
+ ...?widget.actions,
+ WindowTitleBarButtons(foregroundColor: widget.foregroundColor),
+ ],
+ backgroundColor: widget.backgroundColor,
+ foregroundColor: widget.foregroundColor,
+ actionsIconTheme: widget.actionsIconTheme,
+ centerTitle: widget.centerTitle,
+ titleSpacing: widget.titleSpacing,
+ toolbarOpacity: widget.toolbarOpacity,
+ leadingWidth: widget.leadingWidth,
+ toolbarTextStyle: widget.toolbarTextStyle,
+ titleTextStyle: widget.titleTextStyle,
+ title: SizedBox(
+ width: double.infinity, // workaround to force dragging
+ child: widget.title ?? const Text(""),
+ ),
+ scrolledUnderElevation: 0,
+ shadowColor: Colors.transparent,
+ forceMaterialTransparency: true,
+ elevation: 0,
+ ),
+ ),
+ );
+ });
+ }
+}
diff --git a/lib/components/titlebar/titlebar_buttons.dart b/lib/components/titlebar/titlebar_buttons.dart
new file mode 100644
index 00000000..425bf2f1
--- /dev/null
+++ b/lib/components/titlebar/titlebar_buttons.dart
@@ -0,0 +1,124 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:spotube/components/titlebar/titlebar_icon_buttons.dart';
+import 'package:spotube/components/titlebar/window_button.dart';
+import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
+import 'package:spotube/utils/platform.dart';
+import 'package:titlebar_buttons/titlebar_buttons.dart';
+import 'package:window_manager/window_manager.dart';
+
+class WindowTitleBarButtons extends HookConsumerWidget {
+ final Color? foregroundColor;
+ const WindowTitleBarButtons({
+ super.key,
+ this.foregroundColor,
+ });
+
+ @override
+ Widget build(BuildContext context, ref) {
+ final preferences = ref.watch(userPreferencesProvider);
+ final isMaximized = useState(null);
+ const type = ThemeType.auto;
+
+ Future onClose() async {
+ await windowManager.close();
+ }
+
+ useEffect(() {
+ if (kIsDesktop) {
+ windowManager.isMaximized().then((value) {
+ isMaximized.value = value;
+ });
+ }
+ return null;
+ }, []);
+
+ if (!kIsDesktop || kIsMacOS || preferences.systemTitleBar) {
+ return const SizedBox.shrink();
+ }
+
+ if (kIsWindows) {
+ final theme = Theme.of(context);
+ final colors = WindowButtonColors(
+ normal: Colors.transparent,
+ iconNormal: foregroundColor ?? theme.colorScheme.onBackground,
+ mouseOver: theme.colorScheme.onBackground.withOpacity(0.1),
+ mouseDown: theme.colorScheme.onBackground.withOpacity(0.2),
+ iconMouseOver: theme.colorScheme.onBackground,
+ iconMouseDown: theme.colorScheme.onBackground,
+ );
+
+ final closeColors = WindowButtonColors(
+ normal: Colors.transparent,
+ iconNormal: foregroundColor ?? theme.colorScheme.onBackground,
+ mouseOver: Colors.red,
+ mouseDown: Colors.red[800]!,
+ iconMouseOver: Colors.white,
+ iconMouseDown: Colors.black,
+ );
+
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 25),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ MinimizeWindowButton(
+ onPressed: windowManager.minimize,
+ colors: colors,
+ ),
+ if (isMaximized.value != true)
+ MaximizeWindowButton(
+ colors: colors,
+ onPressed: () {
+ windowManager.maximize();
+ isMaximized.value = true;
+ },
+ )
+ else
+ RestoreWindowButton(
+ colors: colors,
+ onPressed: () {
+ windowManager.unmaximize();
+ isMaximized.value = false;
+ },
+ ),
+ CloseWindowButton(
+ colors: closeColors,
+ onPressed: onClose,
+ ),
+ ],
+ ),
+ );
+ }
+
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 20, left: 10),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ DecoratedMinimizeButton(
+ type: type,
+ onPressed: windowManager.minimize,
+ ),
+ DecoratedMaximizeButton(
+ type: type,
+ onPressed: () async {
+ if (await windowManager.isMaximized()) {
+ await windowManager.unmaximize();
+ isMaximized.value = false;
+ } else {
+ await windowManager.maximize();
+ isMaximized.value = true;
+ }
+ },
+ ),
+ DecoratedCloseButton(
+ type: type,
+ onPressed: onClose,
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/components/titlebar/titlebar_icon_buttons.dart b/lib/components/titlebar/titlebar_icon_buttons.dart
new file mode 100644
index 00000000..70170262
--- /dev/null
+++ b/lib/components/titlebar/titlebar_icon_buttons.dart
@@ -0,0 +1,161 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:spotube/components/titlebar/window_button.dart';
+
+class MinimizeWindowButton extends WindowButton {
+ MinimizeWindowButton(
+ {super.key, super.colors, super.onPressed, bool? animate})
+ : super(
+ animate: animate ?? false,
+ iconBuilder: (buttonContext) =>
+ MinimizeIcon(color: buttonContext.iconColor),
+ );
+}
+
+class MaximizeWindowButton extends WindowButton {
+ MaximizeWindowButton(
+ {super.key, super.colors, super.onPressed, bool? animate})
+ : super(
+ animate: animate ?? false,
+ iconBuilder: (buttonContext) =>
+ MaximizeIcon(color: buttonContext.iconColor),
+ );
+}
+
+class RestoreWindowButton extends WindowButton {
+ RestoreWindowButton({super.key, super.colors, super.onPressed, bool? animate})
+ : super(
+ animate: animate ?? false,
+ iconBuilder: (buttonContext) =>
+ RestoreIcon(color: buttonContext.iconColor),
+ );
+}
+
+final _defaultCloseButtonColors = WindowButtonColors(
+ mouseOver: const Color(0xFFD32F2F),
+ mouseDown: const Color(0xFFB71C1C),
+ iconNormal: const Color(0xFF805306),
+ iconMouseOver: const Color(0xFFFFFFFF));
+
+class CloseWindowButton extends WindowButton {
+ CloseWindowButton(
+ {super.key, WindowButtonColors? colors, super.onPressed, bool? animate})
+ : super(
+ colors: colors ?? _defaultCloseButtonColors,
+ animate: animate ?? false,
+ iconBuilder: (buttonContext) =>
+ CloseIcon(color: buttonContext.iconColor),
+ );
+}
+
+// Switched to CustomPaint icons by https://github.com/esDotDev
+
+/// Close
+class CloseIcon extends StatelessWidget {
+ final Color color;
+ const CloseIcon({super.key, required this.color});
+ @override
+ Widget build(BuildContext context) => Align(
+ alignment: Alignment.topLeft,
+ child: Stack(children: [
+ // Use rotated containers instead of a painter because it renders slightly crisper than a painter for some reason.
+ Transform.rotate(
+ angle: pi * .25,
+ child:
+ Center(child: Container(width: 14, height: 1, color: color))),
+ Transform.rotate(
+ angle: pi * -.25,
+ child:
+ Center(child: Container(width: 14, height: 1, color: color))),
+ ]),
+ );
+}
+
+/// Maximize
+class MaximizeIcon extends StatelessWidget {
+ final Color color;
+ const MaximizeIcon({super.key, required this.color});
+ @override
+ Widget build(BuildContext context) => _AlignedPaint(_MaximizePainter(color));
+}
+
+class _MaximizePainter extends _IconPainter {
+ _MaximizePainter(super.color);
+ @override
+ void paint(Canvas canvas, Size size) {
+ Paint p = getPaint(color);
+ canvas.drawRect(Rect.fromLTRB(0, 0, size.width - 1, size.height - 1), p);
+ }
+}
+
+/// Restore
+class RestoreIcon extends StatelessWidget {
+ final Color color;
+ const RestoreIcon({
+ super.key,
+ required this.color,
+ });
+ @override
+ Widget build(BuildContext context) => _AlignedPaint(_RestorePainter(color));
+}
+
+class _RestorePainter extends _IconPainter {
+ _RestorePainter(super.color);
+ @override
+ void paint(Canvas canvas, Size size) {
+ Paint p = getPaint(color);
+ canvas.drawRect(Rect.fromLTRB(0, 2, size.width - 2, size.height), p);
+ canvas.drawLine(const Offset(2, 2), const Offset(2, 0), p);
+ canvas.drawLine(const Offset(2, 0), Offset(size.width, 0), p);
+ canvas.drawLine(
+ Offset(size.width, 0), Offset(size.width, size.height - 2), p);
+ canvas.drawLine(Offset(size.width, size.height - 2),
+ Offset(size.width - 2, size.height - 2), p);
+ }
+}
+
+/// Minimize
+class MinimizeIcon extends StatelessWidget {
+ final Color color;
+ const MinimizeIcon({super.key, required this.color});
+ @override
+ Widget build(BuildContext context) => _AlignedPaint(_MinimizePainter(color));
+}
+
+class _MinimizePainter extends _IconPainter {
+ _MinimizePainter(super.color);
+ @override
+ void paint(Canvas canvas, Size size) {
+ Paint p = getPaint(color);
+ canvas.drawLine(
+ Offset(0, size.height / 2), Offset(size.width, size.height / 2), p);
+ }
+}
+
+/// Helpers
+abstract class _IconPainter extends CustomPainter {
+ _IconPainter(this.color);
+ final Color color;
+
+ @override
+ bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
+}
+
+class _AlignedPaint extends StatelessWidget {
+ const _AlignedPaint(this.painter);
+ final CustomPainter painter;
+
+ @override
+ Widget build(BuildContext context) {
+ return Align(
+ alignment: Alignment.center,
+ child: CustomPaint(size: const Size(10, 10), painter: painter));
+ }
+}
+
+Paint getPaint(Color color, [bool isAntiAlias = false]) => Paint()
+ ..color = color
+ ..style = PaintingStyle.stroke
+ ..isAntiAlias = isAntiAlias
+ ..strokeWidth = 1;
diff --git a/lib/components/titlebar/window_button.dart b/lib/components/titlebar/window_button.dart
new file mode 100644
index 00000000..3201d191
--- /dev/null
+++ b/lib/components/titlebar/window_button.dart
@@ -0,0 +1,133 @@
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:spotube/components/titlebar/mouse_state.dart';
+
+typedef WindowButtonIconBuilder = Widget Function(
+ WindowButtonContext buttonContext);
+typedef WindowButtonBuilder = Widget Function(
+ WindowButtonContext buttonContext, Widget icon);
+
+class WindowButtonContext {
+ BuildContext context;
+ MouseState mouseState;
+ Color? backgroundColor;
+ Color iconColor;
+ WindowButtonContext(
+ {required this.context,
+ required this.mouseState,
+ this.backgroundColor,
+ required this.iconColor});
+}
+
+class WindowButtonColors {
+ late Color normal;
+ late Color mouseOver;
+ late Color mouseDown;
+ late Color iconNormal;
+ late Color iconMouseOver;
+ late Color iconMouseDown;
+ WindowButtonColors(
+ {Color? normal,
+ Color? mouseOver,
+ Color? mouseDown,
+ Color? iconNormal,
+ Color? iconMouseOver,
+ Color? iconMouseDown}) {
+ this.normal = normal ?? _defaultButtonColors.normal;
+ this.mouseOver = mouseOver ?? _defaultButtonColors.mouseOver;
+ this.mouseDown = mouseDown ?? _defaultButtonColors.mouseDown;
+ this.iconNormal = iconNormal ?? _defaultButtonColors.iconNormal;
+ this.iconMouseOver = iconMouseOver ?? _defaultButtonColors.iconMouseOver;
+ this.iconMouseDown = iconMouseDown ?? _defaultButtonColors.iconMouseDown;
+ }
+}
+
+final _defaultButtonColors = WindowButtonColors(
+ normal: Colors.transparent,
+ iconNormal: const Color(0xFF805306),
+ mouseOver: const Color(0xFF404040),
+ mouseDown: const Color(0xFF202020),
+ iconMouseOver: const Color(0xFFFFFFFF),
+ iconMouseDown: const Color(0xFFF0F0F0),
+);
+
+class WindowButton extends StatelessWidget {
+ final WindowButtonBuilder? builder;
+ final WindowButtonIconBuilder? iconBuilder;
+ late final WindowButtonColors colors;
+ final bool animate;
+ final EdgeInsets? padding;
+ final VoidCallback? onPressed;
+
+ WindowButton(
+ {super.key,
+ WindowButtonColors? colors,
+ this.builder,
+ @required this.iconBuilder,
+ this.padding,
+ this.onPressed,
+ this.animate = false}) {
+ this.colors = colors ?? _defaultButtonColors;
+ }
+
+ Color getBackgroundColor(MouseState mouseState) {
+ if (mouseState.isMouseDown) return colors.mouseDown;
+ if (mouseState.isMouseOver) return colors.mouseOver;
+ return colors.normal;
+ }
+
+ Color getIconColor(MouseState mouseState) {
+ if (mouseState.isMouseDown) return colors.iconMouseDown;
+ if (mouseState.isMouseOver) return colors.iconMouseOver;
+ return colors.iconNormal;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (kIsWeb) {
+ return Container();
+ } else {
+ // Don't show button on macOS
+ if (Platform.isMacOS) {
+ return Container();
+ }
+ }
+
+ return MouseStateBuilder(
+ builder: (context, mouseState) {
+ WindowButtonContext buttonContext = WindowButtonContext(
+ mouseState: mouseState,
+ context: context,
+ backgroundColor: getBackgroundColor(mouseState),
+ iconColor: getIconColor(mouseState));
+
+ var icon =
+ (iconBuilder != null) ? iconBuilder!(buttonContext) : Container();
+
+ var fadeOutColor =
+ getBackgroundColor(MouseState()..isMouseOver = true).withOpacity(0);
+ var padding = this.padding ?? const EdgeInsets.all(10);
+ var animationMs =
+ mouseState.isMouseOver ? (animate ? 100 : 0) : (animate ? 200 : 0);
+ Widget iconWithPadding = Padding(padding: padding, child: icon);
+ iconWithPadding = AnimatedContainer(
+ curve: Curves.easeOut,
+ duration: Duration(milliseconds: animationMs),
+ color: buttonContext.backgroundColor ?? fadeOutColor,
+ child: iconWithPadding);
+ var button =
+ (builder != null) ? builder!(buttonContext, icon) : iconWithPadding;
+ return SizedBox(
+ width: 45,
+ height: 32,
+ child: button,
+ );
+ },
+ onPressed: () {
+ if (onPressed != null) onPressed!();
+ },
+ );
+ }
+}
diff --git a/lib/components/shared/track_tile/track_options.dart b/lib/components/track_tile/track_options.dart
similarity index 96%
rename from lib/components/shared/track_tile/track_options.dart
rename to lib/components/track_tile/track_options.dart
index 4b383c47..89f6679d 100644
--- a/lib/components/shared/track_tile/track_options.dart
+++ b/lib/components/track_tile/track_options.dart
@@ -8,13 +8,13 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.dart';
-import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart';
-import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart';
-import 'package:spotube/components/shared/dialogs/prompt_dialog.dart';
-import 'package:spotube/components/shared/dialogs/track_details_dialog.dart';
-import 'package:spotube/components/shared/heart_button.dart';
-import 'package:spotube/components/shared/image/universal_image.dart';
-import 'package:spotube/components/shared/links/artist_link.dart';
+import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart';
+import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart';
+import 'package:spotube/components/dialogs/prompt_dialog.dart';
+import 'package:spotube/components/dialogs/track_details_dialog.dart';
+import 'package:spotube/components/heart_button/use_track_toggle_like.dart';
+import 'package:spotube/components/image/universal_image.dart';
+import 'package:spotube/components/links/artist_link.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart';
diff --git a/lib/components/shared/track_tile/track_tile.dart b/lib/components/track_tile/track_tile.dart
similarity index 96%
rename from lib/components/shared/track_tile/track_tile.dart
rename to lib/components/track_tile/track_tile.dart
index e3aea4de..9ba87abe 100644
--- a/lib/components/shared/track_tile/track_tile.dart
+++ b/lib/components/track_tile/track_tile.dart
@@ -7,11 +7,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
-import 'package:spotube/components/shared/hover_builder.dart';
-import 'package:spotube/components/shared/image/universal_image.dart';
-import 'package:spotube/components/shared/links/artist_link.dart';
-import 'package:spotube/components/shared/links/link_text.dart';
-import 'package:spotube/components/shared/track_tile/track_options.dart';
+import 'package:spotube/components/hover_builder.dart';
+import 'package:spotube/components/image/universal_image.dart';
+import 'package:spotube/components/links/artist_link.dart';
+import 'package:spotube/components/links/link_text.dart';
+import 'package:spotube/components/track_tile/track_options.dart';
import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/duration.dart';
diff --git a/lib/components/shared/tracks_view/sections/body/track_view_body.dart b/lib/components/tracks_view/sections/body/track_view_body.dart
similarity index 92%
rename from lib/components/shared/tracks_view/sections/body/track_view_body.dart
rename to lib/components/tracks_view/sections/body/track_view_body.dart
index c3605f33..0c3cca4e 100644
--- a/lib/components/shared/tracks_view/sections/body/track_view_body.dart
+++ b/lib/components/tracks_view/sections/body/track_view_body.dart
@@ -8,13 +8,13 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/fake.dart';
-import 'package:spotube/components/shared/dialogs/select_device_dialog.dart';
-import 'package:spotube/components/shared/expandable_search/expandable_search.dart';
-import 'package:spotube/components/shared/track_tile/track_tile.dart';
-import 'package:spotube/components/shared/tracks_view/sections/body/track_view_body_headers.dart';
-import 'package:spotube/components/shared/tracks_view/sections/body/use_is_user_playlist.dart';
-import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
-import 'package:spotube/components/shared/tracks_view/track_view_provider.dart';
+import 'package:spotube/components/dialogs/select_device_dialog.dart';
+import 'package:spotube/components/expandable_search/expandable_search.dart';
+import 'package:spotube/components/track_tile/track_tile.dart';
+import 'package:spotube/components/tracks_view/sections/body/track_view_body_headers.dart';
+import 'package:spotube/components/tracks_view/sections/body/use_is_user_playlist.dart';
+import 'package:spotube/components/tracks_view/track_view_props.dart';
+import 'package:spotube/components/tracks_view/track_view_provider.dart';
import 'package:spotube/models/connect/connect.dart';
import 'package:spotube/provider/connect/connect.dart';
import 'package:spotube/provider/history/history.dart';
diff --git a/lib/components/shared/tracks_view/sections/body/track_view_body_headers.dart b/lib/components/tracks_view/sections/body/track_view_body_headers.dart
similarity index 87%
rename from lib/components/shared/tracks_view/sections/body/track_view_body_headers.dart
rename to lib/components/tracks_view/sections/body/track_view_body_headers.dart
index 3a1538a3..564c85d0 100644
--- a/lib/components/shared/tracks_view/sections/body/track_view_body_headers.dart
+++ b/lib/components/tracks_view/sections/body/track_view_body_headers.dart
@@ -1,10 +1,10 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:spotube/components/shared/expandable_search/expandable_search.dart';
-import 'package:spotube/components/shared/sort_tracks_dropdown.dart';
-import 'package:spotube/components/shared/tracks_view/sections/body/track_view_options.dart';
-import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
-import 'package:spotube/components/shared/tracks_view/track_view_provider.dart';
+import 'package:spotube/components/expandable_search/expandable_search.dart';
+import 'package:spotube/components/sort_tracks_dropdown.dart';
+import 'package:spotube/components/tracks_view/sections/body/track_view_options.dart';
+import 'package:spotube/components/tracks_view/track_view_props.dart';
+import 'package:spotube/components/tracks_view/track_view_provider.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
diff --git a/lib/components/shared/tracks_view/sections/body/track_view_options.dart b/lib/components/tracks_view/sections/body/track_view_options.dart
similarity index 92%
rename from lib/components/shared/tracks_view/sections/body/track_view_options.dart
rename to lib/components/tracks_view/sections/body/track_view_options.dart
index c2adf38b..f004b10a 100644
--- a/lib/components/shared/tracks_view/sections/body/track_view_options.dart
+++ b/lib/components/tracks_view/sections/body/track_view_options.dart
@@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
-import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart';
-import 'package:spotube/components/shared/dialogs/confirm_download_dialog.dart';
-import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart';
-import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
-import 'package:spotube/components/shared/tracks_view/track_view_provider.dart';
+import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart';
+import 'package:spotube/components/dialogs/confirm_download_dialog.dart';
+import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart';
+import 'package:spotube/components/tracks_view/track_view_props.dart';
+import 'package:spotube/components/tracks_view/track_view_provider.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/history/history.dart';
diff --git a/lib/components/shared/tracks_view/sections/body/use_is_user_playlist.dart b/lib/components/tracks_view/sections/body/use_is_user_playlist.dart
similarity index 100%
rename from lib/components/shared/tracks_view/sections/body/use_is_user_playlist.dart
rename to lib/components/tracks_view/sections/body/use_is_user_playlist.dart
diff --git a/lib/components/shared/tracks_view/sections/header/flexible_header.dart b/lib/components/tracks_view/sections/header/flexible_header.dart
similarity index 94%
rename from lib/components/shared/tracks_view/sections/header/flexible_header.dart
rename to lib/components/tracks_view/sections/header/flexible_header.dart
index d6e71e8f..6e8fc2d1 100644
--- a/lib/components/shared/tracks_view/sections/header/flexible_header.dart
+++ b/lib/components/tracks_view/sections/header/flexible_header.dart
@@ -4,11 +4,11 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/assets.gen.dart';
-import 'package:spotube/components/shared/image/universal_image.dart';
-import 'package:spotube/components/shared/playbutton_card.dart';
-import 'package:spotube/components/shared/tracks_view/sections/header/header_actions.dart';
-import 'package:spotube/components/shared/tracks_view/sections/header/header_buttons.dart';
-import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
+import 'package:spotube/components/image/universal_image.dart';
+import 'package:spotube/components/playbutton_card.dart';
+import 'package:spotube/components/tracks_view/sections/header/header_actions.dart';
+import 'package:spotube/components/tracks_view/sections/header/header_buttons.dart';
+import 'package:spotube/components/tracks_view/track_view_props.dart';
import 'package:gap/gap.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/hooks/utils/use_palette_color.dart';
diff --git a/lib/components/shared/tracks_view/sections/header/header_actions.dart b/lib/components/tracks_view/sections/header/header_actions.dart
similarity index 92%
rename from lib/components/shared/tracks_view/sections/header/header_actions.dart
rename to lib/components/tracks_view/sections/header/header_actions.dart
index 8c1c8e15..3e0c4cc1 100644
--- a/lib/components/shared/tracks_view/sections/header/header_actions.dart
+++ b/lib/components/tracks_view/sections/header/header_actions.dart
@@ -4,10 +4,10 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
-import 'package:spotube/components/playlist/playlist_create_dialog.dart';
-import 'package:spotube/components/shared/heart_button.dart';
-import 'package:spotube/components/shared/tracks_view/sections/body/use_is_user_playlist.dart';
-import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
+import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
+import 'package:spotube/components/heart_button/heart_button.dart';
+import 'package:spotube/components/tracks_view/sections/body/use_is_user_playlist.dart';
+import 'package:spotube/components/tracks_view/track_view_props.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/history/history.dart';
diff --git a/lib/components/shared/tracks_view/sections/header/header_buttons.dart b/lib/components/tracks_view/sections/header/header_buttons.dart
similarity index 97%
rename from lib/components/shared/tracks_view/sections/header/header_buttons.dart
rename to lib/components/tracks_view/sections/header/header_buttons.dart
index 5cc442cf..aa660f01 100644
--- a/lib/components/shared/tracks_view/sections/header/header_buttons.dart
+++ b/lib/components/tracks_view/sections/header/header_buttons.dart
@@ -7,8 +7,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
-import 'package:spotube/components/shared/dialogs/select_device_dialog.dart';
-import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
+import 'package:spotube/components/dialogs/select_device_dialog.dart';
+import 'package:spotube/components/tracks_view/track_view_props.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/connect/connect.dart';
import 'package:spotube/provider/connect/connect.dart';
diff --git a/lib/components/shared/tracks_view/track_view.dart b/lib/components/tracks_view/track_view.dart
similarity index 77%
rename from lib/components/shared/tracks_view/track_view.dart
rename to lib/components/tracks_view/track_view.dart
index 03d628a8..2a3f5237 100644
--- a/lib/components/shared/tracks_view/track_view.dart
+++ b/lib/components/tracks_view/track_view.dart
@@ -3,11 +3,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:sliver_tools/sliver_tools.dart';
-import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
-import 'package:spotube/components/shared/page_window_title_bar.dart';
-import 'package:spotube/components/shared/tracks_view/sections/header/flexible_header.dart';
-import 'package:spotube/components/shared/tracks_view/sections/body/track_view_body.dart';
-import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
+import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
+import 'package:spotube/components/titlebar/titlebar.dart';
+import 'package:spotube/components/tracks_view/sections/header/flexible_header.dart';
+import 'package:spotube/components/tracks_view/sections/body/track_view_body.dart';
+import 'package:spotube/components/tracks_view/track_view_props.dart';
import 'package:spotube/utils/platform.dart';
class TrackView extends HookConsumerWidget {
diff --git a/lib/components/shared/tracks_view/track_view_props.dart b/lib/components/tracks_view/track_view_props.dart
similarity index 100%
rename from lib/components/shared/tracks_view/track_view_props.dart
rename to lib/components/tracks_view/track_view_props.dart
diff --git a/lib/components/shared/tracks_view/track_view_provider.dart b/lib/components/tracks_view/track_view_provider.dart
similarity index 95%
rename from lib/components/shared/tracks_view/track_view_provider.dart
rename to lib/components/tracks_view/track_view_provider.dart
index 14dc1136..16aa6d9c 100644
--- a/lib/components/shared/tracks_view/track_view_provider.dart
+++ b/lib/components/tracks_view/track_view_provider.dart
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
-import 'package:spotube/components/library/user_local_tracks.dart';
+import 'package:spotube/modules/library/user_local_tracks.dart';
class TrackViewNotifier extends ChangeNotifier {
List