68 Commits

Author SHA1 Message Date
9cda875605 Merge pull request #31 from sillyangel/mobile-support
YAY
2025-08-01 13:30:28 -05:00
ca92ff717e Merge branch 'dev' into mobile-support 2025-08-01 13:27:49 -05:00
fd7690b725 Add debug logging for audio source handling and API initialization; improve error handling in AudioPlayer and AudioPlayerContext 2025-08-01 18:17:09 +00:00
be2266bf3a Update .env.local with new commit SHA; comment out 'Browse' heading in LibraryPage for future reference 2025-08-01 17:29:43 +00:00
462f04c208 Merge pull request #32 from sillyangel/dependabot/npm_and_yarn/dev-380966b8c2
chore(deps-dev): bump the dev group with 3 updates
2025-07-31 17:30:58 -05:00
71de7884c3 Merge branch 'dev' into dependabot/npm_and_yarn/dev-380966b8c2 2025-07-31 17:30:42 -05:00
346d70c85b Merge pull request #36 from sillyangel/dependabot/npm_and_yarn/hookform/resolvers-5.2.0
chore(deps): bump @hookform/resolvers from 3.10.0 to 5.2.0
2025-07-31 17:30:02 -05:00
4412c38d2e Merge pull request #35 from sillyangel/dependabot/npm_and_yarn/zod-4.0.10
chore(deps): bump zod from 4.0.5 to 4.0.10
2025-07-31 17:29:47 -05:00
d7cf308470 Merge pull request #34 from sillyangel/dependabot/npm_and_yarn/radix-ui/react-progress-1.1.7
chore(deps): bump @radix-ui/react-progress from 1.1.1 to 1.1.7
2025-07-31 17:29:40 -05:00
ee956ba2e4 Merge pull request #33 from sillyangel/dependabot/npm_and_yarn/axios-1.11.0
chore(deps): bump axios from 1.8.2 to 1.11.0
2025-07-31 17:29:30 -05:00
b6fa101816 Merge pull request #37 from sillyangel/dependabot/npm_and_yarn/npm_and_yarn-373e2693b3
chore(deps): bump @eslint/plugin-kit from 0.3.3 to 0.3.4 in the npm_and_yarn group
2025-07-31 17:28:56 -05:00
dependabot[bot]
a5fe3be9fd chore(deps): bump @eslint/plugin-kit in the npm_and_yarn group
Bumps the npm_and_yarn group with 1 update: [@eslint/plugin-kit](https://github.com/eslint/rewrite/tree/HEAD/packages/plugin-kit).


Updates `@eslint/plugin-kit` from 0.3.3 to 0.3.4
- [Release notes](https://github.com/eslint/rewrite/releases)
- [Changelog](https://github.com/eslint/rewrite/blob/main/packages/plugin-kit/CHANGELOG.md)
- [Commits](https://github.com/eslint/rewrite/commits/plugin-kit-v0.3.4/packages/plugin-kit)

---
updated-dependencies:
- dependency-name: "@eslint/plugin-kit"
  dependency-version: 0.3.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-29 09:37:26 +00:00
0c32c056c6 Refactor FullScreenPlayer layout for improved readability; update button placement for favorites toggle; replace multiple divs with a single heading element; update album preview images 2025-07-28 23:28:46 +00:00
dependabot[bot]
fed1ce4ee8 chore(deps): bump @hookform/resolvers from 3.10.0 to 5.2.0
Bumps [@hookform/resolvers](https://github.com/react-hook-form/resolvers) from 3.10.0 to 5.2.0.
- [Release notes](https://github.com/react-hook-form/resolvers/releases)
- [Commits](https://github.com/react-hook-form/resolvers/compare/v3.10.0...v5.2.0)

---
updated-dependencies:
- dependency-name: "@hookform/resolvers"
  dependency-version: 5.2.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 00:27:32 +00:00
dependabot[bot]
d2a9c72f1f chore(deps): bump zod from 4.0.5 to 4.0.10
Bumps [zod](https://github.com/colinhacks/zod) from 4.0.5 to 4.0.10.
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v4.0.5...v4.0.10)

---
updated-dependencies:
- dependency-name: zod
  dependency-version: 4.0.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 00:27:29 +00:00
dependabot[bot]
843db69df8 chore(deps): bump @radix-ui/react-progress from 1.1.1 to 1.1.7
Bumps [@radix-ui/react-progress](https://github.com/radix-ui/primitives) from 1.1.1 to 1.1.7.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-progress"
  dependency-version: 1.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 00:27:11 +00:00
dependabot[bot]
61a19d7914 chore(deps): bump axios from 1.8.2 to 1.11.0
Bumps [axios](https://github.com/axios/axios) from 1.8.2 to 1.11.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.8.2...v1.11.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 00:27:10 +00:00
dependabot[bot]
4a327b420c chore(deps-dev): bump the dev group with 3 updates
Bumps the dev group with 3 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [eslint](https://github.com/eslint/eslint) and [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next).


Updates `@types/node` from 24.0.15 to 24.1.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `eslint` from 9.31.0 to 9.32.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.31.0...v9.32.0)

Updates `eslint-config-next` from 15.4.2 to 15.4.4
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v15.4.4/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev
- dependency-name: eslint
  dependency-version: 9.32.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev
- dependency-name: eslint-config-next
  dependency-version: 15.4.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 00:26:47 +00:00
23d6f48ee1 Update AudioPlayer and FullScreenPlayer for improved mobile audio handling; refactor WhatsNewPopup for better dialog structure; clean up LoginForm by removing unused settings 2025-07-25 22:49:52 +00:00
1dfb86fa15 Refactor code structure for improved readability and maintainability 2025-07-25 18:45:46 +00:00
745f164482 Update UserProfile avatar size; add development-only debug tools in SettingsPage 2025-07-25 17:58:12 +00:00
25e9bd6912 Refactor UI components for improved spacing; add UserProfile component for user info display 2025-07-25 17:35:07 +00:00
74b9648eef Enhance FullScreenPlayer for improved iOS scrolling; add debug tools in SettingsPage for localStorage management 2025-07-25 15:46:38 +00:00
86e198aa24 Comment out year display in album cards for cleaner UI 2025-07-25 14:22:31 +00:00
284bb4b29f Remove star button from AlbumPage; conditionally render PopularSongs section in ArtistPage based on mobile view 2025-07-25 14:05:10 +00:00
f4c01e2d20 Update FullScreenPlayer for improved iOS scrolling compatibility; adjust LibraryPage layout for better spacing and Card component padding 2025-07-25 14:02:01 +00:00
a957398f63 Increase cover art resolution to 300px for improved image quality in AlbumPage and SongRecommendations components 2025-07-25 04:54:07 +00:00
fe40c0264c Refactor SongRecommendations and LibraryPage components for improved mobile responsiveness and code clarity 2025-07-25 04:52:46 +00:00
940ed94579 Update app version and changelog for July End of Month Update; remove unused code in entrypoint script 2025-07-25 04:39:30 +00:00
57c4070bed Implement code changes to enhance functionality and improve performance 2025-07-24 22:45:10 +00:00
a5e8cbf982 update package manager to pnpm@10.13.1 and add pnpm workspace configuration 2025-07-24 22:40:28 +00:00
a50be24cc2 Merge branch 'dev' into mobile-support 2025-07-24 17:32:24 -05:00
7412af6626 Merge pull request #24 from sillyangel/dependabot/npm_and_yarn/zod-4.0.5
chore(deps): bump zod from 3.25.70 to 4.0.5
2025-07-24 17:32:07 -05:00
7184f0f01d Merge pull request #25 from sillyangel/dependabot/npm_and_yarn/radix-ui/react-context-menu-2.2.15
chore(deps): bump @radix-ui/react-context-menu from 2.2.4 to 2.2.15
2025-07-24 17:32:03 -05:00
9b8b0df551 Merge pull request #26 from sillyangel/dependabot/npm_and_yarn/radix-ui/react-separator-1.1.7
chore(deps): bump @radix-ui/react-separator from 1.1.1 to 1.1.7
2025-07-24 17:31:58 -05:00
033a510b77 Merge pull request #27 from sillyangel/dependabot/npm_and_yarn/react-hook-form-7.60.0
chore(deps): bump react-hook-form from 7.54.2 to 7.60.0
2025-07-24 17:31:39 -05:00
d98486a9a6 Merge pull request #29 from sillyangel/dependabot/npm_and_yarn/dev-0a4cb714a2
chore(deps-dev): bump the dev group across 1 directory with 3 updates
2025-07-24 17:31:13 -05:00
d207da743f Merge pull request #30 from sillyangel/dependabot/npm_and_yarn/npm_and_yarn-e04d5d616f
chore(deps): bump form-data from 4.0.3 to 4.0.4 in the npm_and_yarn group
2025-07-24 17:31:01 -05:00
c8bc5e80d9 feat: enhance SongRecommendations component for mobile and desktop views, add Apple Touch Icons and viewport settings 2025-07-24 22:03:58 +00:00
abf29caacb feat: implement swipe gesture controls for mobile audio player and enhance theme color handling 2025-07-23 16:10:11 +00:00
abfe2bb3ef feat: optimize cover art URLs for songs and playlists with dynamic sizing 2025-07-23 15:56:27 +00:00
8906b2d81e feat: implement responsive album layout for mobile and desktop views, add debugging configurations for Next.js 2025-07-23 15:37:50 +00:00
3a3c065916 feat: add responsive image size hooks and utility functions for optimal image sizing 2025-07-23 15:23:09 +00:00
fccf3c5d13 feat: enhance FullScreenPlayer with improved lyric scrolling and background styling for mobile 2025-07-23 05:50:01 +00:00
31f8f5dbee feat: add iOS togglefavorite action and enhance mobile player layout with tab navigation 2025-07-23 05:13:06 +00:00
bbdee30f92 fix: update cover art URL size for improved image quality in audio player and playlist 2025-07-23 04:47:47 +00:00
463be90779 feat: enhance mobile audio player with initialization and styling improvements 2025-07-23 04:05:55 +00:00
dependabot[bot]
6953add0c2 chore(deps): bump form-data in the npm_and_yarn group
Bumps the npm_and_yarn group with 1 update: [form-data](https://github.com/form-data/form-data).


Updates `form-data` from 4.0.3 to 4.0.4
- [Release notes](https://github.com/form-data/form-data/releases)
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v4.0.3...v4.0.4)

---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-22 11:10:09 +00:00
dependabot[bot]
599ce057c8 chore(deps-dev): bump the dev group across 1 directory with 3 updates
Bumps the dev group with 3 updates in the / directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [eslint](https://github.com/eslint/eslint) and [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next).


Updates `@types/node` from 24.0.10 to 24.0.15
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `eslint` from 9.30.1 to 9.31.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.30.1...v9.31.0)

Updates `eslint-config-next` from 15.3.5 to 15.4.2
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v15.4.2/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.0.15
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev
- dependency-name: eslint
  dependency-version: 9.31.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev
- dependency-name: eslint-config-next
  dependency-version: 15.4.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 00:20:32 +00:00
a6e65888ab fix: correct image qualities configuration in next.config.mjs 2025-07-20 21:51:32 +00:00
f494ff57d4 fix: update NEXT_PUBLIC_COMMIT_SHA in environment file 2025-07-20 21:24:41 +00:00
ea21150112 Revert "feat: add BottomNavigation and page components"
This reverts commit f4ebfcf478.
2025-07-14 23:16:59 +00:00
f4ebfcf478 feat: add BottomNavigation and page components 2025-07-14 23:15:42 +00:00
dependabot[bot]
a9dd78b5a9 chore(deps): bump @radix-ui/react-separator from 1.1.1 to 1.1.7
Bumps [@radix-ui/react-separator](https://github.com/radix-ui/primitives) from 1.1.1 to 1.1.7.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-separator"
  dependency-version: 1.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 00:55:10 +00:00
dependabot[bot]
2c4798f63f chore(deps): bump react-hook-form from 7.54.2 to 7.60.0
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.54.2 to 7.60.0.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.54.2...v7.60.0)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-version: 7.60.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 00:55:10 +00:00
dependabot[bot]
69c271313b chore(deps): bump @radix-ui/react-context-menu from 2.2.4 to 2.2.15
Bumps [@radix-ui/react-context-menu](https://github.com/radix-ui/primitives) from 2.2.4 to 2.2.15.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-context-menu"
  dependency-version: 2.2.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 00:55:07 +00:00
dependabot[bot]
67f8458a99 chore(deps): bump zod from 3.25.70 to 4.0.5
Bumps [zod](https://github.com/colinhacks/zod) from 3.25.70 to 4.0.5.
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v3.25.70...v4.0.5)

---
updated-dependencies:
- dependency-name: zod
  dependency-version: 4.0.5
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 00:54:18 +00:00
6e4a320097 fix: update mice service image version and port configuration in docker-compose 2025-07-12 18:27:00 +00:00
f09ac2a535 fix: update healthcheck URL for mice service in docker-compose 2025-07-12 18:26:53 +00:00
437640c9a9 feat: implement library page with recent albums and navigation links 2025-07-12 18:20:56 +00:00
3eb16a7b7a fix: adjust z-index values for fullscreen player elements to improve layering 2025-07-11 21:42:32 +00:00
c101ac79eb feat: implement bottom navigation for mobile and enhance audio player with media session support 2025-07-11 21:34:57 +00:00
14d5036e8b feat: enhance mobile experience with responsive audio player and navigation improvements 2025-07-11 20:47:56 +00:00
d8a853401f fix: update example environment variables for mice service in docker-compose 2025-07-11 05:11:36 +00:00
00bd099b26 fix: update mice service image version and comment out sensitive environment variables in docker-compose example 2025-07-11 05:08:11 +00:00
732b3a84c0 Merge pull request #23 from sillynano/dev
remove this worthless piece of junk
2025-07-10 23:27:54 -05:00
Sillynano
caac806baf remove this worthless piece of junk
sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma sigma
lebron
2025-07-10 23:26:14 -05:00
9f3fa3fccb Update release.yml 2025-07-10 19:02:53 -05:00
52 changed files with 3913 additions and 1856 deletions

View File

@@ -1 +1 @@
NEXT_PUBLIC_COMMIT_SHA=35febc5
NEXT_PUBLIC_COMMIT_SHA=0c32c05

View File

@@ -81,12 +81,6 @@ jobs:
cache-to: |
type=gha,mode=max,scope=deps-only
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v1
with:
subject-name: ${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
# - name: Docker Hub Description
# uses: peter-evans/dockerhub-description@v4

38
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,38 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug: Next.js Development",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/.bin/next",
"args": ["dev"],
"console": "integratedTerminal",
"env": {
"NODE_ENV": "development"
},
"runtimeExecutable": "pnpm",
"runtimeArgs": ["run", "dev"],
"skipFiles": ["<node_internals>/**"],
"resolveSourceMapLocations": [
"${workspaceFolder}/**",
"!**/node_modules/**"
]
},
{
"name": "Debug: Next.js Production",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/.bin/next",
"args": ["start"],
"console": "integratedTerminal",
"env": {
"NODE_ENV": "production"
},
"preLaunchTask": "Build: Production Build Only",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["run", "start"],
"skipFiles": ["<node_internals>/**"]
}
]
}

114
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,114 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Dev: Start Development Server",
"type": "shell",
"command": "pnpm",
"args": [
"run",
"dev"
],
"group": {
"kind": "build",
"isDefault": true
},
"isBackground": true,
"problemMatcher": [
"$tsc-watch"
],
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "new",
"showReuseMessage": true,
"clear": false
},
"options": {
"env": {
"NODE_ENV": "development"
}
}
},
{
"label": "Prod: Build and Start Production",
"type": "shell",
"command": "bash",
"args": [
"-c",
"pnpm run build && pnpm run start"
],
"group": "build",
"presentation": {
"echo": true,
"reveal": "always",
"focus": true,
"panel": "new",
"showReuseMessage": true,
"clear": true
},
"options": {
"env": {
"NODE_ENV": "production"
}
},
"problemMatcher": ["$tsc"],
"dependsOrder": "sequence"
},
{
"label": "Debug: Development with Debug Info",
"type": "shell",
"command": "pnpm",
"args": [
"run",
"dev"
],
"group": {
"kind": "test",
"isDefault": false
},
"isBackground": true,
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "new",
"showReuseMessage": true,
"clear": false
},
"options": {
"env": {
"NODE_ENV": "development",
"DEBUG": "*",
"NEXT_TELEMETRY_DISABLED": "1"
}
},
"problemMatcher": ["$tsc-watch"]
},
{
"label": "Build: Production Build Only",
"type": "shell",
"command": "pnpm",
"args": [
"run",
"build"
],
"group": "build",
"presentation": {
"echo": true,
"reveal": "always",
"focus": true,
"panel": "new",
"showReuseMessage": true,
"clear": true
},
"options": {
"env": {
"NODE_ENV": "production"
}
},
"problemMatcher": ["$tsc"]
}
]
}

View File

@@ -1,68 +0,0 @@
# GitHub Actions Docker Publishing Setup
This repository includes a GitHub Actions workflow that automatically builds and publishes Docker images to Docker Hub.
## Workflow Overview
The workflow (`/.github/workflows/publish-docker.yml`) automatically:
1. **Builds** the Docker image using multi-platform support (AMD64 and ARM64)
2. **Publishes** to `sillyangel/mice`
3. **Tags** images appropriately based on git refs
4. **Caches** layers for faster subsequent builds
5. **Generates** build provenance attestations for security
## Trigger Conditions
The workflow runs on:
- **Push to main/master branch** → Creates `latest` tag
- **Push tags** (e.g., `2025.07.02`) → Creates date-based version tags
- **Pull requests** → Creates PR-specific tags for testing
- **Manual dispatch** → Can be triggered manually from GitHub UI
## Image Tags Generated
Based on different triggers, the workflow creates these tags:
### Main Branch Push
- `sillyangel/mice:latest`
### Tag Push (e.g., `2025.07.02`)
- `sillyangel/mice:2025.07.02`
- `sillyangel/mice:latest`
### Pull Request
- `sillyangel/mice:pr-123`
## Multi-Platform Support
The workflow builds for multiple architectures:
- `linux/amd64` (Intel/AMD 64-bit)
- `linux/arm64` (ARM 64-bit, Apple Silicon, etc.)
## Usage After Setup
Once the workflow is set up:
1. **Push to main** → New `latest` image published
2. **Create a release** → Versioned images published
3. **Users can pull**: `docker pull sillyangel/mice:latest`
## Manual Image Building
You can also build and push manually:
```bash
# Build for multiple platforms
docker buildx build --platform linux/amd64,linux/arm64 \
-t sillyangel/mice:latest \
--push .
# Login first (if needed)
echo $DOCKERHUB_TOKEN | docker login -u USERNAME --password-stdin
```

View File

@@ -10,9 +10,9 @@ import Link from 'next/link';
import { useAudioPlayer } from '@/app/components/AudioPlayerContext'
import Loading from "@/app/components/loading";
import { Separator } from '@/components/ui/separator';
import { ScrollArea } from '@/components/ui/scroll-area';
import { getNavidromeAPI } from '@/lib/navidrome';
import { useFavoriteAlbums } from '@/hooks/use-favorite-albums';
import { useIsMobile } from '@/hooks/use-mobile';
export default function AlbumPage() {
const { id } = useParams();
@@ -24,6 +24,7 @@ export default function AlbumPage() {
const { getAlbum, starItem, unstarItem } = useNavidrome();
const { playTrack, addAlbumToQueue, playAlbum, playAlbumFromTrack, currentTrack } = useAudioPlayer();
const { isFavoriteAlbum, toggleFavoriteAlbum } = useFavoriteAlbums();
const isMobile = useIsMobile();
const api = getNavidromeAPI();
useEffect(() => {
@@ -119,110 +120,157 @@ export default function AlbumPage() {
const seconds = duration % 60;
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
};
// Get cover art URL with proper fallback
const coverArtUrl = album.coverArt && api
? api.getCoverArtUrl(album.coverArt, 300)
: '/default-user.jpg';
// Dynamic cover art URLs based on image size
const getMobileCoverArtUrl = () => {
return album.coverArt && api
? api.getCoverArtUrl(album.coverArt, 280)
: '/default-user.jpg';
};
const getDesktopCoverArtUrl = () => {
return album.coverArt && api
? api.getCoverArtUrl(album.coverArt, 300)
: '/default-user.jpg';
};
return (
<>
<div className="h-full px-4 py-6 lg:px-8">
<div className="space-y-4">
<div className="flex items-start gap-6">
<Image
src={coverArtUrl}
alt={album.name}
width={300}
height={300}
className="rounded-md"
/>
<div className="space-y-2">
<div className="flex items-center space-x-4">
<p className="text-3xl font-semibold tracking-tight">{album.name}</p>
<Button onClick={handleStar} variant="ghost" title={isStarred ? "Unstar album" : "Star album"}>
<Heart className={isStarred ? 'text-primary' : 'text-gray-500'} fill={isStarred ? 'var(--primary)' : ""}/>
</Button>
{isMobile ? (
/* Mobile Layout */
<div className="space-y-6">
{/* Album Cover - Centered */}
<div className="flex justify-center">
<Image
src={getMobileCoverArtUrl()}
alt={album.name}
width={280}
height={280}
className="rounded-md shadow-lg"
/>
</div>
<Link href={`/artist/${album.artistId}`}>
<p className="text-xl text-primary mt-0 mb-4 underline">{album.artist}</p>
</Link>
<Button className="px-5" onClick={() => playAlbum(album.id)}>
Play
</Button>
<div className="text-sm text-muted-foreground">
<p>{album.genre} {album.year}</p>
<p>{album.songCount} songs, {formatDuration(album.duration)}</p>
{/* Album Info and Controls */}
<div className="flex justify-between items-start gap-4">
{/* Left side - Album Info */}
<div className="flex-1 space-y-1">
<h1 className="text-2xl font-bold text-left">{album.name}</h1>
<Link href={`/artist/${album.artistId}`}>
<p className="text-lg text-primary underline text-left">{album.artist}</p>
</Link>
<p className="text-sm text-muted-foreground text-left">{album.genre} {album.year}</p>
<p className="text-sm text-muted-foreground text-left">{album.songCount} songs, {formatDuration(album.duration)}</p>
</div>
{/* Right side - Controls */}
<div className="flex flex-col items-center gap-3">
<Button
className="w-12 h-12 rounded-full p-0"
onClick={() => playAlbum(album.id)}
title="Play Album"
>
<Play className="w-6 h-6" />
</Button>
</div>
</div>
</div>
</div>
) : (
/* Desktop Layout */
<div className="flex items-start gap-6">
<Image
src={getDesktopCoverArtUrl()}
alt={album.name}
width={300}
height={300}
className="rounded-md"
/>
<div className="space-y-2">
<div className="flex items-center space-x-4">
<p className="text-3xl font-semibold tracking-tight">{album.name}</p>
<Button onClick={handleStar} variant="ghost" title={isStarred ? "Unstar album" : "Star album"}>
<Heart className={isStarred ? 'text-primary' : 'text-gray-500'} fill={isStarred ? 'var(--primary)' : ""}/>
</Button>
</div>
<Link href={`/artist/${album.artistId}`}>
<p className="text-xl text-primary mt-0 mb-4 underline">{album.artist}</p>
</Link>
<Button className="px-5" onClick={() => playAlbum(album.id)}>
Play
</Button>
<div className="text-sm text-muted-foreground">
<p>{album.genre} {album.year}</p>
<p>{album.songCount} songs, {formatDuration(album.duration)}</p>
</div>
</div>
</div>
)}
<div className="space-y-4">
<Separator />
<ScrollArea className="h-[calc(100vh-500px)]">
{tracklist.length === 0 ? (
<div className="text-center py-12">
<p className="text-muted-foreground">No tracks available.</p>
</div>
) : (
<div className="space-y-1">
{tracklist.map((song, index) => (
<div
key={song.id}
className={`group flex items-center p-3 rounded-lg hover:bg-accent/50 cursor-pointer transition-colors`}
onClick={() => handlePlayClick(song)}
>
{/* Track Number / Play Indicator */}
<div className="w-8 text-center text-sm text-muted-foreground mr-3">
<>
<span className="group-hover:hidden">{song.track || index + 1}</span>
<Play className="w-4 h-4 mx-auto hidden group-hover:block" />
</>
</div>
{tracklist.length === 0 ? (
<div className="text-center py-12">
<p className="text-muted-foreground">No tracks available.</p>
</div>
) : (
<div className="space-y-1 pb-32">
{tracklist.map((song, index) => (
<div
key={song.id}
className={`group flex items-center p-3 rounded-lg hover:bg-accent/50 cursor-pointer transition-colors`}
onClick={() => handlePlayClick(song)}
>
{/* Track Number / Play Indicator */}
<div className="w-8 text-center text-sm text-muted-foreground mr-3">
<>
<span className="group-hover:hidden">{song.track || index + 1}</span>
<Play className="w-4 h-4 mx-auto hidden group-hover:block" />
</>
</div>
{/* Song Info */}
<div className="flex-1 min-w-0 mr-4">
<div className="flex items-center gap-2 mb-1">
<p className={`font-semibold truncate ${
isCurrentlyPlaying(song) ? 'text-primary' : ''
}`}>
{song.title}
</p>
{/* Song Info */}
<div className="flex-1 min-w-0 mr-4">
<div className="flex items-center gap-2 mb-1">
<p className={`font-semibold truncate ${
isCurrentlyPlaying(song) ? 'text-primary' : ''
}`}>
{song.title}
</p>
</div>
<div className="flex items-center text-sm text-muted-foreground">
<div className="flex items-center gap-1">
<span className="truncate">{song.artist}</span>
</div>
<div className="flex items-center text-sm text-muted-foreground">
<div className="flex items-center gap-1">
<span className="truncate">{song.artist}</span>
</div>
</div>
</div>
{/* Duration */}
<div className="flex items-center text-sm text-muted-foreground mr-4">
{formatDuration(song.duration)}
</div>
{/* Actions */}
<div className="flex items-center space-x-2 group-hover:opacity-100 transition-opacity">
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
handleSongStar(song);
}}
className="h-8 w-8 p-0"
>
<Heart
className={`w-4 h-4 ${starredSongs.has(song.id) ? 'text-primary' : 'text-gray-500'}`}
fill={starredSongs.has(song.id) ? 'var(--primary)' : 'none'}
/>
</Button>
</div>
</div>
))}
</div>
)}
</ScrollArea>
{/* Duration */}
<div className="flex items-center text-sm text-muted-foreground mr-4">
{formatDuration(song.duration)}
</div>
{/* Actions */}
<div className="flex items-center space-x-2 group-hover:opacity-100 transition-opacity">
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
handleSongStar(song);
}}
className="h-8 w-8 p-0"
>
<Heart
className={`w-4 h-4 ${starredSongs.has(song.id) ? 'text-primary' : 'text-gray-500'}`}
fill={starredSongs.has(song.id) ? 'var(--primary)' : 'none'}
/>
</Button>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>

View File

@@ -15,6 +15,7 @@ import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';
import Loading from '@/app/components/loading';
import { getNavidromeAPI } from '@/lib/navidrome';
import { useToast } from '@/hooks/use-toast';
import { useIsMobile } from '@/hooks/use-mobile';
export default function ArtistPage() {
const { artist: artistId } = useParams();
@@ -27,6 +28,7 @@ export default function ArtistPage() {
const { getArtist, starItem, unstarItem } = useNavidrome();
const { playArtist } = useAudioPlayer();
const { toast } = useToast();
const isMobile = useIsMobile();
const api = getNavidromeAPI();
useEffect(() => {
@@ -103,7 +105,7 @@ export default function ArtistPage() {
}
// Get artist image URL with proper fallback
const artistImageUrl = artist.coverArt && api
? api.getCoverArtUrl(artist.coverArt, 300)
? api.getCoverArtUrl(artist.coverArt, 1200)
: '/default-user.jpg';
return (
@@ -152,7 +154,7 @@ export default function ArtistPage() {
<ArtistBio artistName={artist.name} />
{/* Popular Songs Section */}
{popularSongs.length > 0 && (
{!isMobile && popularSongs.length > 0 && (
<PopularSongs songs={popularSongs} artistName={artist.name} />
)}

View File

@@ -10,10 +10,19 @@ import { Progress } from '@/components/ui/progress';
import { useToast } from '@/hooks/use-toast';
import { useLastFmScrobbler } from '@/hooks/use-lastfm-scrobbler';
import { useStandaloneLastFm } from '@/hooks/use-standalone-lastfm';
import { useIsMobile } from '@/hooks/use-mobile';
export const AudioPlayer: React.FC = () => {
const { currentTrack, playPreviousTrack, addToQueue, playNextTrack, clearQueue, queue, toggleShuffle, shuffle, toggleCurrentTrackStar } = useAudioPlayer();
const router = useRouter();
const isMobile = useIsMobile();
// Swipe gesture state for mobile
const [touchStart, setTouchStart] = useState<number | null>(null);
const [touchEnd, setTouchEnd] = useState<number | null>(null);
// Minimum swipe distance (in px)
const minSwipeDistance = 50;
const audioRef = useRef<HTMLAudioElement>(null);
const preloadAudioRef = useRef<HTMLAudioElement>(null);
const [progress, setProgress] = useState(0);
@@ -23,9 +32,36 @@ export const AudioPlayer: React.FC = () => {
const [isClient, setIsClient] = useState(false);
const [isMinimized, setIsMinimized] = useState(false);
const [isFullScreen, setIsFullScreen] = useState(false);
const [audioInitialized, setAudioInitialized] = useState(false);
const audioCurrent = audioRef.current;
const { toast } = useToast();
// Swipe gesture handlers for mobile
const handleTouchStart = (e: React.TouchEvent) => {
setTouchEnd(null);
setTouchStart(e.targetTouches[0].clientX);
};
const handleTouchMove = (e: React.TouchEvent) => {
setTouchEnd(e.targetTouches[0].clientX);
};
const handleTouchEnd = () => {
if (!touchStart || !touchEnd) return;
const distance = touchStart - touchEnd;
const isLeftSwipe = distance > minSwipeDistance;
const isRightSwipe = distance < -minSwipeDistance;
if (isLeftSwipe) {
// Swipe left -> next track
playNextTrack();
} else if (isRightSwipe) {
// Swipe right -> previous track
playPreviousTrack();
}
};
// Last.fm scrobbler integration (Navidrome)
const {
onTrackStart: navidromeOnTrackStart,
@@ -91,6 +127,89 @@ export const AudioPlayer: React.FC = () => {
}
}
// Mobile-specific audio initialization
if (isMobile) {
// Detect if running as PWA
const isPWA = window.matchMedia('(display-mode: standalone)').matches ||
(window.navigator as Navigator & { standalone?: boolean }).standalone === true;
console.log('🔍 Audio initialization debug:', {
isMobile,
isPWA,
audioInitialized,
userAgent: navigator.userAgent
});
// Add a document click listener to initialize audio context on first user interaction
const initializeAudioOnMobile = async () => {
if (!audioInitialized) {
try {
console.log('🎵 Initializing mobile audio context...', { isPWA });
const AudioContextClass = window.AudioContext || (window as typeof window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
if (AudioContextClass) {
const audioContext = new AudioContextClass();
console.log('Audio context state:', audioContext.state);
if (audioContext.state === 'suspended') {
console.log('Resuming suspended audio context...');
await audioContext.resume();
console.log('Audio context resumed, new state:', audioContext.state);
}
// For PWA, we need to explicitly unlock audio
if (isPWA && audioRef.current) {
console.log('PWA detected, performing audio unlock...');
// Create a silent audio buffer to unlock audio
const buffer = audioContext.createBuffer(1, 1, 22050);
const source = audioContext.createBufferSource();
source.buffer = buffer;
source.connect(audioContext.destination);
source.start(0);
// Also try to load the audio element
try {
audioRef.current.volume = 0;
const playPromise = audioRef.current.play();
if (playPromise) {
await playPromise;
audioRef.current.pause();
audioRef.current.currentTime = 0;
}
audioRef.current.volume = volume;
console.log('✅ PWA audio unlock successful');
} catch (unlockError) {
console.log('⚠️ PWA audio unlock failed:', unlockError);
}
}
setAudioInitialized(true);
console.log('✅ Mobile audio context initialized successfully');
}
} catch (error) {
console.log('❌ Mobile audio context initialization failed:', error);
}
}
};
// Listen for any user interaction to initialize audio
const handleFirstUserInteraction = () => {
console.log('🎯 First user interaction detected, initializing audio...');
initializeAudioOnMobile();
document.removeEventListener('touchstart', handleFirstUserInteraction);
document.removeEventListener('click', handleFirstUserInteraction);
};
document.addEventListener('touchstart', handleFirstUserInteraction, { passive: true });
document.addEventListener('click', handleFirstUserInteraction);
return () => {
document.removeEventListener('touchstart', handleFirstUserInteraction);
document.removeEventListener('click', handleFirstUserInteraction);
};
}
// Clean up old localStorage entries with track IDs
const keysToRemove: string[] = [];
for (let i = 0; i < localStorage.length; i++) {
@@ -100,7 +219,7 @@ export const AudioPlayer: React.FC = () => {
}
}
keysToRemove.forEach(key => localStorage.removeItem(key));
}, []);
}, [isMobile, audioInitialized, volume]);
// Apply volume to audio element when volume changes
useEffect(() => {
@@ -129,8 +248,76 @@ export const AudioPlayer: React.FC = () => {
// Always clear current track time when changing tracks
localStorage.removeItem('navidrome-current-track-time');
console.log('🔄 Setting audio source:', currentTrack.url);
// Debug: Check if URL is valid
if (!currentTrack.url || currentTrack.url === 'undefined' || currentTrack.url === '') {
console.error('❌ Invalid audio URL:', currentTrack.url);
return;
}
// Debug: Log current audio element state
console.log('🔍 Audio element state before loading:', {
src: audioCurrent.src,
readyState: audioCurrent.readyState,
networkState: audioCurrent.networkState,
crossOrigin: audioCurrent.crossOrigin,
canPlayType_mp3: audioCurrent.canPlayType('audio/mpeg'),
canPlayType_mp4: audioCurrent.canPlayType('audio/mp4'),
canPlayType_webm: audioCurrent.canPlayType('audio/webm'),
canPlayType_ogg: audioCurrent.canPlayType('audio/ogg'),
canPlayType_flac: audioCurrent.canPlayType('audio/flac'),
canPlayType_wav: audioCurrent.canPlayType('audio/wav')
});
// Clear any previous error handlers
audioCurrent.onerror = null;
audioCurrent.onloadstart = null;
audioCurrent.oncanplay = null;
// Simple error handling
audioCurrent.onerror = (e) => {
const event = e as Event;
const error = event.target as HTMLAudioElement;
console.error('❌ Audio element error:', {
error: error.error,
networkState: error.networkState,
readyState: error.readyState,
src: error.src
});
};
audioCurrent.onloadstart = () => {
console.log('📥 Audio load started');
};
audioCurrent.oncanplay = () => {
console.log('✅ Audio can play');
};
// Set source without any CORS configuration
audioCurrent.removeAttribute('crossorigin');
audioCurrent.src = currentTrack.url;
// Force load and log state after setting source
audioCurrent.load();
// Log state after load
setTimeout(() => {
console.log('🔍 Audio element state after load:', {
src: audioCurrent.src,
readyState: audioCurrent.readyState,
networkState: audioCurrent.networkState,
error: audioCurrent.error,
duration: audioCurrent.duration
});
}, 100);
// For iOS, ensure audio element is properly loaded
if (isMobile) {
audioCurrent.load();
}
// Notify scrobbler about new track
onTrackStart(currentTrack);
@@ -157,21 +344,31 @@ export const AudioPlayer: React.FC = () => {
localStorage.removeItem('navidrome-current-track-time');
}
// Auto-play only if the track has the autoPlay flag
if (currentTrack.autoPlay) {
audioCurrent.play().then(() => {
// Auto-play only if the track has the autoPlay flag and audio is initialized
if (currentTrack.autoPlay && (!isMobile || audioInitialized)) {
// Add a small delay for iOS compatibility
const playPromise = isMobile ?
new Promise(resolve => setTimeout(resolve, 100)).then(() => audioCurrent.play()) :
audioCurrent.play();
playPromise.then(() => {
setIsPlaying(true);
// Notify scrobbler about play
onTrackPlay(currentTrack);
}).catch((error) => {
console.error('Failed to auto-play:', error);
setIsPlaying(false);
// On iOS, auto-play might fail - that's normal
if (isMobile) {
console.log('Auto-play failed on mobile - user interaction required');
}
});
} else {
setIsPlaying(false);
}
}
}, [currentTrack, onTrackStart, onTrackPlay]);
}, [currentTrack, onTrackStart, onTrackPlay, isMobile, audioInitialized]);
useEffect(() => {
const audioCurrent = audioRef.current;
@@ -245,69 +442,131 @@ export const AudioPlayer: React.FC = () => {
};
}, [playNextTrack, currentTrack, onTrackProgress, onTrackEnd, onTrackPlay, onTrackPause]);
// Media Session API integration
// Media Session API integration - Enhanced for mobile
useEffect(() => {
if (!isClient || !currentTrack || !('mediaSession' in navigator)) return;
if (!isClient || !currentTrack) return;
// Check if MediaSession is supported
if (!('mediaSession' in navigator)) {
console.log('MediaSession API not supported');
return;
}
// Set metadata
navigator.mediaSession.metadata = new MediaMetadata({
title: currentTrack.name,
artist: currentTrack.artist,
album: currentTrack.album,
artwork: currentTrack.coverArt ? [
{ src: currentTrack.coverArt, sizes: '512x512', type: 'image/jpeg' }
] : undefined,
});
try {
// Set metadata
navigator.mediaSession.metadata = new MediaMetadata({
title: currentTrack.name,
artist: currentTrack.artist,
album: currentTrack.album,
artwork: currentTrack.coverArt ? [
{ src: currentTrack.coverArt, sizes: '96x96', type: 'image/jpeg' },
{ src: currentTrack.coverArt, sizes: '128x128', type: 'image/jpeg' },
{ src: currentTrack.coverArt, sizes: '192x192', type: 'image/jpeg' },
{ src: currentTrack.coverArt, sizes: '256x256', type: 'image/jpeg' },
{ src: currentTrack.coverArt, sizes: '384x384', type: 'image/jpeg' },
{ src: currentTrack.coverArt, sizes: '512x512', type: 'image/jpeg' }
] : [
{ src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
{ src: '/icon-512.png', sizes: '512x512', type: 'image/png' }
],
});
// Set playback state
navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused';
// Set playback state
navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused';
// Set action handlers
navigator.mediaSession.setActionHandler('play', () => {
const audioCurrent = audioRef.current;
if (audioCurrent && currentTrack) {
audioCurrent.play();
setIsPlaying(true);
onTrackPlay(currentTrack);
// Set action handlers with error handling
navigator.mediaSession.setActionHandler('play', () => {
const audioCurrent = audioRef.current;
if (audioCurrent && currentTrack) {
audioCurrent.play().then(() => {
setIsPlaying(true);
onTrackPlay(currentTrack);
}).catch(console.error);
}
});
navigator.mediaSession.setActionHandler('pause', () => {
const audioCurrent = audioRef.current;
if (audioCurrent && currentTrack) {
audioCurrent.pause();
setIsPlaying(false);
onTrackPause(audioCurrent.currentTime);
}
});
navigator.mediaSession.setActionHandler('previoustrack', () => {
playPreviousTrack();
});
navigator.mediaSession.setActionHandler('nexttrack', () => {
playNextTrack();
});
navigator.mediaSession.setActionHandler('seekto', (details) => {
const audioCurrent = audioRef.current;
if (audioCurrent && details.seekTime !== undefined) {
audioCurrent.currentTime = details.seekTime;
}
});
// Add togglefavorite action for iOS
try {
// togglefavorite is an iOS-specific action that may not be in TypeScript definitions
const mediaSession = navigator.mediaSession as MediaSession & {
setActionHandler(action: 'togglefavorite', handler: MediaSessionActionHandler | null): void;
};
mediaSession.setActionHandler('togglefavorite', () => {
toggleCurrentTrackStar();
});
} catch (error) {
// togglefavorite might not be supported on all platforms
console.log('togglefavorite action not supported:', error);
}
});
navigator.mediaSession.setActionHandler('pause', () => {
const audioCurrent = audioRef.current;
if (audioCurrent && currentTrack) {
audioCurrent.pause();
setIsPlaying(false);
onTrackPause(audioCurrent.currentTime);
}
});
// Update position state for better scrubbing support
const updatePositionState = () => {
const audioCurrent = audioRef.current;
if (audioCurrent && currentTrack && 'setPositionState' in navigator.mediaSession) {
try {
navigator.mediaSession.setPositionState({
duration: audioCurrent.duration || 0,
playbackRate: audioCurrent.playbackRate || 1.0,
position: audioCurrent.currentTime || 0,
});
} catch (error) {
console.log('Position state update failed:', error);
}
}
};
navigator.mediaSession.setActionHandler('previoustrack', () => {
playPreviousTrack();
});
// Update position state periodically
const positionInterval = setInterval(updatePositionState, 1000);
navigator.mediaSession.setActionHandler('nexttrack', () => {
playNextTrack();
});
navigator.mediaSession.setActionHandler('seekto', (details) => {
const audioCurrent = audioRef.current;
if (audioCurrent && details.seekTime !== undefined) {
audioCurrent.currentTime = details.seekTime;
}
});
return () => {
if ('mediaSession' in navigator) {
navigator.mediaSession.setActionHandler('play', null);
navigator.mediaSession.setActionHandler('pause', null);
navigator.mediaSession.setActionHandler('previoustrack', null);
navigator.mediaSession.setActionHandler('nexttrack', null);
navigator.mediaSession.setActionHandler('seekto', null);
}
};
}, [currentTrack, isPlaying, isClient, playPreviousTrack, playNextTrack, onTrackPlay, onTrackPause]);
return () => {
clearInterval(positionInterval);
if ('mediaSession' in navigator) {
navigator.mediaSession.setActionHandler('play', null);
navigator.mediaSession.setActionHandler('pause', null);
navigator.mediaSession.setActionHandler('previoustrack', null);
navigator.mediaSession.setActionHandler('nexttrack', null);
navigator.mediaSession.setActionHandler('seekto', null);
try {
const mediaSession = navigator.mediaSession as MediaSession & {
setActionHandler(action: 'togglefavorite', handler: MediaSessionActionHandler | null): void;
};
mediaSession.setActionHandler('togglefavorite', null);
} catch (error) {
// togglefavorite might not be supported
}
}
};
} catch (error) {
console.error('MediaSession setup failed:', error);
}
}, [currentTrack, isPlaying, isClient, playPreviousTrack, playNextTrack, onTrackPlay, onTrackPause, toggleCurrentTrackStar]);
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
e.stopPropagation(); // Prevent triggering fullscreen
if (audioCurrent && currentTrack) {
const rect = e.currentTarget.getBoundingClientRect();
const clickX = e.clientX - rect.left;
@@ -319,20 +578,135 @@ export const AudioPlayer: React.FC = () => {
}
};
const togglePlayPause = () => {
const togglePlayPause = async () => {
if (audioCurrent && currentTrack) {
// Detect if running as PWA
const isPWA = window.matchMedia('(display-mode: standalone)').matches ||
(window.navigator as Navigator & { standalone?: boolean }).standalone === true;
console.log('🎵 togglePlayPause called:', {
isPlaying,
isMobile,
isPWA,
audioInitialized,
currentTrackUrl: currentTrack.url,
audioSrc: audioCurrent.src,
audioReadyState: audioCurrent.readyState
});
if (isPlaying) {
console.log('⏸️ Pausing audio');
audioCurrent.pause();
setIsPlaying(false);
onTrackPause(audioCurrent.currentTime);
} else {
audioCurrent.play().then(() => {
try {
// PWA-specific initialization if needed
if (isPWA && !audioInitialized) {
console.log('🔧 PWA detected - initializing audio context...');
try {
const AudioContextClass = window.AudioContext || (window as typeof window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
if (AudioContextClass) {
const audioContext = new AudioContextClass();
if (audioContext.state === 'suspended') {
await audioContext.resume();
}
setAudioInitialized(true);
console.log('✅ PWA audio context initialized');
}
} catch (contextError) {
console.log('⚠️ PWA audio context initialization failed:', contextError);
}
}
// On mobile, ensure audio element is properly loaded before playing
if (isMobile) {
// Ensure the audio element has the correct source
if (audioCurrent.src !== currentTrack.url) {
console.log('🔄 Setting audio source:', currentTrack.url);
audioCurrent.src = currentTrack.url;
audioCurrent.load(); // Force reload the audio element
}
// Wait for the audio to be ready to play
if (audioCurrent.readyState < 3) { // HAVE_FUTURE_DATA
console.log('⏳ Waiting for audio to be ready...');
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
audioCurrent.removeEventListener('canplay', handleCanPlay);
audioCurrent.removeEventListener('error', handleError);
reject(new Error('Audio load timeout'));
}, 10000); // 10 second timeout
const handleCanPlay = () => {
console.log('✅ Audio ready to play');
clearTimeout(timeout);
audioCurrent.removeEventListener('canplay', handleCanPlay);
audioCurrent.removeEventListener('error', handleError);
resolve(void 0);
};
const handleError = () => {
console.log('❌ Audio load error');
clearTimeout(timeout);
audioCurrent.removeEventListener('canplay', handleCanPlay);
audioCurrent.removeEventListener('error', handleError);
reject(new Error('Audio failed to load'));
};
audioCurrent.addEventListener('canplay', handleCanPlay);
audioCurrent.addEventListener('error', handleError);
});
}
}
console.log('▶️ Attempting to play audio...');
await audioCurrent.play();
setIsPlaying(true);
setAudioInitialized(true);
onTrackPlay(currentTrack);
}).catch((error) => {
console.error('Failed to play audio:', error);
setIsPlaying(false);
});
console.log('✅ Audio play successful');
} catch (error) {
console.error('❌ Failed to play audio:', error);
// Additional mobile-specific handling
if (isMobile) {
try {
console.log('🔄 Attempting mobile audio recovery...');
// Try creating and resuming audio context
const AudioContextClass = window.AudioContext || (window as typeof window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
if (AudioContextClass) {
const audioContext = new AudioContextClass();
if (audioContext.state === 'suspended') {
await audioContext.resume();
}
setAudioInitialized(true);
}
// Force load and retry
audioCurrent.load();
await new Promise(resolve => setTimeout(resolve, 200)); // Small delay for iOS
console.log('🔄 Retrying audio play...');
await audioCurrent.play();
setIsPlaying(true);
onTrackPlay(currentTrack);
console.log('✅ Audio play retry successful');
} catch (retryError) {
console.error('❌ Audio play retry failed:', retryError);
setIsPlaying(false);
// Show user-friendly error on mobile
toast({
variant: "destructive",
title: "Playback Error",
description: isPWA
? "Unable to play audio in PWA mode. Try refreshing the app or playing in Safari browser."
: "Unable to play audio. Please try again or check your connection.",
});
}
} else {
setIsPlaying(false);
}
}
}
}
};
@@ -354,109 +728,121 @@ export const AudioPlayer: React.FC = () => {
return null;
}
// Mini player (collapsed state)
if (isMinimized) {
// Mobile compact mini player :3
if (isMobile) {
return (
<div className="fixed bottom-4 left-4 z-50">
<div
className="bg-background/95 backdrop-blur-xs border rounded-lg shadow-lg cursor-pointer hover:scale-[1.02] transition-transform w-80"
onClick={() => setIsMinimized(false)}
>
<div className="flex items-center p-3">
<Image
src={currentTrack.coverArt || '/default-user.jpg'}
alt={currentTrack.name}
width={40}
height={40}
className="w-10 h-10 rounded-md shrink-0"
/>
<div className="flex-1 min-w-0 mx-3">
<div className="overflow-hidden">
<p className="font-semibold text-sm whitespace-nowrap animate-infinite-scroll">
{currentTrack.name}
</p>
</div>
<p className="text-xs text-muted-foreground truncate">{currentTrack.artist}</p>
</div>
{/* Heart icon for favoriting */}
<button
className="p-1.5 hover:bg-gray-700/50 rounded-full transition-colors mr-2"
onClick={(e) => {
e.stopPropagation();
toggleCurrentTrackStar();
}}
title={currentTrack.starred ? 'Remove from favorites' : 'Add to favorites'}
>
<Heart
className={`w-4 h-4 ${currentTrack.starred ? 'text-primary fill-primary' : 'text-gray-400'}`}
<>
<div className="fixed bottom-16 left-0 right-0 z-[60] bg-background/95 backdrop-blur-sm border-t shadow-lg mobile-audio-player mobile-safe-bottom">
<div className="px-4 py-3">
{/* Progress bar at top for mobile */}
<div className="mb-3">
<Progress
value={progress}
className="h-1 cursor-pointer progress-mobile"
onClick={handleProgressClick}
/>
</button>
<div className="flex items-center justify-center space-x-2">
<button className="p-1.5 hover:bg-gray-700/50 rounded-full transition-colors" onClick={playPreviousTrack}>
<FaBackward className="w-3 h-3" />
</button>
<button className="p-2 hover:bg-gray-700/50 rounded-full transition-colors" onClick={togglePlayPause}>
{isPlaying ? <FaPause className="w-4 h-4" /> : <FaPlay className="w-4 h-4" />}
</button>
<button className="p-1.5 hover:bg-gray-700/50 rounded-full transition-colors" onClick={playNextTrack}>
<FaForward className="w-3 h-3" />
</button>
</div>
</div>
<div className="flex items-center justify-between">
{/* Track info with swipe gestures */}
<div
className="flex items-center flex-1 min-w-0 cursor-pointer"
onClick={() => setIsFullScreen(true)}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
<Image
src={currentTrack.coverArt || '/default-user.jpg'}
alt={currentTrack.name}
width={48}
height={48}
className="w-12 h-12 rounded-lg mr-3 shrink-0 shadow-sm"
/>
<div className="flex-1 min-w-0">
<p className="font-semibold text-sm truncate">{currentTrack.name}</p>
<p className="text-xs text-muted-foreground truncate">{currentTrack.artist}</p>
</div>
</div>
{/* Mobile controls - Only heart and play/pause */}
<div className="flex items-center space-x-2">
<button
className="p-3 hover:bg-muted/50 rounded-full transition-all duration-200 active:scale-95 touch-manipulation"
onClick={(e) => {
e.stopPropagation();
toggleCurrentTrackStar();
}}
type="button"
aria-label={currentTrack.starred ? 'Remove from favorites' : 'Add to favorites'}
title={currentTrack.starred ? 'Remove from favorites' : 'Add to favorites'}
>
<Heart
className={`w-4 h-4 ${currentTrack.starred ? 'text-primary fill-primary' : ''}`}
/>
</button>
<button
className="p-4 hover:bg-muted/50 rounded-full transition-all duration-200 active:scale-95 bg-primary/10 touch-manipulation"
onClick={togglePlayPause}
style={{ touchAction: 'manipulation' }}
type="button"
data-testid="play-pause-button"
aria-label={isPlaying ? 'Pause' : 'Play'}
>
{isPlaying ? <FaPause className="w-5 h-5" /> : <FaPlay className="w-5 h-5" />}
</button>
</div>
</div>
</div>
</div>
<audio ref={audioRef} hidden />
{/* Full Screen Player for mobile - rendered outside mini player */}
<FullScreenPlayer
isOpen={isFullScreen}
onClose={() => setIsFullScreen(false)}
onOpenQueue={handleOpenQueue}
/>
{/* Single audio element - shared across all UI states */}
<audio
ref={audioRef}
playsInline
preload="metadata"
style={{ display: 'none' }}
/>
<audio ref={preloadAudioRef} hidden preload="metadata" />
</div>
</>
);
}
// Compact floating player (default state)
return (
<div className="fixed bottom-4 left-4 right-4 z-50">
<div className="bg-background/95 backdrop-blur-xs border rounded-lg shadow-lg p-3 cursor-pointer hover:scale-[1.01] transition-transform">
<div className="flex items-center">
{/* Track info */}
<div className="flex items-center flex-1 min-w-0">
<Image
src={
currentTrack.coverArt &&
(currentTrack.coverArt.startsWith('http') || currentTrack.coverArt.startsWith('/'))
? currentTrack.coverArt
: '/default-user.jpg'
}
alt={currentTrack.name}
width={48}
height={48}
className="w-12 h-12 rounded-md mr-4 shrink-0"
/>
<div className="flex-1 min-w-0">
<p className="font-semibold truncate text-base">{currentTrack.name}</p>
<p className="text-sm text-muted-foreground truncate">{currentTrack.artist}</p>
</div>
</div>
{/* Center section with controls and progress */}
<div className="flex flex-col items-center flex-1 justify-center">
{/* Control buttons */}
<div className="flex items-center justify-center space-x-3">
<button
onClick={toggleShuffle}
className={`p-2 hover:bg-gray-700/50 rounded-full transition-colors ${shuffle ? 'text-primary bg-primary/20' : ''}`}
title={shuffle ? 'Shuffle On - Queue is shuffled' : 'Shuffle Off - Click to shuffle queue'}
>
<FaShuffle className="w-4 h-4" />
</button>
<button className="p-2 hover:bg-gray-700/50 rounded-full transition-colors" onClick={playPreviousTrack}>
<FaBackward className="w-4 h-4" />
</button>
<button className="p-3 hover:bg-gray-700/50 rounded-full transition-colors" onClick={togglePlayPause}>
{isPlaying ? <FaPause className="w-5 h-5" /> : <FaPlay className="w-5 h-5" />}
</button>
<button className="p-2 hover:bg-gray-700/50 rounded-full transition-colors" onClick={playNextTrack}>
<FaForward className="w-4 h-4" />
</button>
// Desktop mini player (collapsed state)
if (isMinimized) {
return (
<>
<div className="fixed bottom-4 left-4 z-50">
<div
className="bg-background/95 backdrop-blur-xs border rounded-lg shadow-lg cursor-pointer hover:scale-[1.02] transition-transform w-80"
onClick={() => setIsMinimized(false)}
>
<div className="flex items-center p-3">
<Image
src={currentTrack.coverArt || '/default-user.jpg'}
alt={currentTrack.name}
width={40}
height={40}
className="w-10 h-10 rounded-md shrink-0"
/>
<div className="flex-1 min-w-0 mx-3">
<div className="overflow-hidden">
<p className="font-semibold text-sm whitespace-nowrap animate-infinite-scroll">
{currentTrack.name}
</p>
</div>
<p className="text-xs text-muted-foreground truncate">{currentTrack.artist}</p>
</div>
{/* Heart icon for favoriting */}
<button
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors flex items-center justify-center"
className="p-1.5 hover:bg-gray-700/50 rounded-full transition-colors mr-2"
onClick={(e) => {
e.stopPropagation();
toggleCurrentTrackStar();
@@ -464,51 +850,144 @@ export const AudioPlayer: React.FC = () => {
title={currentTrack.starred ? 'Remove from favorites' : 'Add to favorites'}
>
<Heart
className={`w-5 h-5 ${currentTrack.starred ? 'text-primary fill-primary' : ''}`}
className={`w-4 h-4 ${currentTrack.starred ? 'text-primary fill-primary' : 'text-gray-400'}`}
/>
</button>
</div>
{/* Progress bar */}
{/* <div className="flex items-center space-x-2 w-80">
<span className="text-xs text-muted-foreground w-8 text-right">
{formatTime(audioCurrent?.currentTime ?? 0)}
</span>
<Progress value={progress} className="flex-1 cursor-pointer h-1" onClick={handleProgressClick}/>
<span className="text-xs text-muted-foreground w-8">
{formatTime(audioCurrent?.duration ?? 0)}
</span>
</div> */}
<div className="flex items-center justify-center space-x-2">
<button className="p-1.5 hover:bg-gray-700/50 rounded-full transition-colors" onClick={playPreviousTrack}>
<FaBackward className="w-3 h-3" />
</button>
<button className="p-2 hover:bg-gray-700/50 rounded-full transition-colors" onClick={togglePlayPause}>
{isPlaying ? <FaPause className="w-4 h-4" /> : <FaPlay className="w-4 h-4" />}
</button>
<button className="p-1.5 hover:bg-gray-700/50 rounded-full transition-colors" onClick={playNextTrack}>
<FaForward className="w-3 h-3" />
</button>
</div>
{/* Right side buttons */}
<div className="flex items-center justify-end space-x-2 flex-1">
<button
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors"
onClick={() => setIsFullScreen(true)}
title="Full Screen"
>
<FaExpand className="w-4 h-4" />
</button>
<button
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors"
onClick={() => setIsMinimized(true)}
title="Minimize"
>
<FaCompress className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* Single audio element - shared across all UI states */}
<audio
ref={audioRef}
playsInline
preload="metadata"
style={{ display: 'none' }}
/>
<audio ref={preloadAudioRef} hidden preload="metadata" />
</>
);
}
// Desktop compact floating player (default state)
return (
<>
<div className="fixed bottom-4 left-4 right-4 z-50">
<div className="bg-background/95 backdrop-blur-xs border rounded-lg shadow-lg p-3 cursor-pointer hover:scale-[1.01] transition-transform">
<div className="flex items-center">
{/* Track info */}
<div className="flex items-center flex-1 min-w-0">
<Image
src={
currentTrack.coverArt &&
(currentTrack.coverArt.startsWith('http') || currentTrack.coverArt.startsWith('/'))
? currentTrack.coverArt
: '/default-user.jpg'
}
alt={currentTrack.name}
width={48}
height={48}
className="w-12 h-12 rounded-md mr-4 shrink-0"
/>
<div className="flex-1 min-w-0">
<p className="font-semibold truncate text-base">{currentTrack.name}</p>
<p className="text-sm text-muted-foreground truncate">{currentTrack.artist}</p>
</div>
</div>
{/* Center section with controls and progress */}
<div className="flex flex-col items-center flex-1 justify-center">
{/* Control buttons */}
<div className="flex items-center justify-center space-x-3">
<button
onClick={toggleShuffle}
className={`p-2 hover:bg-gray-700/50 rounded-full transition-colors ${shuffle ? 'text-primary bg-primary/20' : ''}`}
title={shuffle ? 'Shuffle On - Queue is shuffled' : 'Shuffle Off - Click to shuffle queue'}
>
<FaShuffle className="w-4 h-4" />
</button>
<button className="p-2 hover:bg-gray-700/50 rounded-full transition-colors" onClick={playPreviousTrack}>
<FaBackward className="w-4 h-4" />
</button>
<button className="p-3 hover:bg-gray-700/50 rounded-full transition-colors" onClick={togglePlayPause}>
{isPlaying ? <FaPause className="w-5 h-5" /> : <FaPlay className="w-5 h-5" />}
</button>
<button className="p-2 hover:bg-gray-700/50 rounded-full transition-colors" onClick={playNextTrack}>
<FaForward className="w-4 h-4" />
</button>
<button
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors flex items-center justify-center"
onClick={(e) => {
e.stopPropagation();
toggleCurrentTrackStar();
}}
title={currentTrack.starred ? 'Remove from favorites' : 'Add to favorites'}
>
<Heart
className={`w-5 h-5 ${currentTrack.starred ? 'text-primary fill-primary' : ''}`}
/>
</button>
</div>
{/* Progress bar */}
{/* <div className="flex items-center space-x-2 w-80">
<span className="text-xs text-muted-foreground w-8 text-right">
{formatTime(audioCurrent?.currentTime ?? 0)}
</span>
<Progress value={progress} className="flex-1 cursor-pointer h-1" onClick={handleProgressClick}/>
<span className="text-xs text-muted-foreground w-8">
{formatTime(audioCurrent?.duration ?? 0)}
</span>
</div> */}
</div>
{/* Right side buttons */}
<div className="flex items-center justify-end space-x-2 flex-1">
<button
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors"
onClick={() => setIsFullScreen(true)}
title="Full Screen"
>
<FaExpand className="w-4 h-4" />
</button>
<button
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors"
onClick={() => setIsMinimized(true)}
title="Minimize"
>
<FaCompress className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* Full Screen Player */}
<FullScreenPlayer
isOpen={isFullScreen}
onClose={() => setIsFullScreen(false)}
onOpenQueue={handleOpenQueue}
/>
</div>
<audio ref={audioRef} hidden />
<audio ref={preloadAudioRef} hidden preload="metadata" />
{/* Full Screen Player */}
<FullScreenPlayer
isOpen={isFullScreen}
onClose={() => setIsFullScreen(false)}
onOpenQueue={handleOpenQueue}
{/* Single audio element - shared across all UI states with mobile support */}
<audio
ref={audioRef}
playsInline
preload="metadata"
style={{ display: 'none' }}
/>
</div>
<audio ref={preloadAudioRef} hidden preload="metadata" />
</>
);
};

View File

@@ -53,7 +53,15 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c
const [isLoading, setIsLoading] = useState(false);
const [shuffle, setShuffle] = useState(false);
const { toast } = useToast();
const api = useMemo(() => getNavidromeAPI(), []);
const api = useMemo(() => {
const navidromeApi = getNavidromeAPI();
if (!navidromeApi) {
console.warn('⚠️ Navidrome API not configured');
} else {
console.log('✅ Navidrome API initialized');
}
return navidromeApi;
}, []);
useEffect(() => {
const savedQueue = localStorage.getItem('navidrome-audioQueue');
@@ -98,14 +106,18 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c
if (!api) {
throw new Error('Navidrome API not configured');
}
const streamUrl = api.getStreamUrl(song.id);
console.log('🎵 Creating track with stream URL:', streamUrl);
return {
id: song.id,
name: song.title,
url: api.getStreamUrl(song.id),
url: streamUrl,
artist: song.artist,
album: song.album,
duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 512) : undefined,
albumId: song.albumId,
artistId: song.artistId,
starred: !!song.starred

View File

@@ -0,0 +1,67 @@
'use client';
import { useRouter, usePathname } from 'next/navigation';
import { Home, Search, Disc, Users, Music, Heart, List, Settings } from 'lucide-react';
import { cn } from '@/lib/utils';
interface NavItem {
href: string;
label: string;
icon: React.ComponentType<{ className?: string }>;
}
const navigationItems: NavItem[] = [
{ href: '/', label: 'Home', icon: Home },
{ href: '/search', label: 'Search', icon: Search },
{ href: '/library', label: 'Library', icon: Music },
{ href: '/queue', label: 'Queue', icon: List },
];
export function BottomNavigation() {
const router = useRouter();
const pathname = usePathname();
const handleNavigation = (href: string) => {
router.push(href);
};
const isActive = (href: string) => {
if (href === '/') {
return pathname === '/';
}
return pathname.startsWith(href);
};
return (
<div className="fixed bottom-0 left-0 right-0 z-[50] bg-background/95 backdrop-blur-sm border-t border-border">
<div className="flex items-center justify-around px-2 py-2 pb-safe mb-2">
{navigationItems.map((item) => {
const isItemActive = isActive(item.href);
const Icon = item.icon;
return (
<button
key={item.href}
onClick={() => handleNavigation(item.href)}
className={cn(
"flex flex-col items-center justify-center p-2 rounded-lg transition-all duration-200 min-w-[60px] touch-manipulation",
"active:scale-95 active:bg-primary/20",
isItemActive
? "text-primary bg-primary/10"
: "text-muted-foreground hover:text-foreground hover:bg-muted/50"
)}
>
<Icon className={cn("w-5 h-5 mb-1", isItemActive && "text-primary")} />
<span className={cn(
"text-xs font-medium",
isItemActive ? "text-primary" : "text-muted-foreground"
)}>
{item.label}
</span>
</button>
);
})}
</div>
</div>
);
}

View File

@@ -143,7 +143,7 @@ export function CacheManagement() {
};
return (
<Card className="break-inside-avoid">
<Card className="break-inside-avoid py-5">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Database className="h-5 w-5" />

View File

@@ -7,6 +7,7 @@ import { useAudioPlayer } from '@/app/components/AudioPlayerContext';
import { Progress } from '@/components/ui/progress';
import { lrcLibClient } from '@/lib/lrclib';
import Link from 'next/link';
import { useIsMobile } from '@/hooks/use-mobile';
import {
FaPlay,
FaPause,
@@ -34,8 +35,20 @@ interface FullScreenPlayerProps {
onOpenQueue?: () => void;
}
type MobileTab = 'player' | 'lyrics' | 'queue';
export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onClose, onOpenQueue }) => {
const { currentTrack, playPreviousTrack, playNextTrack, shuffle, toggleShuffle, toggleCurrentTrackStar } = useAudioPlayer();
const {
currentTrack,
playPreviousTrack,
playNextTrack,
shuffle,
toggleShuffle,
toggleCurrentTrackStar,
queue
} = useAudioPlayer();
const isMobile = useIsMobile();
const router = useRouter();
const [progress, setProgress] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
@@ -47,8 +60,19 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
const [lyrics, setLyrics] = useState<LyricLine[]>([]);
const [currentLyricIndex, setCurrentLyricIndex] = useState(-1);
const [showLyrics, setShowLyrics] = useState(true);
const [activeTab, setActiveTab] = useState<MobileTab>('player');
const lyricsRef = useRef<HTMLDivElement>(null);
// Debug logging for component changes
useEffect(() => {
console.log('🔍 FullScreenPlayer state changed:', {
isOpen,
currentTrack,
currentTrackKeys: currentTrack ? Object.keys(currentTrack) : 'null',
queueLength: queue?.length || 0
});
}, [isOpen, currentTrack, queue?.length]);
// Load lyrics when track changes
useEffect(() => {
const loadLyrics = async () => {
@@ -72,7 +96,7 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
setLyrics([]);
}
} catch (error) {
console.error('Failed to load lyrics:', error);
console.log('Failed to load lyrics:', error);
setLyrics([]);
}
};
@@ -88,62 +112,106 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
}
}, [lyrics, currentTime, currentLyricIndex]);
// Auto-scroll lyrics using lyricsRef
// Auto-scroll lyrics using lyricsRef - Disabled on mobile to prevent iOS audio issues
useEffect(() => {
if (currentLyricIndex >= 0 && lyrics.length > 0 && showLyrics && lyricsRef.current) {
// Only auto-scroll on desktop to avoid iOS audio interference
const shouldScroll = !isMobile && showLyrics && lyrics.length > 0;
if (currentLyricIndex >= 0 && shouldScroll && lyricsRef.current) {
const scrollTimeout = setTimeout(() => {
// Find the ScrollArea viewport
const scrollViewport = lyricsRef.current?.querySelector('[data-radix-scroll-area-viewport]') as HTMLElement;
const currentLyricElement = lyricsRef.current?.querySelector(`[data-lyric-index="${currentLyricIndex}"]`) as HTMLElement;
if (scrollViewport && currentLyricElement) {
const containerHeight = scrollViewport.clientHeight;
const elementTop = currentLyricElement.offsetTop;
const elementHeight = currentLyricElement.offsetHeight;
try {
const scrollContainer = lyricsRef.current?.querySelector('[data-radix-scroll-area-viewport]') as HTMLElement;
const currentLyricElement = lyricsRef.current?.querySelector(`[data-lyric-index="${currentLyricIndex}"]`) as HTMLElement;
// Calculate scroll position to center the current lyric
const targetScrollTop = elementTop - (containerHeight / 2) + (elementHeight / 2);
scrollViewport.scrollTo({
top: Math.max(0, targetScrollTop),
behavior: 'smooth'
});
if (scrollContainer && currentLyricElement) {
const containerHeight = scrollContainer.clientHeight;
const elementTop = currentLyricElement.offsetTop;
const elementHeight = currentLyricElement.offsetHeight;
const targetScrollTop = elementTop - (containerHeight / 2) + (elementHeight / 2);
scrollContainer.scrollTo({
top: Math.max(0, targetScrollTop),
behavior: 'smooth'
});
}
} catch (error) {
console.warn('Lyrics scroll failed:', error);
}
}, 100);
}, 200);
return () => clearTimeout(scrollTimeout);
}
}, [currentLyricIndex, showLyrics, lyrics.length]);
}, [currentLyricIndex, showLyrics, lyrics.length, isMobile]);
// Reset lyrics to top when song changes
// Reset lyrics to top when song changes - Disabled on mobile to prevent iOS audio issues
useEffect(() => {
if (currentTrack && showLyrics && lyricsRef.current) {
// Reset scroll position using lyricsRef
const resetScroll = () => {
const scrollViewport = lyricsRef.current?.querySelector('[data-radix-scroll-area-viewport]') as HTMLElement;
if (scrollViewport) {
scrollViewport.scrollTo({
top: 0,
behavior: 'instant' // Use instant for track changes
});
}
};
// Small delay to ensure DOM is ready
// Only reset scroll on desktop to avoid iOS audio interference
const shouldReset = !isMobile && showLyrics && lyrics.length > 0;
if (currentTrack?.id && shouldReset && lyricsRef.current) {
const resetTimeout = setTimeout(() => {
resetScroll();
try {
const scrollContainer = lyricsRef.current?.querySelector('[data-radix-scroll-area-viewport]') as HTMLElement;
if (scrollContainer) {
scrollContainer.scrollTo({
top: 0,
behavior: 'instant'
});
}
} catch (error) {
console.warn('Lyrics reset scroll failed:', error);
}
setCurrentLyricIndex(-1);
}, 50);
return () => clearTimeout(resetTimeout);
}
}, [currentTrack?.id, showLyrics, currentTrack]); // Only reset when track ID changes
}, [currentTrack?.id, showLyrics, isMobile, lyrics.length]);
// Sync with main audio player (improved responsiveness)
useEffect(() => {
const syncWithMainPlayer = () => {
const mainAudio = document.querySelector('audio') as HTMLAudioElement;
console.log('=== FULLSCREEN PLAYER AUDIO DEBUG ===');
console.log('currentTrack from context:', currentTrack);
console.log('currentTrack keys:', currentTrack ? Object.keys(currentTrack) : 'null');
if (currentTrack) {
console.log('currentTrack.url:', currentTrack.url);
console.log('currentTrack.id:', currentTrack.id);
console.log('currentTrack.name:', currentTrack.name);
console.log('currentTrack.artist:', currentTrack.artist);
}
console.log('Audio element found:', !!mainAudio);
if (mainAudio) {
console.log('Audio element src:', mainAudio.src);
console.log('Audio element currentSrc:', mainAudio.currentSrc);
console.log('Audio state:', {
currentTime: mainAudio.currentTime,
duration: mainAudio.duration,
paused: mainAudio.paused,
ended: mainAudio.ended,
readyState: mainAudio.readyState,
networkState: mainAudio.networkState,
error: mainAudio.error
});
// Check if audio source matches current track
if (currentTrack) {
const audioSourceMatches = mainAudio.src === currentTrack.url || mainAudio.currentSrc === currentTrack.url;
console.log('Audio source matches current track URL:', audioSourceMatches);
if (!audioSourceMatches) {
console.log('⚠️ Audio source mismatch!');
console.log('Expected:', currentTrack.url);
console.log('Audio src:', mainAudio.src);
console.log('Audio currentSrc:', mainAudio.currentSrc);
}
}
}
console.log('==========================================');
if (mainAudio && currentTrack) {
const newCurrentTime = mainAudio.currentTime;
const newDuration = mainAudio.duration || 0;
@@ -206,20 +274,96 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
setDominantColor(`rgb(${r}, ${g}, ${b})`);
}
} catch (error) {
console.error('Failed to extract color:', error);
console.log('Failed to extract color:', error);
}
};
img.src = currentTrack.coverArt;
}, [currentTrack]);
const togglePlayPause = () => {
const mainAudio = document.querySelector('audio') as HTMLAudioElement;
if (!mainAudio) return;
if (isPlaying) {
mainAudio.pause();
console.log('🎵 FullScreenPlayer Toggle Play/Pause clicked');
// Find the main audio player's play/pause button and click it
// This ensures we use the same logic as the main player
const mainPlayButton = document.querySelector('[data-testid="play-pause-button"]') as HTMLButtonElement;
if (mainPlayButton) {
console.log('✅ Found main play button, triggering click');
mainPlayButton.click();
} else {
mainAudio.play();
console.log('❌ Main play button not found, falling back to direct audio control');
// Fallback to direct audio control if button not found
const mainAudio = document.querySelector('audio') as HTMLAudioElement;
if (!mainAudio) {
console.log('❌ No audio element found');
// Try to find ALL audio elements for debugging
const allAudio = document.querySelectorAll('audio');
console.log('🔍 Found audio elements:', allAudio.length);
allAudio.forEach((audio, index) => {
console.log(`Audio ${index}:`, {
src: audio.src,
currentSrc: audio.currentSrc,
paused: audio.paused,
hidden: audio.hidden,
style: audio.style.display
});
});
return;
}
console.log('🔍 Detailed audio element state:');
console.log('- Audio src:', mainAudio.src);
console.log('- Audio currentSrc:', mainAudio.currentSrc);
console.log('- Audio paused:', mainAudio.paused);
console.log('- Audio currentTime:', mainAudio.currentTime);
console.log('- Audio duration:', mainAudio.duration);
console.log('- Audio readyState:', mainAudio.readyState, '(0=HAVE_NOTHING, 1=HAVE_METADATA, 2=HAVE_CURRENT_DATA, 3=HAVE_FUTURE_DATA, 4=HAVE_ENOUGH_DATA)');
console.log('- Audio networkState:', mainAudio.networkState, '(0=EMPTY, 1=IDLE, 2=LOADING, 3=NO_SOURCE)');
console.log('- Audio error:', mainAudio.error);
console.log('- Audio ended:', mainAudio.ended);
console.log('- Audio seeking:', mainAudio.seeking);
console.log('- Audio volume:', mainAudio.volume);
console.log('- Audio muted:', mainAudio.muted);
console.log('- Audio autoplay:', mainAudio.autoplay);
console.log('- Audio loop:', mainAudio.loop);
console.log('- Audio preload:', mainAudio.preload);
console.log('- Audio crossOrigin:', mainAudio.crossOrigin);
if (isPlaying) {
console.log('⏸️ Attempting to pause audio');
try {
mainAudio.pause();
console.log('✅ Audio pause() succeeded');
} catch (error) {
console.log('❌ Audio pause() failed:', error);
}
} else {
console.log('▶️ Attempting to play audio');
// Check if audio has a valid source
if (!mainAudio.src && !mainAudio.currentSrc) {
console.log('❌ Audio has no source set!');
console.log('currentTrack:', currentTrack);
if (currentTrack) {
console.log('Setting audio source to:', currentTrack.url);
mainAudio.src = currentTrack.url;
mainAudio.load();
}
}
mainAudio.play().then(() => {
console.log('✅ Audio play() succeeded');
}).catch((error) => {
console.log('❌ Audio play() failed:', error);
console.log('Error details:', {
name: error.name,
message: error.message,
code: error.code
});
});
}
}
};
@@ -269,212 +413,485 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
if (!isOpen || !currentTrack) return null;
return (
<div className="fixed inset-0 z-50 bg-black overflow-hidden">
{/* Blurred background image */}
<div className="fixed inset-0 z-[70] bg-black overflow-hidden">
{/* Enhanced Blurred background image */}
{currentTrack.coverArt && (
<div
className="absolute inset-0 w-full h-full"
style={{
backgroundImage: `url(${currentTrack.coverArt})`,
backgroundSize: '120%',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
filter: 'blur(20px) brightness(0.3)',
transform: 'scale(1.1)',
}}
/>
<div className="absolute inset-0 w-full h-full">
{/* Main background */}
<div
className="absolute inset-0 w-full h-full"
style={{
backgroundImage: `url(${currentTrack.coverArt})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
filter: 'blur(20px) brightness(0.3)',
transform: 'scale(1.1)',
}}
/>
{/* Top gradient blur for mobile */}
<div
className="absolute top-0 left-0 right-0 h-32"
style={{
background: `linear-gradient(to bottom,
rgba(0,0,0,0.8) 0%,
rgba(0,0,0,0.4) 50%,
transparent 100%)`,
backdropFilter: 'blur(10px)',
}}
/>
{/* Bottom gradient blur for mobile */}
<div
className="absolute bottom-0 left-0 right-0 h-32"
style={{
background: `linear-gradient(to top,
rgba(0,0,0,0.8) 0%,
rgba(0,0,0,0.4) 50%,
transparent 100%)`,
backdropFilter: 'blur(10px)',
}}
/>
</div>
)}
{/* Overlay for better contrast */}
<div className="absolute inset-0 bg-black/50" />
<div className="relative h-full w-full">
{/* Floating Header */}
<div className="absolute top-0 right-0 z-50 p-4 lg:p-6">
<div className="flex items-center gap-2">
{onOpenQueue && (
<button
onClick={onOpenQueue}
className="text-white hover:bg-white/20 p-2 rounded-full transition-colors flex items-center justify-center w-10 h-10"
title="Open Queue"
>
<FaListUl className="w-5 h-5" />
</button>
)}
<button
<div className="absolute inset-0 bg-black/30" />
<div className="relative h-full w-full flex flex-col">
{/* Mobile Close Handle */}
{isMobile && (
<div className="flex justify-center py-4 px-4">
<div
onClick={onClose}
className="text-white hover:bg-white/20 p-2 rounded-full transition-colors flex items-center justify-center w-10 h-10"
title="Close Player"
className="cursor-pointer px-8 py-3 -mx-8 -my-3"
style={{ touchAction: 'manipulation' }}
>
<FaXmark className="w-5 h-5" />
</button>
<div className="w-8 h-1 bg-gray-300 rounded-full opacity-60" />
</div>
</div>
</div>
)}
{/* Main Content */}
<div className="h-full flex flex-col lg:flex-row gap-4 lg:gap-8 p-4 lg:p-6 overflow-hidden">
{/* Left Side - Album Art and Controls */}
<div className="flex flex-col items-center justify-center min-h-0 flex-1 min-w-0">
{/* Album Art */}
<div className="relative mb-4 lg:mb-6 shrink-0">
<Image
src={currentTrack.coverArt || '/default-album.png'}
alt={currentTrack.album}
width={320}
height={320}
className="w-56 h-56 sm:w-64 sm:h-64 lg:w-80 lg:h-80 rounded-lg shadow-2xl object-cover"
priority
/>
</div>
{/* Track Info */}
<div className="text-center mb-4 lg:mb-6 px-4 shrink-0 max-w-full">
<h1 className="text-lg sm:text-xl lg:text-3xl font-bold text-foreground mb-2 line-clamp-2 leading-tight">
{currentTrack.name}
</h1>
<Link href={`/artist/${currentTrack.artistId}`} className="text-base sm:text-lg lg:text-xl text-foreground/80 mb-1 line-clamp-1">
{currentTrack.artist}
</Link>
<Link href={`/album/${currentTrack.albumId}`} className="text-sm sm:text-base lg:text-lg text-foreground/60 line-clamp-1 cursor-pointer hover:underline">
{currentTrack.album}
</Link>
</div>
{/* Progress */}
<div className="w-full max-w-sm lg:max-w-md mb-4 lg:mb-6 px-4 shrink-0">
<div className="w-full" onClick={handleSeek}>
<Progress value={progress} className="h-2 cursor-pointer" />
</div>
<div className="flex justify-between text-sm text-foreground/60 mt-2">
<span>{formatTime(currentTime)}</span>
<span>{formatTime(duration)}</span>
</div>
</div>
{/* Controls */}
<div className="flex items-center gap-3 sm:gap-4 lg:gap-6 mb-4 lg:mb-6 shrink-0">
<button
onClick={toggleShuffle}
className={`p-2 hover:bg-gray-700/50 rounded-full transition-colors ${
shuffle ? 'text-primary bg-primary/20' : 'text-gray-400'
}`}
title={shuffle ? 'Shuffle On - Queue is shuffled' : 'Shuffle Off - Click to shuffle queue'}
>
<FaShuffle className="w-4 h-4 sm:w-5 sm:h-5" />
</button>
<button
onClick={playPreviousTrack}
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors">
<FaBackward className="w-4 h-4 sm:w-5 sm:h-5" />
</button>
<button
onClick={togglePlayPause}
className="p-3 hover:bg-gray-700/50 rounded-full transition-colors">
{isPlaying ? (
<FaPause className="w-8 h-8 sm:w-10 sm:h-10" />
) : (
<FaPlay className="w-8 h-8 sm:w-10 sm:h-10" />
)}
</button>
<button
onClick={playNextTrack}
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors">
<FaForward className="w-4 h-4 sm:w-5 sm:h-5" />
</button>
<button
onClick={toggleCurrentTrackStar}
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors"
title={currentTrack?.starred ? 'Remove from favorites' : 'Add to favorites'}
>
<Heart
className={`w-4 h-4 sm:w-5 sm:h-5 ${currentTrack?.starred ? 'text-primary fill-primary' : 'text-gray-400'}`}
/>
</button>
</div>
{/* Volume and Lyrics Toggle */}
<div className="flex items-center gap-3 shrink-0 justify-center">
<button
onMouseEnter={() => setShowVolumeSlider(true)}
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors">
{volume === 0 ? (
<FaVolumeXmark className="w-4 h-4 sm:w-5 sm:h-5" />
) : (
<FaVolumeHigh className="w-4 h-4 sm:w-5 sm:h-5" />
)}
</button>
{lyrics.length > 0 && (
<button
onClick={() => setShowLyrics(!showLyrics)}
className={`p-2 hover:bg-gray-700/50 rounded-full transition-colors ${
showLyrics ? 'text-primary bg-primary/20' : 'text-gray-500'
}`}
title={showLyrics ? 'Hide Lyrics' : 'Show Lyrics'}
{/* Desktop Header */}
{!isMobile && (
<div className="absolute top-0 right-0 z-10 p-4 lg:p-6">
<div className="flex items-center gap-2">
{onOpenQueue && (
<button
onClick={onOpenQueue}
className="text-white hover:bg-white/20 p-2 rounded-full transition-colors flex items-center justify-center w-10 h-10"
title="Open Queue"
>
<FaQuoteLeft className="w-4 h-4 sm:w-5 sm:h-5" />
<FaListUl className="w-5 h-5" />
</button>
)}
{showVolumeSlider && (
<div
className="w-16 sm:w-20 lg:w-24"
onMouseLeave={() => setShowVolumeSlider(false)}
>
<input
type="range"
min="0"
max="100"
value={volume * 100}
onChange={handleVolumeChange}
className="w-full accent-foreground"
/>
</div>
)}
<button
onClick={onClose}
className="text-white hover:bg-white/20 p-2 rounded-full transition-colors flex items-center justify-center w-10 h-10"
title="Close Player"
>
<FaXmark className="w-5 h-5" />
</button>
</div>
</div>
)}
{/* Right Side - Lyrics */}
{showLyrics && lyrics.length > 0 && (
<div className="flex-1 min-w-0 min-h-0 flex flex-col" ref={lyricsRef}>
<div className="h-full flex flex-col">
<ScrollArea className="flex-1 min-h-0">
<div className="space-y-2 sm:space-y-3 pl-4 pr-4 py-4">
{lyrics.map((line, index) => (
<div
key={index}
data-lyric-index={index}
onClick={() => handleLyricClick(line.time)}
className={`text-sm sm:text-base lg:text-base leading-relaxed transition-all duration-300 break-words cursor-pointer hover:text-foreground ${
index === currentLyricIndex
? 'text-foreground font-bold text-2xl'
: index < currentLyricIndex
? 'text-foreground/60'
: 'text-foreground/40'
{/* Main Content */}
<div className="flex-1 overflow-hidden">
{isMobile ? (
/* Mobile Tab Content */
<div className="h-full flex flex-col">
<div className="flex-1 overflow-hidden">
{activeTab === 'player' && (
<div className="h-full flex flex-col justify-center items-center px-8 py-4">
{/* Mobile Album Art */}
<div className="relative mb-6 shrink-0">
<Image
src={currentTrack.coverArt || '/default-album.png'}
alt={currentTrack.album}
width={260}
height={260}
className={`rounded-lg shadow-2xl object-cover transition-all duration-300 ${
!isPlaying ? 'w-52 h-52 opacity-70 scale-95' : 'w-64 h-64'
}`}
style={{
wordWrap: 'break-word',
overflowWrap: 'break-word',
hyphens: 'auto',
paddingBottom: '4px',
paddingLeft: '8px'
}}
title={`Click to jump to ${formatTime(line.time)}`}
>
{line.text || '♪'}
priority
/>
</div>
{/* Track Info - Left Aligned and Heart on Same Line */}
<div className="w-full mb-6 shrink-0">
<div className="flex items-center justify-between mb-0">
<h1 className="text-2xl font-bold text-foreground line-clamp-1 flex-1 text-left">
{currentTrack.name}
</h1>
<button
onClick={toggleCurrentTrackStar}
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors ml-3 pb-0"
title={currentTrack?.starred ? 'Remove from favorites' : 'Add to favorites'}
>
<Heart
className={`w-6 h-6 ${currentTrack?.starred ? 'text-primary fill-primary' : 'text-gray-400'}`}
/>
</button>
</div>
))}
{/* Add extra padding at the bottom to allow last lyric to center */}
<div style={{ height: '200px' }} />
<Link
href={`/artist/${currentTrack.artistId}`}
className="text-lg text-foreground/80 line-clamp-1 block text-left mb-1"
>
{currentTrack.artist}
</Link>
<Link
href={`/album/${currentTrack.albumId}`}
className="text-base text-foreground/60 line-clamp-1 cursor-pointer hover:underline block text-left"
>
{currentTrack.album}
</Link>
</div>
{/* Progress */}
<div className="w-full mb-4 shrink-0">
<div className="w-full" onClick={handleSeek}>
<Progress value={progress} className="h-2 cursor-pointer" />
</div>
{/* Time below progress on mobile */}
<div className="flex justify-between text-sm text-foreground/60 mt-2">
<span>{formatTime(currentTime)}</span>
<span>{formatTime(duration)}</span>
</div>
</div>
{/* Controls */}
<div className="flex items-center gap-6 mb-4 shrink-0">
<button
onClick={toggleShuffle}
className={`p-2 hover:bg-gray-700/50 rounded-full transition-colors ${
shuffle ? 'text-primary bg-primary/20' : 'text-gray-400'
}`}
title={shuffle ? 'Shuffle On - Queue is shuffled' : 'Shuffle Off - Click to shuffle queue'}
>
<FaShuffle className="w-5 h-5" />
</button>
<button
onClick={playPreviousTrack}
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors">
<FaBackward className="w-6 h-6" />
</button>
<button
onClick={togglePlayPause}
className="p-4 hover:bg-gray-700/50 rounded-full transition-colors">
{isPlaying ? (
<FaPause className="w-10 h-10" />
) : (
<FaPlay className="w-10 h-10" />
)}
</button>
<button
onClick={playNextTrack}
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors">
<FaForward className="w-6 h-6" />
</button>
<button
onMouseEnter={() => setShowVolumeSlider(true)}
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors">
{volume === 0 ? (
<FaVolumeXmark className="w-5 h-5" />
) : (
<FaVolumeHigh className="w-5 h-5" />
)}
</button>
</div>
{/* Volume Slider */}
{showVolumeSlider && (
<div
className="w-32 mb-4"
onMouseLeave={() => setShowVolumeSlider(false)}
>
<input
type="range"
min="0"
max="100"
value={volume * 100}
onChange={handleVolumeChange}
className="w-full accent-foreground"
/>
</div>
)}
</div>
</ScrollArea>
)}
{activeTab === 'lyrics' && lyrics.length > 0 && (
<div className="h-full flex flex-col px-4">
<div
className="flex-1 overflow-y-auto"
ref={lyricsRef}
>
<div className="space-y-3 py-4">
{lyrics.map((line, index) => (
<div
key={index}
data-lyric-index={index}
onClick={() => handleLyricClick(line.time)}
className={`text-base leading-relaxed transition-all duration-300 break-words cursor-pointer hover:text-foreground px-2 ${
index === currentLyricIndex
? 'text-foreground font-bold text-xl'
: index < currentLyricIndex
? 'text-foreground/60'
: 'text-foreground/40'
}`}
style={{
wordWrap: 'break-word',
overflowWrap: 'break-word',
hyphens: 'auto',
paddingBottom: '4px'
}}
title={`Click to jump to ${formatTime(line.time)}`}
>
{line.text || '♪'}
</div>
))}
<div style={{ height: '200px' }} />
</div>
</div>
</div>
)}
{activeTab === 'queue' && (
<div className="h-full flex flex-col px-4">
<ScrollArea className="flex-1">
<div className="space-y-2 py-4">
{queue.map((track, index) => (
<div
key={`${track.id}-${index}`}
className={`flex items-center p-3 rounded-lg ${
track.id === currentTrack?.id ? 'bg-primary/20' : 'bg-gray-800/30'
}`}
>
<Image
src={track.coverArt || '/default-album.png'}
alt={track.album}
width={40}
height={40}
className="rounded mr-3"
/>
<div className="flex-1 min-w-0">
<p className="font-medium text-sm truncate">
{track.name}
</p>
<p className="text-xs text-gray-400 truncate">
{track.artist}
</p>
</div>
</div>
))}
</div>
</ScrollArea>
</div>
)}
</div>
{/* Mobile Tab Bar */}
<div className="flex-shrink-0 pb-safe">
<div className="flex justify-around py-4 mb-2">
<button
onClick={() => setActiveTab('player')}
className={`flex items-center justify-center p-4 rounded-full transition-colors ${
activeTab === 'player' ? 'text-primary bg-primary/20' : 'text-gray-400'
}`}
>
<FaPlay className="w-6 h-6" />
</button>
{lyrics.length > 0 && (
<button
onClick={() => setActiveTab('lyrics')}
className={`flex items-center justify-center p-4 rounded-full transition-colors ${
activeTab === 'lyrics' ? 'text-primary bg-primary/20' : 'text-gray-400'
}`}
>
<FaQuoteLeft className="w-6 h-6" />
</button>
)}
<button
onClick={() => setActiveTab('queue')}
className={`flex items-center justify-center p-4 rounded-full transition-colors ${
activeTab === 'queue' ? 'text-primary bg-primary/20' : 'text-gray-400'
}`}
>
<FaListUl className="w-6 h-6" />
</button>
</div>
</div>
</div>
) : (
/* Desktop Layout */
<div className="h-full flex flex-row gap-8 p-6 overflow-hidden">
{/* Left Side - Album Art and Controls */}
<div className="flex flex-col items-center justify-center min-h-0 flex-1 min-w-0">
{/* Album Art */}
<div className="relative mb-6 shrink-0">
<Image
src={currentTrack.coverArt || '/default-album.png'}
alt={currentTrack.album}
width={320}
height={320}
className="w-80 h-80 rounded-lg shadow-2xl object-cover"
priority
/>
</div>
{/* Track Info */}
<div className="text-center mb-6 px-4 shrink-0 max-w-full">
<h1 className="text-3xl font-bold text-foreground line-clamp-2 leading-tight mb-2">
{currentTrack.name}
</h1>
<Link href={`/artist/${currentTrack.artistId}`} className="text-xl text-foreground/80 mb-1 line-clamp-1">
{currentTrack.artist}
</Link>
<Link href={`/album/${currentTrack.albumId}`} className="text-lg text-foreground/60 line-clamp-1 cursor-pointer hover:underline">
{currentTrack.album}
</Link>
</div>
{/* Progress */}
<div className="w-full max-w-md mb-6 px-4 shrink-0">
<div className="w-full" onClick={handleSeek}>
<Progress value={progress} className="h-2 cursor-pointer" />
</div>
<div className="flex justify-between text-sm text-foreground/60 mt-2">
<span>{formatTime(currentTime)}</span>
<span>{formatTime(duration)}</span>
</div>
</div>
{/* Controls */}
<div className="flex items-center gap-6 mb-6 shrink-0">
<button
onClick={toggleShuffle}
className={`p-2 hover:bg-gray-700/50 rounded-full transition-colors ${
shuffle ? 'text-primary bg-primary/20' : 'text-gray-400'
}`}
title={shuffle ? 'Shuffle On - Queue is shuffled' : 'Shuffle Off - Click to shuffle queue'}
>
<FaShuffle className="w-5 h-5" />
</button>
<button
onClick={playPreviousTrack}
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors">
<FaBackward className="w-5 h-5" />
</button>
<button
onClick={togglePlayPause}
className="p-3 hover:bg-gray-700/50 rounded-full transition-colors">
{isPlaying ? (
<FaPause className="w-10 h-10" />
) : (
<FaPlay className="w-10 h-10" />
)}
</button>
<button
onClick={playNextTrack}
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors">
<FaForward className="w-5 h-5" />
</button>
<button
onClick={toggleCurrentTrackStar}
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors"
title={currentTrack?.starred ? 'Remove from favorites' : 'Add to favorites'}
>
<Heart
className={`w-5 h-5 ${currentTrack?.starred ? 'text-primary fill-primary' : 'text-gray-400'}`}
/>
</button>
</div>
{/* Volume and Lyrics Toggle - Desktop Only */}
<div className="flex items-center gap-3 shrink-0 justify-center">
<button
onMouseEnter={() => setShowVolumeSlider(true)}
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors">
{volume === 0 ? (
<FaVolumeXmark className="w-5 h-5" />
) : (
<FaVolumeHigh className="w-5 h-5" />
)}
</button>
{lyrics.length > 0 && (
<button
onClick={() => setShowLyrics(!showLyrics)}
className={`p-2 hover:bg-gray-700/50 rounded-full transition-colors ${
showLyrics ? 'text-primary bg-primary/20' : 'text-gray-500'
}`}
title={showLyrics ? 'Hide Lyrics' : 'Show Lyrics'}
>
<FaQuoteLeft className="w-5 h-5" />
</button>
)}
{showVolumeSlider && (
<div
className="w-24"
onMouseLeave={() => setShowVolumeSlider(false)}
>
<input
type="range"
min="0"
max="100"
value={volume * 100}
onChange={handleVolumeChange}
className="w-full accent-foreground"
/>
</div>
)}
</div>
</div>
{/* Right Side - Lyrics (Desktop Only) */}
{showLyrics && lyrics.length > 0 && (
<div className="flex-1 min-w-0 min-h-0 flex flex-col" ref={lyricsRef}>
<div className="h-full flex flex-col">
<ScrollArea className="flex-1 min-h-0">
<div className="space-y-3 pl-4 pr-4 py-4">
{lyrics.map((line, index) => (
<div
key={index}
data-lyric-index={index}
onClick={() => handleLyricClick(line.time)}
className={`text-base leading-relaxed transition-all duration-300 break-words cursor-pointer hover:text-foreground ${
index === currentLyricIndex
? 'text-foreground font-bold text-2xl'
: index < currentLyricIndex
? 'text-foreground/60'
: 'text-foreground/40'
}`}
style={{
wordWrap: 'break-word',
overflowWrap: 'break-word',
hyphens: 'auto',
paddingBottom: '4px',
paddingLeft: '8px'
}}
title={`Click to jump to ${formatTime(line.time)}`}
>
{line.text || '♪'}
</div>
))}
<div style={{ height: '200px' }} />
</div>
</ScrollArea>
</div>
</div>
)}
</div>
)}
</div>

View File

@@ -36,7 +36,7 @@ export function PopularSongs({ songs, artistName }: PopularSongsProps) {
artist: song.artist,
album: song.album,
duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 1200) : undefined,
albumId: song.albumId,
artistId: song.artistId,
starred: !!song.starred
@@ -95,7 +95,7 @@ export function PopularSongs({ songs, artistName }: PopularSongsProps) {
<div className="relative w-12 h-12 bg-muted rounded-md overflow-hidden shrink-0">
{song.coverArt && api && (
<Image
src={api.getCoverArtUrl(song.coverArt, 96)}
src={api.getCoverArtUrl(song.coverArt, 300)}
alt={song.album}
width={48}
height={48}

View File

@@ -9,6 +9,8 @@ import { PostHogProvider } from "../components/PostHogProvider";
import { WhatsNewPopup } from "../components/WhatsNewPopup";
import Ihateserverside from "./ihateserverside";
import DynamicViewportTheme from "./DynamicViewportTheme";
import ThemeColorHandler from "./ThemeColorHandler";
import { useViewportThemeColor } from "@/hooks/use-viewport-theme-color";
import { LoginForm } from "./start-screen";
import Image from "next/image";
@@ -83,6 +85,7 @@ export default function RootLayoutClient({ children }: { children: React.ReactNo
<PostHogProvider>
<ThemeProvider>
<DynamicViewportTheme />
<ThemeColorHandler />
<NavidromeConfigProvider>
<NavidromeProvider>
<NavidromeErrorBoundary>

View File

@@ -38,7 +38,7 @@ export function SettingsManagement() {
};
return (
<Card>
<Card className="py-5">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Settings className="h-5 w-5" />

View File

@@ -153,7 +153,7 @@ export function SidebarCustomization() {
return (
<Card>
<Card className="py-5">
<CardHeader>
<CardTitle>Sidebar Customization</CardTitle>
<CardDescription>

View File

@@ -1,14 +1,16 @@
'use client';
import React, { useState, useEffect } from 'react';
import { Song } from '@/lib/navidrome';
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { Song, Album } from '@/lib/navidrome';
import { useNavidrome } from '@/app/components/NavidromeContext';
import { useAudioPlayer } from '@/app/components/AudioPlayerContext';
import { useIsMobile } from '@/hooks/use-mobile';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Play, Heart, Music, Shuffle } from 'lucide-react';
import Image from 'next/image';
import Link from 'next/link';
import { UserProfile } from './UserProfile';
interface SongRecommendationsProps {
userName?: string;
@@ -17,14 +19,26 @@ interface SongRecommendationsProps {
export function SongRecommendations({ userName }: SongRecommendationsProps) {
const { api, isConnected } = useNavidrome();
const { playTrack, shuffle, toggleShuffle } = useAudioPlayer();
const isMobile = useIsMobile();
const [recommendedSongs, setRecommendedSongs] = useState<Song[]>([]);
const [recommendedAlbums, setRecommendedAlbums] = useState<Album[]>([]);
const [loading, setLoading] = useState(true);
const [songStates, setSongStates] = useState<Record<string, boolean>>({});
const [imageLoadingStates, setImageLoadingStates] = useState<Record<string, boolean>>({});
// Get greeting based on time of day
const hour = new Date().getHours();
const greeting = hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening';
// Memoize the greeting to prevent recalculation
const greeting = useMemo(() => {
const hour = new Date().getHours();
return hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening';
}, []);
// Memoized callbacks to prevent re-renders
const handleImageLoad = useCallback(() => {
// Image loaded - no state update needed to prevent re-renders
}, []);
const handleImageError = useCallback(() => {
// Image error - no state update needed to prevent re-renders
}, []);
useEffect(() => {
const loadRecommendations = async () => {
@@ -32,43 +46,47 @@ export function SongRecommendations({ userName }: SongRecommendationsProps) {
setLoading(true);
try {
// Get random albums and extract songs from them
const randomAlbums = await api.getAlbums('random', 10); // Get 10 random albums
const allSongs: Song[] = [];
// Get random albums for both mobile album view and desktop song extraction
const randomAlbums = await api.getAlbums('random', 10);
// Get songs from first few albums
for (let i = 0; i < Math.min(3, randomAlbums.length); i++) {
try {
const albumSongs = await api.getAlbumSongs(randomAlbums[i].id);
allSongs.push(...albumSongs);
} catch (error) {
console.error('Failed to get album songs:', error);
if (isMobile) {
// For mobile: show 6 random albums
setRecommendedAlbums(randomAlbums.slice(0, 6));
} else {
// For desktop: extract songs from albums (original behavior)
const allSongs: Song[] = [];
// Get songs from first few albums
for (let i = 0; i < Math.min(3, randomAlbums.length); i++) {
try {
const albumSongs = await api.getAlbumSongs(randomAlbums[i].id);
allSongs.push(...albumSongs);
} catch (error) {
console.error('Failed to get album songs:', error);
}
}
// Shuffle and limit to 6 songs
const shuffled = allSongs.sort(() => Math.random() - 0.5);
const recommendations = shuffled.slice(0, 6);
setRecommendedSongs(recommendations);
// Initialize starred states for songs
const states: Record<string, boolean> = {};
recommendations.forEach((song: Song) => {
states[song.id] = !!song.starred;
});
setSongStates(states);
}
// Shuffle and limit to 6 songs
const shuffled = allSongs.sort(() => Math.random() - 0.5);
const recommendations = shuffled.slice(0, 6);
setRecommendedSongs(recommendations);
// Initialize starred states and image loading states
const states: Record<string, boolean> = {};
const imageStates: Record<string, boolean> = {};
recommendations.forEach((song: Song) => {
states[song.id] = !!song.starred;
imageStates[song.id] = true; // Start with loading state
});
setSongStates(states);
setImageLoadingStates(imageStates);
} catch (error) {
console.error('Failed to load song recommendations:', error);
console.error('Failed to load recommendations:', error);
} finally {
setLoading(false);
}
};
loadRecommendations();
}, [api, isConnected]);
}, [api, isConnected, isMobile]);
const handlePlaySong = async (song: Song) => {
if (!api) return;
@@ -83,7 +101,7 @@ export function SongRecommendations({ userName }: SongRecommendationsProps) {
album: song.album || 'Unknown Album',
albumId: song.albumId || '',
duration: song.duration || 0,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 64) : undefined,
starred: !!song.starred
};
await playTrack(track, true);
@@ -92,17 +110,50 @@ export function SongRecommendations({ userName }: SongRecommendationsProps) {
}
};
const handlePlayAlbum = async (album: Album) => {
if (!api) return;
try {
// Get album songs and play the first one
const albumSongs = await api.getAlbumSongs(album.id);
if (albumSongs.length > 0) {
const track = {
id: albumSongs[0].id,
name: albumSongs[0].title,
url: api.getStreamUrl(albumSongs[0].id),
artist: albumSongs[0].artist || 'Unknown Artist',
artistId: albumSongs[0].artistId || '',
album: albumSongs[0].album || 'Unknown Album',
albumId: albumSongs[0].albumId || '',
duration: albumSongs[0].duration || 0,
coverArt: albumSongs[0].coverArt ? api.getCoverArtUrl(albumSongs[0].coverArt, 64) : undefined,
starred: !!albumSongs[0].starred
};
await playTrack(track, true);
}
} catch (error) {
console.error('Failed to play album:', error);
}
};
const handleShuffleAll = async () => {
if (recommendedSongs.length === 0) return;
if (isMobile && recommendedAlbums.length === 0) return;
if (!isMobile && recommendedSongs.length === 0) return;
// Enable shuffle if not already on
if (!shuffle) {
toggleShuffle();
}
// Play a random song from recommendations
const randomSong = recommendedSongs[Math.floor(Math.random() * recommendedSongs.length)];
await handlePlaySong(randomSong);
if (isMobile) {
// Play a random album
const randomAlbum = recommendedAlbums[Math.floor(Math.random() * recommendedAlbums.length)];
await handlePlayAlbum(randomAlbum);
} else {
// Play a random song from recommendations
const randomSong = recommendedSongs[Math.floor(Math.random() * recommendedSongs.length)];
await handlePlaySong(randomSong);
}
};
const formatDuration = (duration: number): string => {
@@ -118,11 +169,19 @@ export function SongRecommendations({ userName }: SongRecommendationsProps) {
<div className="h-8 w-48 bg-muted animate-pulse rounded" />
<div className="h-4 w-64 bg-muted animate-pulse rounded" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="h-16 bg-muted animate-pulse rounded" />
))}
</div>
{isMobile ? (
<div className="grid grid-cols-3 gap-3">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="aspect-square bg-muted animate-pulse rounded" />
))}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="h-16 bg-muted animate-pulse rounded" />
))}
</div>
)}
</div>
);
}
@@ -135,95 +194,153 @@ export function SongRecommendations({ userName }: SongRecommendationsProps) {
{greeting}{userName ? `, ${userName}` : ''}!
</h2>
<p className="text-muted-foreground">
Here are some songs you might enjoy
{isMobile ? 'Here are some albums you might enjoy' : 'Here are some songs you might enjoy'}
</p>
</div>
{recommendedSongs.length > 0 && (
<Button onClick={handleShuffleAll} variant="outline" size="sm">
<Shuffle className="w-4 h-4 mr-2" />
Shuffle All
</Button>
)}
<div className="flex items-center gap-3">
{/* Mobile User Profile */}
{isMobile && <UserProfile variant="mobile" />}
{/* Shuffle All Button (Desktop only) */}
{(isMobile ? recommendedAlbums.length > 0 : recommendedSongs.length > 0) && !isMobile && (
<Button onClick={handleShuffleAll} variant="outline" size="sm">
<Shuffle className="w-4 h-4 mr-2" />
Shuffle All
</Button>
)}
</div>
</div>
{recommendedSongs.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{recommendedSongs.map((song) => (
<Card
key={song.id}
className="group cursor-pointer hover:bg-accent/50 transition-colors py-2"
onClick={() => handlePlaySong(song)}
>
<CardContent className="px-2">
<div className="flex items-center gap-3">
<div className="relative w-12 h-12 rounded overflow-hidden bg-muted flex-shrink-0">
{song.coverArt && api ? (
<>
{imageLoadingStates[song.id] && (
<div className="absolute inset-0 bg-muted flex items-center justify-center">
<Music className="w-6 h-6 text-muted-foreground animate-pulse" />
</div>
)}
<Image
src={api.getCoverArtUrl(song.coverArt, 100)}
alt={song.title}
fill
className={`object-cover transition-opacity duration-300 ${
imageLoadingStates[song.id] ? 'opacity-0' : 'opacity-100'
}`}
sizes="48px"
onLoad={() => setImageLoadingStates(prev => ({ ...prev, [song.id]: false }))}
onError={() => setImageLoadingStates(prev => ({ ...prev, [song.id]: false }))}
/>
</>
{isMobile ? (
/* Mobile: Show albums in 3x2 grid */
recommendedAlbums.length > 0 ? (
<div className="grid grid-cols-3 gap-3">
{recommendedAlbums.map((album) => (
<div key={album.id} className="space-y-2">
<Link
href={`/album/${album.id}`}
className="group cursor-pointer block"
>
<div className="relative aspect-square rounded-lg overflow-hidden bg-muted">
{album.coverArt && api ? (
<Image
src={api.getCoverArtUrl(album.coverArt, 300)}
alt={album.name}
width={600}
height={600}
className="object-cover"
sizes="(max-width: 768px) 33vw, 200px"
onLoad={handleImageLoad}
onError={handleImageError}
loading="lazy"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<Music className="w-6 h-6 text-muted-foreground" />
</div>
)}
{!imageLoadingStates[song.id] && (
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<Play className="w-4 h-4 text-white" />
<Music className="w-8 h-8 text-muted-foreground" />
</div>
)}
</div>
<div className="flex-1 min-w-0">
<p className="font-medium truncate">{song.title}</p>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Link
href={`/artist/${song.artistId}`}
className="hover:underline truncate"
onClick={(e) => e.stopPropagation()}
>
{song.artist}
</Link>
{song.duration && (
</Link>
<div className="space-y-1">
<Link
href={`/album/${album.id}`}
className="font-medium text-sm truncate hover:underline block"
>
{album.name}
</Link>
<Link
href={`/artist/${album.artistId || album.artist}`}
className="text-xs text-muted-foreground truncate hover:underline block"
>
{album.artist}
</Link>
</div>
</div>
))}
</div>
) : (
<Card>
<CardContent className="p-6 text-center">
<Music className="w-12 h-12 mx-auto mb-4 text-muted-foreground" />
<p className="text-muted-foreground">
No albums available for recommendations
</p>
</CardContent>
</Card>
)
) : (
/* Desktop: Show songs in original format */
recommendedSongs.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{recommendedSongs.map((song) => (
<Card
key={song.id}
className="group cursor-pointer hover:bg-accent/50 transition-colors py-2"
onClick={() => handlePlaySong(song)}
>
<CardContent className="px-2">
<div className="flex items-center gap-3">
<div className="relative w-12 h-12 rounded overflow-hidden bg-muted flex-shrink-0">
{song.coverArt && api ? (
<>
<span></span>
<span>{formatDuration(song.duration)}</span>
<Image
src={api.getCoverArtUrl(song.coverArt, 48)}
alt={song.title}
fill
className="object-cover"
sizes="48px"
onLoad={handleImageLoad}
onError={handleImageError}
loading="lazy"
/>
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<Play className="w-4 h-4 text-white" />
</div>
</>
) : (
<div className="w-full h-full flex items-center justify-center">
<Music className="w-6 h-6 text-muted-foreground" />
</div>
)}
</div>
<div className="flex-1 min-w-0">
<p className="font-medium truncate">{song.title}</p>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Link
href={`/artist/${song.artistId}`}
className="hover:underline truncate"
onClick={(e) => e.stopPropagation()}
>
{song.artist}
</Link>
{song.duration && (
<>
<span></span>
<span>{formatDuration(song.duration)}</span>
</>
)}
</div>
</div>
{songStates[song.id] && (
<Heart className="w-4 h-4 text-primary flex-shrink-0" fill="currentColor" />
)}
</div>
{songStates[song.id] && (
<Heart className="w-4 h-4 text-primary flex-shrink-0" fill="currentColor" />
)}
</div>
</CardContent>
</Card>
))}
</div>
) : (
<Card>
<CardContent className="p-6 text-center">
<Music className="w-12 h-12 mx-auto mb-4 text-muted-foreground" />
<p className="text-muted-foreground">
No songs available for recommendations
</p>
</CardContent>
</Card>
</CardContent>
</Card>
))}
</div>
) : (
<Card>
<CardContent className="p-6 text-center">
<Music className="w-12 h-12 mx-auto mb-4 text-muted-foreground" />
<p className="text-muted-foreground">
No songs available for recommendations
</p>
</CardContent>
</Card>
)
)}
</div>
);

View File

@@ -0,0 +1,8 @@
'use client';
import { useViewportThemeColor } from '@/hooks/use-viewport-theme-color';
export default function ThemeColorHandler() {
useViewportThemeColor();
return null;
}

View File

@@ -0,0 +1,209 @@
'use client';
import React, { useState, useEffect } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { User, ChevronDown, Settings, LogOut } from 'lucide-react';
import { useNavidrome } from '@/app/components/NavidromeContext';
import { getGravatarUrl } from '@/lib/gravatar';
import { User as NavidromeUser } from '@/lib/navidrome';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Button } from '@/components/ui/button';
interface UserProfileProps {
variant?: 'desktop' | 'mobile';
}
export function UserProfile({ variant = 'desktop' }: UserProfileProps) {
const { api, isConnected } = useNavidrome();
const [userInfo, setUserInfo] = useState<NavidromeUser | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUserInfo = async () => {
if (!api || !isConnected) {
setLoading(false);
return;
}
try {
const user = await api.getUserInfo();
setUserInfo(user);
} catch (error) {
console.error('Failed to fetch user info:', error);
} finally {
setLoading(false);
}
};
fetchUserInfo();
}, [api, isConnected]);
const handleLogout = () => {
// Clear Navidrome config and reload
localStorage.removeItem('navidrome-config');
window.location.reload();
};
if (!userInfo) {
if (variant === 'desktop') {
return (
<Link href="/settings">
<Button variant="ghost" size="sm" className="gap-2">
<User className="w-4 h-4" />
</Button>
</Link>
);
} else {
return (
<Link href="/settings">
<Button variant="ghost" size="sm" className="gap-2">
<User className="w-4 h-4" />
</Button>
</Link>
);
}
}
const gravatarUrl = userInfo.email
? getGravatarUrl(userInfo.email, variant === 'desktop' ? 32 : 48, 'identicon')
: null;
if (variant === 'desktop') {
// Desktop: Only show profile icon
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="flex items-center gap-1 h-auto p-2">
{gravatarUrl ? (
<Image
src={gravatarUrl}
alt={`${userInfo.username}'s avatar`}
width={16}
height={16}
className="rounded-full"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.style.display = 'none';
}}
/>
) : (
<div className="w-8 h-8 bg-primary/10 rounded-full flex items-center justify-center">
<User className="w-4 h-4 text-primary" />
</div>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<div className="flex items-center gap-2 p-2">
{gravatarUrl ? (
<Image
src={gravatarUrl}
alt={`${userInfo.username}'s avatar`}
width={16}
height={16}
className="rounded-full"
/>
) : (
<div className="w-10 h-10 bg-primary/10 rounded-full flex items-center justify-center">
<User className="w-5 h-5 text-primary" />
</div>
)}
<div>
<p className="text-sm font-medium">{userInfo.username}</p>
{userInfo.email && (
<p className="text-xs text-muted-foreground">{userInfo.email}</p>
)}
</div>
</div>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link href="/settings" className="flex items-center gap-2">
<Settings className="w-4 h-4" />
Settings
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={handleLogout}
className="flex items-center gap-2 text-red-600 focus:text-red-600"
>
<LogOut className="w-4 h-4" />
Logout
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
} else {
// Mobile: Show only icon with dropdown
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="flex items-center gap-1 h-auto p-2">
{gravatarUrl ? (
<Image
src={gravatarUrl}
alt={`${userInfo.username}'s avatar`}
width={32}
height={32}
className="rounded-full"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.style.display = 'none';
}}
/>
) : (
<div className="w-8 h-8 bg-primary/10 rounded-full flex items-center justify-center">
<User className="w-4 h-4 text-primary" />
</div>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<div className="flex items-center gap-2 p-2">
{gravatarUrl ? (
<Image
src={gravatarUrl}
alt={`${userInfo.username}'s avatar`}
width={32}
height={32}
className="rounded-full"
/>
) : (
<div className="w-10 h-10 bg-primary/10 rounded-full flex items-center justify-center">
<User className="w-5 h-5 text-primary" />
</div>
)}
<div>
<p className="text-sm font-medium">{userInfo.username}</p>
{userInfo.email && (
<p className="text-xs text-muted-foreground">{userInfo.email}</p>
)}
</div>
</div>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link href="/settings" className="flex items-center gap-2">
<Settings className="w-4 h-4" />
Settings
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={handleLogout}
className="flex items-center gap-2 text-red-600 focus:text-red-600"
>
<LogOut className="w-4 h-4" />
Logout
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
}

View File

@@ -1,16 +1,33 @@
'use client';
import { useState, useEffect } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
// Current app version from package.json
const APP_VERSION = '2025.07.10';
const APP_VERSION = '2025.07.31';
// Changelog data - add new versions at the top
const CHANGELOG = [
{
version: '2025.07.31',
title: 'July End of Month Update',
changes: [
'Native support for moblie devices (using pwa)',
],
fixes: [
'Fixed issue with mobile navigation bar not displaying correctly',
'Improved performance on mobile devices',
'Resolved layout issues on smaller screens',
'Fixed audio player controls not responding on mobile',
'Improved touch interactions for better usability',
'Fixed issue with album artwork not loading on mobile',
'Resolved bug with search functionality on mobile devices',
'Improved caching for faster load times on mobile',
],
breaking: [
]
},
{
version: '2025.07.10',
title: 'July Major Update',
@@ -189,65 +206,86 @@ export function WhatsNewPopup() {
);
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-2xl max-h-[80vh]">
<DialogHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
<div>
<DialogTitle className="text-2xl font-bold flex items-center gap-2">
What&apos;s New in Mice
<Badge variant="outline">
{tab === 'latest' ? currentVersionChangelog.version : archiveChangelog?.version}
</Badge>
</DialogTitle>
<>
{isOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div
className="fixed inset-0 bg-black/50"
onClick={handleClose}
/>
{/* Dialog content */}
<div className="relative bg-background rounded-lg shadow-lg max-w-2xl w-full mx-4 max-h-[80vh] flex flex-col">
{/* Header */}
<div className="flex flex-row items-center justify-between space-y-0 p-6 pb-4 shrink-0">
<div>
<h2 className="text-2xl font-bold flex items-center gap-2">
What&apos;s New in Mice
<Badge variant="outline">
{tab === 'latest' ? currentVersionChangelog.version : archiveChangelog?.version}
</Badge>
</h2>
</div>
<button
onClick={handleClose}
className="text-muted-foreground hover:text-foreground transition-colors"
>
</button>
</div>
{/* Tabs */}
<div className="flex gap-2 px-6 pt-4 shrink-0">
<Button
variant={tab === 'latest' ? 'default' : 'outline'}
size="sm"
onClick={() => setTab('latest')}
>
Latest
</Button>
<Button
variant={tab === 'archive' ? 'default' : 'outline'}
size="sm"
onClick={() => setTab('archive')}
disabled={archiveChangelogs.length === 0}
>
Archive
</Button>
{tab === 'archive' && archiveChangelogs.length > 0 && (
<select
className="ml-2 border rounded px-2 py-1 text-sm bg-background"
value={selectedArchive}
onChange={e => setSelectedArchive(e.target.value)}
>
{archiveChangelogs.map(entry => (
<option key={entry.version} value={entry.version}>
{entry.version}
</option>
))}
</select>
)}
</div>
{/* Scrollable content */}
<div className="flex-1 overflow-y-auto px-6 py-4 min-h-0">
<div className="space-y-6">
{tab === 'latest'
? renderChangelog(currentVersionChangelog)
: archiveChangelog && renderChangelog(archiveChangelog)}
</div>
</div>
{/* Footer button */}
<div className="flex justify-center p-6 pt-4 shrink-0">
<Button onClick={handleClose}>
Got it!
</Button>
</div>
</div>
</DialogHeader>
{/* Tabs */}
<>
<div className="flex gap-2 mb-4">
<Button
variant={tab === 'latest' ? 'default' : 'outline'}
size="sm"
onClick={() => setTab('latest')}
>
Latest
</Button>
<Button
variant={tab === 'archive' ? 'default' : 'outline'}
size="sm"
onClick={() => setTab('archive')}
disabled={archiveChangelogs.length === 0}
>
Archive
</Button>
{tab === 'archive' && archiveChangelogs.length > 0 && (
<select
className="ml-2 border rounded px-2 py-1 text-sm"
value={selectedArchive}
onChange={e => setSelectedArchive(e.target.value)}
>
{archiveChangelogs.map(entry => (
<option key={entry.version} value={entry.version}>
{entry.version}
</option>
))}
</select>
)}
</div>
<ScrollArea className="max-h-[60vh] pr-4">
{tab === 'latest'
? renderChangelog(currentVersionChangelog)
: archiveChangelog && renderChangelog(archiveChangelog)}
</ScrollArea>
<div className="flex justify-center pt-4">
<Button onClick={handleClose}>
Got it!
</Button>
</div>
</>
</DialogContent>
</Dialog>
)}
</>
);
}

View File

@@ -20,7 +20,7 @@ import { useNavidrome } from "./NavidromeContext"
import Link from "next/link";
import { useAudioPlayer, Track } from "@/app/components/AudioPlayerContext";
import { getNavidromeAPI } from "@/lib/navidrome";
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { ArtistIcon } from "@/app/components/artist-icon";
@@ -46,8 +46,24 @@ export function AlbumArtwork({
const router = useRouter();
const { addAlbumToQueue, playTrack, addToQueue } = useAudioPlayer();
const { playlists, starItem, unstarItem } = useNavidrome();
const [imageLoading, setImageLoading] = useState(true);
const [imageError, setImageError] = useState(false);
// Memoize cover art URL with dynamic sizing
const coverArtUrl = useMemo(() => {
if (!api || !album.coverArt) return '/default-user.jpg';
// Use width prop or default size for optimization
const imageSize = width || height || 300;
return api.getCoverArtUrl(album.coverArt, imageSize);
}, [api, album.coverArt, width, height]);
// Use callback to prevent function recreation on every render
const handleImageLoad = useCallback(() => {
// Image loaded successfully - no state update needed
}, []);
const handleImageError = useCallback(() => {
// Image failed to load - could set error state if needed
}, []);
const handleClick = () => {
router.push(`/album/${album.id}`);
@@ -80,7 +96,7 @@ export function AlbumArtwork({
artistId: song.artistId,
url: api.getStreamUrl(song.id),
duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt) : undefined,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 1200) : undefined,
starred: !!song.starred
}));
@@ -105,68 +121,42 @@ export function AlbumArtwork({
console.error('Failed to toggle favorite:', error);
}
};
// Get cover art URL with proper fallback
const coverArtUrl = album.coverArt && api
? api.getCoverArtUrl(album.coverArt, 300)
: '/default-user.jpg';
return (
<div className={cn("space-y-3", className)} {...props}>
<ContextMenu>
<ContextMenuTrigger>
<Card key={album.id} className="overflow-hidden cursor-pointer px-0 py-0 gap-0" onClick={() => handleClick()}>
<div className="aspect-square relative group">
{album.coverArt && api ? (
<>
{imageLoading && (
<div className="absolute inset-0 bg-muted animate-pulse rounded flex items-center justify-center">
<Disc className="w-12 h-12 text-muted-foreground animate-spin" />
</div>
)}
<Image
src={api.getCoverArtUrl(album.coverArt)}
alt={album.name}
fill
className={`w-full h-full object-cover transition-opacity duration-300 ${
imageLoading ? 'opacity-0' : 'opacity-100'
}`}
sizes="(max-width: 768px) 100vw, 300px"
onLoad={() => setImageLoading(false)}
onError={() => {
setImageLoading(false);
setImageError(true);
}}
/>
</>
) : (
<div className="w-full h-full bg-muted rounded flex items-center justify-center">
<Disc className="w-12 h-12 text-muted-foreground" />
</div>
)}
{!imageLoading && (
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
<Play className="w-6 h-6 mx-auto hidden group-hover:block" onClick={() => handlePlayAlbum(album)}/>
</div>
)}
</div>
<CardContent className="p-4">
{imageLoading ? (
<>
<div className="h-5 w-3/4 bg-muted animate-pulse rounded mb-2" />
<div className="h-4 w-1/2 bg-muted animate-pulse rounded mb-1" />
<div className="h-3 w-2/3 bg-muted animate-pulse rounded" />
</>
) : (
<>
<h3 className="font-semibold truncate">{album.name}</h3>
<p className="text-sm text-muted-foreground truncate " onClick={() => router.push(album.artistId)}>{album.artist}</p>
<p className="text-xs text-muted-foreground mt-1">
{album.songCount} songs {Math.floor(album.duration / 60)} min
</p>
</>
)}
</CardContent>
</Card>
<div className="aspect-square relative group">
{album.coverArt && api ? (
<Image
src={coverArtUrl}
alt={album.name}
fill
className="w-full h-full object-cover transition-all"
sizes="(max-width: 768px) 100vw, 300px"
onLoad={handleImageLoad}
onError={handleImageError}
priority={false}
loading="lazy"
/>
) : (
<div className="w-full h-full bg-muted rounded flex items-center justify-center">
<Disc className="w-12 h-12 text-muted-foreground" />
</div>
)}
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
<Play className="w-6 h-6 mx-auto hidden group-hover:block" onClick={() => handlePlayAlbum(album)}/>
</div>
</div>
<CardContent className="p-4">
<h3 className="font-semibold truncate">{album.name}</h3>
<p className="text-sm text-muted-foreground truncate " onClick={() => router.push(album.artistId)}>{album.artist}</p>
<p className="text-xs text-muted-foreground mt-1">
{album.songCount} songs {Math.floor(album.duration / 60)} min
</p>
</CardContent>
</Card>
{/* <div onClick={handleClick} className="overflow-hidden rounded-md">
<Image
src={coverArtUrl}

View File

@@ -5,6 +5,7 @@ import { Menu } from "@/app/components/menu";
import { Sidebar } from "@/app/components/sidebar";
import { useNavidrome } from "@/app/components/NavidromeContext";
import { AudioPlayer } from "./AudioPlayer";
import { BottomNavigation } from './BottomNavigation';
import { Toaster } from "@/components/ui/toaster";
import { useFavoriteAlbums } from "@/hooks/use-favorite-albums";
@@ -96,48 +97,74 @@ const Ihateserverside: React.FC<IhateserversideProps> = ({ children }) => {
</div>
);
}
return (
<div className="hidden md:flex md:flex-col md:h-screen md:w-screen md:overflow-hidden">
{/* Top Menu */}
<div
className="sticky z-10 bg-background border-b w-full"
style={{
left: 'env(titlebar-area-x, 0)',
top: 'env(titlebar-area-y, 0)',
}}
>
<Menu
toggleSidebar={toggleSidebarVisibility}
isSidebarVisible={isSidebarVisible}
toggleStatusBar={() => setIsStatusBarVisible(!isStatusBarVisible)}
isStatusBarVisible={isStatusBarVisible}
/>
</div>
<>
{/* Mobile Layout */}
<div className="flex md:hidden flex-col h-screen w-screen overflow-hidden">
{/* Top Menu */}
{/* <div className="shrink-0 bg-background border-b w-full">
<Menu
toggleSidebar={toggleSidebarVisibility}
isSidebarVisible={isSidebarVisible}
toggleStatusBar={() => setIsStatusBarVisible(!isStatusBarVisible)}
isStatusBarVisible={isStatusBarVisible}
/>
</div> */}
{/* Main Content Area */}
<div className="flex-1 flex overflow-hidden w-full">
{isSidebarVisible && (
<div className="w-16 shrink-0 border-r transition-all duration-200">
<Sidebar
playlists={playlists}
className="h-full overflow-y-auto"
visible={isSidebarVisible}
favoriteAlbums={favoriteAlbums}
onRemoveFavoriteAlbum={removeFavoriteAlbum}
/>
</div>
)}
<div className="flex-1 overflow-y-auto min-w-0">
<div>{children}</div>
</div>
{/* Main Content Area with bottom padding for audio player and bottom nav */}
<div className="flex-1 overflow-y-auto pb-40">
<div>{children}</div>
</div>
{/* Floating Audio Player */}
{isStatusBarVisible && (
<AudioPlayer />
)}
<Toaster />
</div>
{/* Bottom Navigation for Mobile */}
<BottomNavigation />
<Toaster />
</div>
{/* Desktop Layout */}
<div className="hidden md:flex md:flex-col md:h-screen md:w-screen md:overflow-hidden">
{/* Top Menu */}
<div
className="sticky z-10 bg-background border-b w-full"
style={{
left: 'env(titlebar-area-x, 0)',
top: 'env(titlebar-area-y, 0)',
}}
>
<Menu
toggleSidebar={toggleSidebarVisibility}
isSidebarVisible={isSidebarVisible}
toggleStatusBar={() => setIsStatusBarVisible(!isStatusBarVisible)}
isStatusBarVisible={isStatusBarVisible}
/>
</div>
{/* Main Content Area */}
<div className="flex-1 flex overflow-hidden w-full">
{isSidebarVisible && (
<div className="w-16 shrink-0 border-r transition-all duration-200">
<Sidebar
playlists={playlists}
className="h-full overflow-y-auto"
visible={isSidebarVisible}
favoriteAlbums={favoriteAlbums}
onRemoveFavoriteAlbum={removeFavoriteAlbum}
/>
</div>
)}
<div className="flex-1 overflow-y-auto min-w-0">
<div>{children}</div>
</div>
</div>
<Toaster />
</div>
{/* Single Shared Audio Player - shows on all layouts */}
<AudioPlayer />
</>
);
};

View File

@@ -1,7 +1,8 @@
import { useCallback } from "react";
import { useRouter } from 'next/navigation';
import Image from "next/image";
import { Github, Mail } from "lucide-react"
import { Github, Mail, Menu as MenuIcon, X } from "lucide-react"
import { UserProfile } from "@/app/components/UserProfile";
import {
Menubar,
MenubarCheckboxItem,
@@ -28,9 +29,35 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
} from "@/components/ui/dialog"
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { useIsMobile } from "@/hooks/use-mobile"
import Link from "next/link"
import {
Search,
Home,
List,
Radio,
Users,
Disc,
Music,
Heart,
Grid3X3,
Clock,
Settings,
Circle
} from "lucide-react";
interface MenuProps {
toggleSidebar: () => void;
@@ -43,9 +70,27 @@ export function Menu({ toggleSidebar, isSidebarVisible, toggleStatusBar, isStatu
const [isFullScreen, setIsFullScreen] = useState(false)
const router = useRouter();
const [open, setOpen] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const { isConnected } = useNavidrome();
const [isClient, setIsClient] = useState(false);
const [navidromeUrl, setNavidromeUrl] = useState<string | null>(null);
const isMobile = useIsMobile();
// Navigation items for mobile menu
const navigationItems = [
{ href: '/', label: 'Home', icon: Home },
{ href: '/search', label: 'Search', icon: Search },
{ href: '/library/albums', label: 'Albums', icon: Disc },
{ href: '/library/artists', label: 'Artists', icon: Users },
{ href: '/library/songs', label: 'Songs', icon: Circle },
{ href: '/library/playlists', label: 'Playlists', icon: Music },
{ href: '/favorites', label: 'Favorites', icon: Heart },
{ href: '/queue', label: 'Queue', icon: List },
{ href: '/radio', label: 'Radio', icon: Radio },
{ href: '/browse', label: 'Browse', icon: Grid3X3 },
{ href: '/history', label: 'History', icon: Clock },
{ href: '/settings', label: 'Settings', icon: Settings },
];
// For this demo, we'll show connection status instead of user auth
const connectionStatus = isConnected ? "Connected to Navidrome" : "Not connected";
@@ -112,28 +157,35 @@ export function Menu({ toggleSidebar, isSidebarVisible, toggleStatusBar, isStatu
return (
<>
<div className="flex items-center justify-between w-full">
<Menubar
className="rounded-none border-b border-none px-2 lg:px-2 flex-1 min-w-0"
style={{
minWidth: 0,
WebkitAppRegion: "drag"
} as React.CSSProperties}
>
<div style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties} className="flex items-center gap-2">
<MenubarMenu>
<MenubarTrigger className="font-bold">mice</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={() => setOpen(true)}>About Music</MenubarItem>
<MenubarSeparator />
<MenubarItem onClick={() => router.push('/settings')}>
Preferences <MenubarShortcut>,</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem onClick={() => isClient && window.close()}>
Quit Music <MenubarShortcut>Q</MenubarShortcut>
</MenubarItem>
</MenubarContent>
</MenubarMenu>
{/* Mobile Top Bar - Simplified since navigation is now at bottom */}
{isMobile ? (
// hey bear!
// nothing
null
) : (
/* Desktop Navigation */
<Menubar
className="rounded-none border-b border-none px-2 lg:px-2 flex-1 min-w-0"
style={{
minWidth: 0,
WebkitAppRegion: "drag"
} as React.CSSProperties}
>
<div style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties} className="flex items-center gap-2">
<MenubarMenu>
<MenubarTrigger className="font-bold">mice</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={() => setOpen(true)}>About Music</MenubarItem>
<MenubarSeparator />
<MenubarItem onClick={() => router.push('/settings')}>
Preferences <MenubarShortcut>,</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem onClick={() => isClient && window.close()}>
Quit Music <MenubarShortcut>Q</MenubarShortcut>
</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger className="relative">File</MenubarTrigger>
<MenubarContent>
@@ -279,6 +331,14 @@ export function Menu({ toggleSidebar, isSidebarVisible, toggleStatusBar, isStatu
</MenubarMenu>
</div>
</Menubar>
)}
{/* User Profile - Desktop only */}
{!isMobile && (
<div className="ml-auto">
<UserProfile variant="desktop" />
</div>
)}
</div>

View File

@@ -109,7 +109,7 @@ export function Sidebar({ className, playlists, visible = true, favoriteAlbums =
>
{album.coverArt && api ? (
<Image
src={api.getCoverArtUrl(album.coverArt, 32)}
src={api.getCoverArtUrl(album.coverArt, 150)}
alt={album.name}
width={16}
height={16}
@@ -165,7 +165,7 @@ export function Sidebar({ className, playlists, visible = true, favoriteAlbums =
>
{album.coverArt && api ? (
<Image
src={api.getCoverArtUrl(album.coverArt, 32)}
src={api.getCoverArtUrl(album.coverArt, 150)}
alt={album.name}
width={16}
height={16}

View File

@@ -17,7 +17,7 @@ import { Badge } from '@/components/ui/badge';
import { useNavidromeConfig } from '@/app/components/NavidromeConfigContext';
import { useTheme } from '@/app/components/ThemeProvider';
import { useToast } from '@/hooks/use-toast';
import { FaServer, FaUser, FaLock, FaCheck, FaTimes, FaPalette, FaLastfm, FaBars } from 'react-icons/fa';
import { FaServer, FaUser, FaLock, FaCheck, FaTimes, FaPalette, FaLastfm } from 'react-icons/fa';
export function LoginForm({
className,
@@ -45,20 +45,7 @@ export function LoginForm({
return true;
});
// New settings
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
if (typeof window !== 'undefined') {
return localStorage.getItem('sidebar-collapsed') === 'true';
}
return false;
});
const [standaloneLastfmEnabled, setStandaloneLastfmEnabled] = useState(() => {
if (typeof window !== 'undefined') {
return localStorage.getItem('standalone-lastfm-enabled') === 'true';
}
return false;
});
// New settings - removed sidebar and standalone lastfm options
// Check if Navidrome is configured via environment variables
const hasEnvConfig = React.useMemo(() => {
@@ -187,8 +174,6 @@ export function LoginForm({
const handleFinishSetup = () => {
// Save all settings
localStorage.setItem('lastfm-scrobbling-enabled', scrobblingEnabled.toString());
localStorage.setItem('sidebar-collapsed', sidebarCollapsed.toString());
localStorage.setItem('standalone-lastfm-enabled', standaloneLastfmEnabled.toString());
// Mark onboarding as complete
localStorage.setItem('onboarding-completed', '1.1.0');
@@ -252,7 +237,7 @@ export function LoginForm({
if (step === 'settings') {
return (
<div className={cn("flex flex-col gap-6", className)} {...props}>
<Card>
<Card className='py-5'>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FaPalette className="w-5 h-5" />
@@ -286,29 +271,6 @@ export function LoginForm({
</Select>
</div>
{/* Sidebar Settings */}
<div className="grid gap-3">
<Label className="flex items-center gap-2">
<FaBars className="w-4 h-4" />
Sidebar Layout
</Label>
<Select
value={sidebarCollapsed ? "collapsed" : "expanded"}
onValueChange={(value) => setSidebarCollapsed(value === "collapsed")}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="expanded">Expanded (with labels)</SelectItem>
<SelectItem value="collapsed">Collapsed (icons only)</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
You can always toggle this later using the button in the sidebar
</p>
</div>
{/* Last.fm Scrobbling */}
<div className="grid gap-3">
<Label className="flex items-center gap-2">
@@ -334,31 +296,6 @@ export function LoginForm({
</p>
</div>
{/* Standalone Last.fm */}
<div className="grid gap-3">
<Label className="flex items-center gap-2">
<FaLastfm className="w-4 h-4" />
Standalone Last.fm (Advanced)
</Label>
<Select
value={standaloneLastfmEnabled ? "enabled" : "disabled"}
onValueChange={(value) => setStandaloneLastfmEnabled(value === "enabled")}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="enabled">Enabled</SelectItem>
<SelectItem value="disabled">Disabled</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
{standaloneLastfmEnabled
? "Direct Last.fm API integration (configure in Settings later)"
: "Use only Navidrome's Last.fm integration"}
</p>
</div>
<div className="flex flex-col gap-3">
<Button onClick={handleFinishSetup} className="w-full">
<FaCheck className="w-4 h-4 mr-2" />
@@ -383,7 +320,7 @@ export function LoginForm({
return (
<div className={cn("flex flex-col gap-6", className)} {...props}>
<Card>
<Card className="py-5">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FaServer className="w-5 h-5" />

View File

@@ -58,7 +58,7 @@ const FavoritesPage = () => {
artistId: song.artistId,
url: api?.getStreamUrl(song.id) || '',
duration: song.duration,
coverArt: song.coverArt ? api?.getCoverArtUrl(song.coverArt) : undefined,
coverArt: song.coverArt ? api?.getCoverArtUrl(song.coverArt, 1200) : undefined,
starred: !!song.starred
});
};
@@ -78,7 +78,7 @@ const FavoritesPage = () => {
artistId: song.artistId,
url: api.getStreamUrl(song.id),
duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt) : undefined,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 1200) : undefined,
starred: !!song.starred
}));
@@ -201,7 +201,7 @@ const FavoritesPage = () => {
<div className="w-12 h-12 relative shrink-0">
{song.coverArt && api ? (
<Image
src={api.getCoverArtUrl(song.coverArt)}
src={api.getCoverArtUrl(song.coverArt, 1200)}
alt={song.album}
fill
className="rounded object-cover"

View File

@@ -88,6 +88,18 @@
body {
font-family: Arial, Helvetica, sans-serif;
}
/* Hide scrollbars on mobile */
@media (max-width: 768px) {
* {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* Internet Explorer 10+ */
}
*::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
}
}
@layer utilities {
@@ -816,34 +828,170 @@
---break---
*/
/*
will delete after the new theme replaces the old one
since the new theme already has the sidebar colors defined
:root {
--sidebar: hsl(0 0% 98%);
--sidebar-foreground: hsl(240 5.3% 26.1%);
--sidebar-primary: hsl(240 5.9% 10%);
--sidebar-primary-foreground: hsl(0 0% 98%);
--sidebar-accent: hsl(240 4.8% 95.9%);
--sidebar-accent-foreground: hsl(240 5.9% 10%);
--sidebar-border: hsl(220 13% 91%);
--sidebar-ring: hsl(217.2 91.2% 59.8%);
/* Mobile-specific optimizations */
@media (max-width: 767px) {
/* Improve touch targets for mobile */
button {
min-height: 44px;
min-width: 44px;
}
/* Better touch feedback */
button:active {
transform: scale(0.95);
transition: transform 0.1s ease;
}
/* Ensure proper viewport behavior */
html {
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
}
/* Smooth scrolling for mobile */
.overflow-y-auto {
-webkit-overflow-scrolling: touch;
}
/* Mobile audio player specific */
.mobile-audio-player {
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
}
/* Prevent horizontal scroll */
body {
overflow-x: hidden;
}
}
.dark {
--sidebar: hsl(240 5.9% 10%);
--sidebar-foreground: hsl(240 4.8% 95.9%);
--sidebar-primary: hsl(224.3 76.3% 48%);
--sidebar-primary-foreground: hsl(0 0% 100%);
--sidebar-accent: hsl(240 3.7% 15.9%);
--sidebar-accent-foreground: hsl(240 4.8% 95.9%);
--sidebar-border: hsl(240 3.7% 15.9%);
--sidebar-ring: hsl(217.2 91.2% 59.8%);
} */
/* Safe area support for mobile devices */
.pb-safe {
padding-bottom: env(safe-area-inset-bottom, 0.5rem);
}
.mobile-safe-bottom {
margin-bottom: env(safe-area-inset-bottom, 0);
}
/* Touch-optimized navigation */
.touch-manipulation {
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
/* Bottom navigation z-index fix */
.bottom-nav {
z-index: 45;
}
/* Audio player above bottom nav */
.mobile-audio-above-nav {
z-index: 50;
bottom: calc(4rem + env(safe-area-inset-bottom, 0));
}
/* Mobile Audio Player Styles */
.mobile-audio-player {
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.mobile-audio-player button {
-webkit-tap-highlight-color: transparent;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
/* Prevent iOS zoom on input focus */
@media screen and (max-width: 767px) {
input[type="range"] {
font-size: 16px;
}
/* Improve button touch targets */
.mobile-audio-player button {
min-height: 44px;
min-width: 44px;
}
}
/* Better focus states for accessibility */
button:focus-visible {
outline: 2px solid hsl(var(--primary));
outline-offset: 2px;
}
/* Improved animations */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in-up {
animation: fadeInUp 0.3s ease-out;
}
/* Safe area insets for mobile devices */
@supports (padding: max(0px)) {
.mobile-safe-bottom {
padding-bottom: max(1rem, env(safe-area-inset-bottom));
}
.mobile-safe-top {
padding-top: max(0.5rem, env(safe-area-inset-top));
}
}
/* Progress bar improvements for mobile */
@media (max-width: 767px) {
.progress-mobile {
height: 3px;
cursor: pointer;
-webkit-appearance: none;
touch-action: manipulation;
}
.progress-mobile::-webkit-slider-thumb {
-webkit-appearance: none;
height: 16px;
width: 16px;
border-radius: 50%;
background: hsl(var(--primary));
cursor: pointer;
margin-top: -6px;
}
}
/*
---break---
*/
*/
/* Mobile Bottom Navigation Styles */
.pb-safe {
padding-bottom: env(safe-area-inset-bottom, 0.5rem);
}
.mobile-safe-bottom {
margin-bottom: env(safe-area-inset-bottom, 0);
}
.touch-manipulation {
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
.mobile-audio-above-nav {
bottom: calc(4rem + env(safe-area-inset-bottom, 0));
}

View File

@@ -26,6 +26,35 @@ export const metadata = {
'max-snippet': -1,
},
},
viewport: {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
userScalable: false,
},
appleWebApp: {
capable: true,
statusBarStyle: 'black-translucent',
title: isDev && shortCommit ? `mice (dev: ${shortCommit})` : 'mice',
},
formatDetection: {
telephone: false,
},
other: {
'apple-mobile-web-app-capable': 'yes',
'apple-mobile-web-app-status-bar-style': 'black-translucent',
'format-detection': 'telephone=no',
},
icons: {
icon: [
{ url: '/favicon.ico', sizes: '48x48' },
{ url: '/icon-192.png', sizes: '192x192', type: 'image/png' },
{ url: '/icon-512.png', sizes: '512x512', type: 'image/png' },
],
apple: [
{ url: '/apple-touch-icon-precomposed.png', sizes: '180x180', type: 'image/png' },
],
},
};
const geistSans = localFont({

244
app/library/page.tsx Normal file
View File

@@ -0,0 +1,244 @@
'use client';
import React, { useEffect, useState } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { Music, Users, Disc, ListMusic, Heart, Play } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { getNavidromeAPI } from '@/lib/navidrome';
import NavidromeAPI from '@/lib/navidrome';
import { useAudioPlayer } from '@/app/components/AudioPlayerContext';
import { useIsMobile } from '@/hooks/use-mobile';
interface Album {
id: string;
name: string;
artist: string;
artistId?: string;
coverArt?: string;
year?: number;
songCount: number;
}
interface LibraryStats {
albums: number;
artists: number;
songs: number;
playlists: number;
}
export default function LibraryPage() {
const [recentAlbums, setRecentAlbums] = useState<Album[]>([]);
const [stats, setStats] = useState<LibraryStats>({ albums: 0, artists: 0, songs: 0, playlists: 0 });
const [loading, setLoading] = useState(true);
const [api, setApi] = useState<NavidromeAPI | null>(null);
const { playAlbum } = useAudioPlayer();
const isMobile = useIsMobile();
useEffect(() => {
const loadLibraryData = async () => {
try {
const navidromeApi = getNavidromeAPI();
if (!navidromeApi) {
console.error('Navidrome API not available');
return;
}
setApi(navidromeApi);
// Load recent albums
const albumsData = await navidromeApi.getAlbums('newest', 4, 0);
setRecentAlbums(albumsData || []);
// Load library stats
const [allAlbums, allArtists, allPlaylists] = await Promise.all([
navidromeApi.getAlbums('alphabeticalByName', 1, 0), // Just to get count
navidromeApi.getArtists(),
navidromeApi.getPlaylists()
]);
setStats({
albums: allAlbums?.length || 0,
artists: allArtists?.length || 0,
songs: 0, // We don't have a direct method for this
playlists: allPlaylists?.length || 0
});
} catch (error) {
console.error('Failed to load library data:', error);
} finally {
setLoading(false);
}
};
loadLibraryData();
}, []);
const handlePlayAlbum = async (album: Album) => {
try {
await playAlbum(album.id);
} catch (error) {
console.error('Failed to play album:', error);
}
};
const libraryLinks = [
{
href: '/library/albums',
label: 'Albums',
icon: Disc,
description: 'Browse all albums',
count: stats.albums
},
{
href: '/library/artists',
label: 'Artists',
icon: Users,
description: 'Discover artists',
count: stats.artists
},
{
href: '/library/songs',
label: 'Songs',
icon: Music,
description: 'All your music',
count: stats.songs
},
{
href: '/library/playlists',
label: 'Playlists',
icon: ListMusic,
description: 'Your playlists',
count: stats.playlists
},
{
href: '/favorites',
label: 'Favorites',
icon: Heart,
description: 'Starred music',
count: 0
}
];
if (loading) {
return (
<div className="p-4 pb-20 space-y-6">
<div className="space-y-4">
<h1 className="text-2xl font-bold">Your Library</h1>
{/* Loading skeleton for library links */}
<div>
<h2 className="text-lg font-semibold mb-3">Browse</h2>
<div className="space-y-3">
{[...Array(5)].map((_, i) => (
<div key={i} className="animate-pulse">
<div className="bg-muted rounded-lg h-16"></div>
</div>
))}
</div>
</div>
{/* Loading skeleton for recent albums */}
<div>
<h2 className="text-lg font-semibold mb-3">Recently Added</h2>
<div className="grid grid-cols-2 gap-4">
{[...Array(4)].map((_, i) => (
<div key={i} className="animate-pulse">
<div className="bg-muted rounded-lg aspect-square mb-2"></div>
<div className="bg-muted h-4 rounded mb-1"></div>
<div className="bg-muted h-3 rounded w-3/4"></div>
</div>
))}
</div>
</div>
</div>
</div>
);
}
return (
<div className="p-4 pb-20 space-y-6">
<div className="space-y-4">
<h1 className="text-2xl font-bold">Your Library</h1>
{/* Library Navigation - Always at top */}
<div>
{/* <h2 className="text-lg font-semibold mb-3">Browse</h2> */}
<div className="flex flex-col gap-2">
{libraryLinks.map((link) => {
const Icon = link.icon;
return (
<Link key={link.href} href={link.href}>
<Card className="hover:bg-muted/50 transition-colors cursor-pointer">
<CardContent className="p-2">
<div className="flex items-center space-x-4">
<div className="p-2 bg-primary/10 rounded-lg">
<Icon className="w-6 h-6 text-primary" />
</div>
<div className="flex-1">
<h3 className="font-medium">{link.label}</h3>
<p className="text-sm text-muted-foreground">{link.description}</p>
</div>
{link.count > 0 && (
<div className="text-sm text-muted-foreground">
{link.count}
</div>
)}
</div>
</CardContent>
</Card>
</Link>
);
})}
</div>
</div>
{/* Recently Added Albums - At bottom on mobile, after Browse on desktop */}
<div>
<h2 className="text-lg font-semibold mb-3">Recently Added</h2>
<div className="grid grid-cols-2 gap-4">
{recentAlbums.map((album) => (
<Card key={album.id} className="group cursor-pointer hover:bg-muted/50 transition-colors">
<CardContent className="p-3">
<Link href={`/album/${album.id}`}>
<div className="relative aspect-square mb-2">
<Image
src={album.coverArt && api ? api.getCoverArtUrl(album.coverArt, 300) : '/default-user.jpg'}
alt={album.name}
width={600}
height={600}
className="object-cover rounded-lg"
sizes="(max-width: 768px) 50vw, 200px"
/>
{!isMobile && (
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handlePlayAlbum(album);
}}
className="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg flex items-center justify-center"
>
<Play className="w-8 h-8 text-white fill-white" />
</button>
)}
</div>
<h3 className="font-medium text-sm truncate hover:underline">{album.name}</h3>
<Link
href={`/artist/${album.artistId || album.artist}`}
className="text-xs text-muted-foreground truncate hover:underline block"
onClick={(e) => e.stopPropagation()}
>
{album.artist}
</Link>
{/* {album.year && (
<p className="text-xs text-muted-foreground">{album.year}</p>
)} */}
</Link>
</CardContent>
</Card>
))}
</div>
</div>
</div>
</div>
);
}

View File

@@ -53,7 +53,7 @@ const PlaylistsPage: React.FC = () => {
<ScrollArea>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 pb-4"> {playlists.map((playlist) => {
const playlistCoverUrl = playlist.coverArt && api
? api.getCoverArtUrl(playlist.coverArt, 200)
? api.getCoverArtUrl(playlist.coverArt, 600)
: '/default-user.jpg';
return (

View File

@@ -101,7 +101,7 @@ export default function SongsPage() {
setFilteredSongs(filtered);
}, [songs, searchQuery, sortBy, sortDirection]);
const handlePlaySong = (song: Song) => {
const handlePlayClick = (song: Song) => {
if (!api) {
console.error('Navidrome API not available');
return;
@@ -114,7 +114,7 @@ export default function SongsPage() {
artist: song.artist,
album: song.album,
duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 64) : undefined,
albumId: song.albumId,
artistId: song.artistId,
starred: !!song.starred
@@ -135,7 +135,7 @@ export default function SongsPage() {
artist: song.artist,
album: song.album,
duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 64) : undefined,
albumId: song.albumId,
artistId: song.artistId,
starred: !!song.starred
@@ -222,7 +222,7 @@ export default function SongsPage() {
className={`group flex items-center p-3 rounded-lg hover:bg-accent/50 cursor-pointer transition-colors ${
isCurrentlyPlaying(song) ? 'bg-accent/50 border-l-4 border-primary' : ''
}`}
onClick={() => handlePlaySong(song)}
onClick={() => handlePlayClick(song)}
>
{/* Track Number / Play Indicator */}
<div className="w-8 text-center text-sm text-muted-foreground mr-3">
@@ -240,7 +240,7 @@ export default function SongsPage() {
{/* Album Art */}
<div className="w-12 h-12 mr-4 shrink-0"> <Image
src={song.coverArt && api ? api.getCoverArtUrl(song.coverArt, 100) : '/default-user.jpg'}
src={song.coverArt && api ? api.getCoverArtUrl(song.coverArt, 48) : '/default-user.jpg'}
alt={song.album}
width={48}
height={48}

View File

@@ -38,6 +38,25 @@ export default function manifest(): MetadataRoute.Manifest {
type: 'image/png',
sizes: '512x512',
purpose: 'maskable'
},
// Apple Touch Icons for iOS
{
src: '/apple-touch-icon.png',
type: 'image/png',
sizes: '180x180',
purpose: 'any'
},
{
src: '/icon-192.png',
type: 'image/png',
sizes: '152x152',
purpose: 'any'
},
{
src: '/icon-192.png',
type: 'image/png',
sizes: '120x120',
purpose: 'any'
}
],
screenshots: [

View File

@@ -12,6 +12,8 @@ import { useSearchParams } from 'next/navigation';
import { useAudioPlayer } from './components/AudioPlayerContext';
import { SongRecommendations } from './components/SongRecommendations';
import { Skeleton } from '@/components/ui/skeleton';
import { useIsMobile } from '@/hooks/use-mobile';
import { UserProfile } from './components/UserProfile';
type TimeOfDay = 'morning' | 'afternoon' | 'evening';
@@ -24,6 +26,7 @@ function MusicPageContent() {
const [favoriteAlbums, setFavoriteAlbums] = useState<Album[]>([]);
const [favoritesLoading, setFavoritesLoading] = useState(true);
const [shortcutProcessed, setShortcutProcessed] = useState(false);
const isMobile = useIsMobile();
useEffect(() => {
if (albums.length > 0) {

View File

@@ -57,7 +57,7 @@ export default function PlaylistPage() {
artist: song.artist,
album: song.album,
duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 64) : undefined,
albumId: song.albumId,
artistId: song.artistId,
starred: !!song.starred
@@ -77,7 +77,7 @@ export default function PlaylistPage() {
artist: song.artist,
album: song.album,
duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 64) : undefined,
albumId: song.albumId,
artistId: song.artistId,
starred: !!song.starred
@@ -98,7 +98,7 @@ export default function PlaylistPage() {
artist: song.artist,
album: song.album,
duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 64) : undefined,
albumId: song.albumId,
artistId: song.artistId,
starred: !!song.starred
@@ -209,7 +209,7 @@ export default function PlaylistPage() {
{/* Album Art */}
<div className="w-12 h-12 mr-4 shrink-0"> <Image
src={song.coverArt && api ? api.getCoverArtUrl(song.coverArt, 100) : '/default-user.jpg'}
src={song.coverArt && api ? api.getCoverArtUrl(song.coverArt, 48) : '/default-user.jpg'}
alt={song.album}
width={48}
height={48}

View File

@@ -353,7 +353,7 @@ const SettingsPage = () => {
style={{ columnFill: 'balance' }}>
{!hasEnvConfig && (
<Card className="mb-6 break-inside-avoid">
<Card className="mb-6 break-inside-avoid py-5">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FaServer className="w-5 h-5" />
@@ -442,7 +442,7 @@ const SettingsPage = () => {
)}
{hasEnvConfig && (
<Card className="mb-6 break-inside-avoid">
<Card className="mb-6 break-inside-avoid py-5">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FaServer className="w-5 h-5" />
@@ -469,7 +469,7 @@ const SettingsPage = () => {
</Card>
)}
<Card className="mb-6 break-inside-avoid">
<Card className="mb-6 break-inside-avoid py-5">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FaLastfm className="w-5 h-5" />
@@ -547,7 +547,7 @@ const SettingsPage = () => {
</CardContent>
</Card> */}
<Card className="mb-6 break-inside-avoid">
<Card className="mb-6 break-inside-avoid py-5">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Settings className="w-5 h-5" />
@@ -602,7 +602,7 @@ const SettingsPage = () => {
</CardContent>
</Card>
<Card className="mb-6 break-inside-avoid">
{/* <Card className="mb-6 break-inside-avoid py-5">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FaLastfm className="w-5 h-5" />
@@ -695,7 +695,7 @@ const SettingsPage = () => {
</>
)}
</CardContent>
</Card>
</Card> */}
{/* Sidebar Customization */}
<div className="break-inside-avoid mb-6">
@@ -712,7 +712,7 @@ const SettingsPage = () => {
<CacheManagement />
</div>
<Card className="mb-6 break-inside-avoid">
<Card className="mb-6 break-inside-avoid py-5">
<CardHeader>
<CardTitle>Appearance</CardTitle>
<CardDescription>
@@ -761,7 +761,7 @@ const SettingsPage = () => {
</Card>
{/* Theme Preview */}
<Card className="mb-6 break-inside-avoid">
<Card className="mb-6 break-inside-avoid py-5">
<CardHeader>
<CardTitle>Preview</CardTitle>
<CardDescription>
@@ -789,6 +789,47 @@ const SettingsPage = () => {
</div>
</CardContent>
</Card>
{/* Debug Section - Development Only */}
{process.env.NODE_ENV === 'development' && (
<Card className="mb-6 break-inside-avoid py-5 border-orange-200 bg-orange-50/50">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-orange-700">
<Settings className="w-5 h-5" />
Debug Tools
</CardTitle>
<CardDescription className="text-orange-600">
Development-only debugging utilities
</CardDescription>
</CardHeader>
<CardContent>
<Button
onClick={() => {
// Save Navidrome config before clearing
const navidromeConfig = localStorage.getItem('navidrome-config');
// Clear all localStorage
localStorage.clear();
// Restore Navidrome config
if (navidromeConfig) {
localStorage.setItem('navidrome-config', navidromeConfig);
}
// Reload page to reset state
window.location.reload();
}}
variant="outline"
className="w-full bg-orange-100 border-orange-300 text-orange-700 hover:bg-orange-200"
>
Clear All Data (Keep Navidrome Config)
</Button>
<p className="text-xs text-orange-600 mt-2">
This will clear all localStorage data except your Navidrome server configuration, then reload the page.
</p>
</CardContent>
</Card>
)}
</div>
</div>
)}

View File

@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-0 shadow-sm",
className
)}
{...props}

View File

@@ -3,18 +3,18 @@ version: '3.8'
services:
mice:
container_name: mice-public
image: sillyangel/mice:latest
image: sillyangel/mice:dev-latest
ports:
- "40625:40625"
environment:
# Navidrome Server Configuration
- NAVIDROME_URL=https://navi.sillyangel.dev
- NAVIDROME_USERNAME=kryptonite
- NAVIDROME_PASSWORD=kryptonite
# - NAVIDROME_URL=http://navidrome:4533
# - NAVIDROME_USERNAME=user
# - NAVIDROME_PASSWORD=password
# PostHog Analytics
- POSTHOG_KEY=phc_Sa39J7754MwaHrPxYiWnWETVSD3g1cU4nOplMGczRE9
- POSTHOG_HOST=https://us.i.posthog.com
# # PostHog Analytics
# - POSTHOG_KEY=phc_Sa39J7754MwaHrPxYiWnWETVSD3g1cU4nOplMGczRE9
# - POSTHOG_HOST=https://us.i.posthog.com
# Application Port
- PORT=40625
@@ -27,3 +27,24 @@ services:
start_period: 40s
restart: unless-stopped
navidrome:
container_name: navidrome
image: deluan/navidrome:latest
ports:
- "4533:4533"
environment:
- ND_SCANINTERVAL=1m
- ND_LOGLEVEL=info
- ND_SESSIONTIMEOUT=24h
- ND_PORT=4533
# - ND_BASEURL=/navidrome
# - ND_MUSICFOLDER=/music
volumes:
- navidrome_data:/data
- navidrome_music:/music
restart: unless-stopped
volumes:
navidrome_data:
navidrome_music:

View File

@@ -2,9 +2,10 @@ version: '3.8'
services:
mice:
image: sillyangel/mice:latest
container_name: mice-public
image: sillyangel/mice:dev-latest
ports:
- "${HOST_PORT:-3000}:${PORT:-3000}"
- "40625:40625"
environment:
# Navidrome Server Configuration
# These will be injected at runtime using the entrypoint script
@@ -13,18 +14,35 @@ services:
- NEXT_PUBLIC_NAVIDROME_PASSWORD=${NAVIDROME_PASSWORD:-}
# PostHog Analytics (optional)
- NEXT_PUBLIC_POSTHOG_KEY=${POSTHOG_KEY:-}
- NEXT_PUBLIC_POSTHOG_HOST=${POSTHOG_HOST:-}
# - NEXT_PUBLIC_POSTHOG_KEY=phc_Sa39J7754MwaHrPxYiWnWETVSD3g1cU4nOplMGczRE9
# - NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
# Application Port
- PORT=${PORT:-3000}
- PORT=40625
# Optional: Add a health check
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:${PORT:-3000}"]
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:40625"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
restart: unless-stopped
navidrome:
container_name: navidrome
image: deluan/navidrome:latest
ports:
- "4533:4533"
environment:
- ND_SCANINTERVAL=1m
- ND_LOGLEVEL=info
- ND_SESSIONTIMEOUT=24h
- ND_PORT=4533
# - ND_BASEURL=/navidrome
# - ND_MUSICFOLDER=/music
volumes:
- navidrome_data:/data
- navidrome_music:/music
restart: unless-stopped

View File

@@ -15,6 +15,3 @@ printenv | grep NEXT_PUBLIC_ | while read -r line ; do
done
echo "✅ Environment variable replacement complete"
# Execute the container's main process (CMD in Dockerfile)
exec "$@"

View File

@@ -0,0 +1,96 @@
import { useState, useEffect, useRef } from 'react';
interface UseResponsiveImageSizeOptions {
/** Minimum size threshold */
minSize?: number;
/** Maximum size threshold */
maxSize?: number;
/** Multiplier for high DPI displays */
dpiMultiplier?: number;
/** Available size tiers from Navidrome */
availableSizes?: number[];
}
/**
* Hook to calculate optimal image size based on container dimensions
*/
export function useResponsiveImageSize(options: UseResponsiveImageSizeOptions = {}) {
const {
minSize = 60,
maxSize = 1200,
dpiMultiplier = typeof window !== 'undefined' ? (window.devicePixelRatio || 1) : 1,
availableSizes = [60, 120, 240, 400, 600, 1200] // Clean divisions of 1200
} = options;
const containerRef = useRef<HTMLElement>(null);
const [imageSize, setImageSize] = useState<number>(300); // Default fallback
useEffect(() => {
const calculateOptimalSize = () => {
if (!containerRef.current) return;
const element = containerRef.current;
const rect = element.getBoundingClientRect();
// Use the larger dimension (width or height) as base
const displaySize = Math.max(rect.width, rect.height);
// Account for device pixel ratio for crisp images on high DPI displays
const targetSize = Math.round(displaySize * dpiMultiplier);
// Clamp to min/max bounds
const clampedSize = Math.max(minSize, Math.min(maxSize, targetSize));
// Find the next larger available size to ensure quality
const optimalSize = availableSizes.find(size => size >= clampedSize) || availableSizes[availableSizes.length - 1];
setImageSize(optimalSize);
};
// Calculate initial size
calculateOptimalSize();
// Recalculate on resize
const resizeObserver = new ResizeObserver(calculateOptimalSize);
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
}
return () => {
resizeObserver.disconnect();
};
}, [minSize, maxSize, dpiMultiplier, availableSizes]);
return {
containerRef,
imageSize,
/** Get size for a specific display dimension */
getSizeForDimension: (dimension: number) => {
const targetSize = Math.round(dimension * dpiMultiplier);
const clampedSize = Math.max(minSize, Math.min(maxSize, targetSize));
return availableSizes.find(size => size >= clampedSize) || availableSizes[availableSizes.length - 1];
}
};
}
/**
* Simple function to get optimal image size for known dimensions
*/
export function getOptimalImageSize(
displayWidth: number,
displayHeight: number,
options: Omit<UseResponsiveImageSizeOptions, 'availableSizes'> & { availableSizes?: number[] } = {}
): number {
const {
minSize = 60,
maxSize = 1200,
dpiMultiplier = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1,
availableSizes = [60, 120, 240, 400, 600, 1200] // Clean divisions of 1200
} = options;
const displaySize = Math.max(displayWidth, displayHeight);
const targetSize = Math.round(displaySize * dpiMultiplier);
const clampedSize = Math.max(minSize, Math.min(maxSize, targetSize));
return availableSizes.find(size => size >= clampedSize) || availableSizes[availableSizes.length - 1];
}

38
lib/gravatar.ts Normal file
View File

@@ -0,0 +1,38 @@
import crypto from 'crypto';
/**
* Generate a Gravatar URL from an email address
* @param email - The email address
* @param size - The size of the image (default: 80)
* @param defaultImage - Default image type if no Gravatar found (default: 'identicon')
* @returns The Gravatar URL
*/
export function getGravatarUrl(
email: string,
size: number = 80,
defaultImage: string = 'identicon'
): string {
// Normalize email: trim whitespace and convert to lowercase
const normalizedEmail = email.trim().toLowerCase();
// Generate MD5 hash of the email
const hash = crypto.createHash('md5').update(normalizedEmail).digest('hex');
// Construct the Gravatar URL
return `https://www.gravatar.com/avatar/${hash}?s=${size}&d=${defaultImage}`;
}
/**
* Generate a Gravatar URL with retina support (2x size)
* @param email - The email address
* @param size - The base size of the image
* @param defaultImage - Default image type if no Gravatar found
* @returns The Gravatar URL at 2x resolution
*/
export function getGravatarUrlRetina(
email: string,
size: number = 80,
defaultImage: string = 'identicon'
): string {
return getGravatarUrl(email, size * 2, defaultImage);
}

125
lib/image-utils.ts Normal file
View File

@@ -0,0 +1,125 @@
/**
* Utility functions for calculating optimal image sizes for different contexts
*/
export interface ImageSizeContext {
/** The display width in CSS pixels */
displayWidth: number;
/** The display height in CSS pixels */
displayHeight: number;
/** Device pixel ratio for high-DPI displays */
devicePixelRatio?: number;
/** Additional scaling factor (e.g., for hover effects) */
scaleFactor?: number;
}
/**
* Calculate the optimal image size for the given context
* Takes into account device pixel ratio and potential scaling effects
*/
export function calculateOptimalImageSize(context: ImageSizeContext): number {
const { displayWidth, displayHeight, devicePixelRatio = 1, scaleFactor = 1.1 } = context;
// Use the larger dimension to ensure we cover the entire display area
const baseDimension = Math.max(displayWidth, displayHeight);
// Account for device pixel ratio and potential scaling
const optimalSize = Math.ceil(baseDimension * devicePixelRatio * scaleFactor);
// Cap at reasonable maximum to avoid excessive bandwidth usage
return Math.min(optimalSize, 1200);
}
/**
* Get optimal image size for common component contexts
* All sizes are clean divisions of 1200 for optimal scaling
*/
export const ImageSizes = {
// Small thumbnails in lists - 1200/20 = 60, rounded to 64 for better display
THUMBNAIL: 60,
// Small album covers in compact views - 1200/10 = 120
SMALL_ALBUM: 120,
// Medium album covers in grid views - 1200/5 = 240
MEDIUM_ALBUM: 240,
// Large album covers in detail views - 1200/3 = 400
LARGE_ALBUM: 400,
// Extra large for full-screen displays - 1200/2 = 600
XLARGE_ALBUM: 600,
// Full resolution - 1200/1 = 1200
FULL_ALBUM: 1200,
// Artist images
ARTIST_SMALL: 120, // 1200/10
ARTIST_MEDIUM: 240, // 1200/5
ARTIST_LARGE: 400, // 1200/3
// Player images
PLAYER_MINI: 60, // 1200/20
PLAYER_COMPACT: 120, // 1200/10
PLAYER_FULL: 400, // 1200/3
} as const;
/**
* Get responsive image size based on container and viewport
*/
export function getResponsiveImageSize(
containerWidth: number,
viewportWidth: number = typeof window !== 'undefined' ? window?.innerWidth || 1920 : 1920,
devicePixelRatio: number = typeof window !== 'undefined' ? window?.devicePixelRatio || 1 : 1
): number {
let targetSize: number;
// Determine base size based on container and viewport
// All sizes are clean divisions of 1200
if (containerWidth <= 60) {
targetSize = ImageSizes.THUMBNAIL; // 60px
} else if (containerWidth <= 120) {
targetSize = ImageSizes.SMALL_ALBUM; // 120px
} else if (containerWidth <= 240 || viewportWidth <= 768) {
targetSize = ImageSizes.MEDIUM_ALBUM; // 240px
} else if (containerWidth <= 400 || viewportWidth <= 1024) {
targetSize = ImageSizes.LARGE_ALBUM; // 400px
} else if (containerWidth <= 600 || viewportWidth <= 1440) {
targetSize = ImageSizes.XLARGE_ALBUM; // 600px
} else {
targetSize = ImageSizes.FULL_ALBUM; // 1200px
}
// Apply device pixel ratio but ensure we stay within clean divisions of 1200
const scaledSize = Math.ceil(targetSize * devicePixelRatio);
// Round to nearest clean division of 1200
const divisions = [60, 120, 240, 400, 600, 1200];
return divisions.find(size => size >= scaledSize) || 1200;
}
/**
* Hook to get optimal image size for a container
* Returns clean divisions of 1200 for optimal scaling
*/
export function useOptimalImageSize(
width: number,
height: number = width,
scaleFactor: number = 1.1
): number {
if (typeof window === 'undefined') {
// SSR fallback - return appropriate size based on dimensions
return getResponsiveImageSize(width, 1920, 1);
}
const optimalSize = calculateOptimalImageSize({
displayWidth: width,
displayHeight: height,
devicePixelRatio: window.devicePixelRatio || 1,
scaleFactor,
});
// Round to nearest clean division of 1200
const divisions = [60, 120, 240, 400, 600, 1200];
return divisions.find(size => size >= optimalSize) || 1200;
}

View File

@@ -110,6 +110,26 @@ export interface ArtistInfo {
similarArtist?: Artist[];
}
export interface User {
username: string;
email?: string;
scrobblingEnabled: boolean;
maxBitRate?: number;
adminRole: boolean;
settingsRole: boolean;
downloadRole: boolean;
uploadRole: boolean;
playlistRole: boolean;
coverArtRole: boolean;
commentRole: boolean;
podcastRole: boolean;
streamRole: boolean;
jukeboxRole: boolean;
shareRole: boolean;
videoConversionRole: boolean;
avatarLastChanged?: string;
}
class NavidromeAPI {
private config: NavidromeConfig;
private clientName = 'miceclient';
@@ -171,6 +191,12 @@ class NavidromeAPI {
}
}
async getUserInfo(): Promise<User> {
const response = await this.makeRequest('getUser', { username: this.config.username });
const userData = response.user as User;
return userData;
}
async getArtists(): Promise<Artist[]> {
const response = await this.makeRequest('getArtists');
const artists: Artist[] = [];

View File

@@ -1,6 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
qualities: [50, 75, 100],
remotePatterns: [
{
protocol: "https",

View File

@@ -13,14 +13,14 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@hookform/resolvers": "^3.9.1",
"@hookform/resolvers": "^5.2.0",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-aspect-ratio": "^1.1.7",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-context-menu": "^2.2.2",
"@radix-ui/react-context-menu": "^2.2.15",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-hover-card": "^1.1.14",
@@ -29,11 +29,11 @@
"@radix-ui/react-menubar": "^1.1.15",
"@radix-ui/react-navigation-menu": "^1.2.13",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-progress": "^1.1.1",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-scroll-area": "^1.2.1",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.5",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.2.5",
@@ -43,7 +43,7 @@
"@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.7",
"@types/react-beautiful-dnd": "^13.1.8",
"axios": "^1.8.2",
"axios": "^1.11.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@@ -52,14 +52,14 @@
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.525.0",
"next": "15.3.4",
"next": "15.4.4",
"next-themes": "^0.4.6",
"posthog-js": "^1.255.0",
"posthog-node": "^5.1.1",
"react": "19.1.0",
"react-day-picker": "^9.7.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.53.2",
"react-hook-form": "^7.60.0",
"react-icons": "^5.3.0",
"react-resizable-panels": "^3.0.3",
"recharts": "^3.0.2",
@@ -67,23 +67,34 @@
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2",
"zod": "^3.25.70"
"zod": "^4.0.10"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.11",
"@types/node": "^24.0.10",
"@types/node": "^24.1.0",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"chalk": "^5.3.0",
"eslint": "^9.30",
"eslint-config-next": "15.3.5",
"eslint": "^9.31",
"eslint": "^9.32",
"eslint-config-next": "15.4.4",
"postcss": "^8",
"tailwindcss": "^4.1.11",
"typescript": "^5"
},
"packageManager": "pnpm@10.12.4",
"packageManager": "pnpm@10.13.1",
"overrides": {
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6"
},
"pnpm": {
"overrides": {
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6"
},
"onlyBuiltDependencies": [
"sharp",
"unrs-resolver"
]
}
}

1358
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

2
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,2 @@
onlyBuiltDependencies:
- unrs-resolver

Binary file not shown.

Before

Width:  |  Height:  |  Size: 869 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 MiB

After

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 397 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 MiB

After

Width:  |  Height:  |  Size: 339 KiB