Compare commits
318 Commits
8.0
...
main-workf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bcb7f735d | ||
|
|
62689e5d10 | ||
|
|
124d1be1a4 | ||
|
|
8265459ff8 | ||
|
|
a44de6f15b | ||
|
|
0e6eb75f8d | ||
|
|
e5d8bfec0e | ||
|
|
bc9cf3c11e | ||
|
|
896816c185 | ||
|
|
6d209e10fb | ||
|
|
a763ead3ec | ||
|
|
9688f0ba96 | ||
|
|
cb30790fae | ||
|
|
5be6955b13 | ||
|
|
17560819a1 | ||
|
|
65d26da867 | ||
|
|
4e08570599 | ||
|
|
57a6060f93 | ||
|
|
012d7bf5a8 | ||
|
|
6f1b81d503 | ||
|
|
07a6e2bbf4 | ||
|
|
503eef29c8 | ||
|
|
745e4e7106 | ||
|
|
9824b8e99a | ||
|
|
6cf6ad1fe3 | ||
|
|
c0efd7f01d | ||
|
|
98b8f6e00e | ||
|
|
276bb78c1e | ||
|
|
000fc49b01 | ||
|
|
724cdb48a0 | ||
|
|
ba512a25d1 | ||
|
|
76c9aa4df9 | ||
|
|
d84b09801c | ||
|
|
6ca19fc48d | ||
|
|
30aa53fdd7 | ||
|
|
37578d7638 | ||
|
|
3e5dddf617 | ||
|
|
c88b3cc03c | ||
|
|
b8647538fd | ||
|
|
d0dbd7e75c | ||
|
|
819057ed8b | ||
|
|
8cbb5a5c92 | ||
|
|
ae42aa1241 | ||
|
|
19c2c29df0 | ||
|
|
944e899870 | ||
|
|
c49bc39770 | ||
|
|
80ee7885e1 | ||
|
|
8beda77a76 | ||
|
|
f58ad986da | ||
|
|
66fe285fb1 | ||
|
|
64f69bbbbd | ||
|
|
17bbdeb7b9 | ||
|
|
b2087f4733 | ||
|
|
8a83671c88 | ||
|
|
d0e91f7099 | ||
|
|
e218ad512e | ||
|
|
c8bd531bb5 | ||
|
|
2ded256bda | ||
|
|
7835ef1dec | ||
|
|
92bddd0538 | ||
|
|
e43615bcdf | ||
|
|
7b48b3a4d8 | ||
|
|
ce4ae1039f | ||
|
|
039ef863bb | ||
|
|
e5e9260ac3 | ||
|
|
7c93e95200 | ||
|
|
775cae375f | ||
|
|
05b7d5c293 | ||
|
|
3f638855d7 | ||
|
|
f2a9d0a05c | ||
|
|
291f1f054c | ||
|
|
cfa8442f28 | ||
|
|
2b59f07e4b | ||
|
|
62ae44acc4 | ||
|
|
c22793b7cf | ||
|
|
0b6fb4f3c2 | ||
|
|
7807462c7b | ||
|
|
7c2e17b44d | ||
|
|
0431fc830d | ||
|
|
73bc89d7ae | ||
|
|
49c4c0ecdb | ||
|
|
8265bf06e7 | ||
|
|
9ac7327691 | ||
|
|
6dd60fbd45 | ||
|
|
91c49e0e81 | ||
|
|
dfd5c3f07e | ||
|
|
6d2756a7ee | ||
|
|
61bf53bdda | ||
|
|
7059c10918 | ||
|
|
e78bc17187 | ||
|
|
eebedfd76e | ||
|
|
0111d6f1e2 | ||
|
|
ddd34fd5c0 | ||
|
|
754707c26c | ||
|
|
bcf0e655ca | ||
|
|
dea01cc02e | ||
|
|
72f3f432b5 | ||
|
|
4ddab432eb | ||
|
|
87ee910f68 | ||
|
|
2c376c7506 | ||
|
|
59824eb6ef | ||
|
|
6785011eb5 | ||
|
|
9fdc771569 | ||
|
|
f07190c3d9 | ||
|
|
1ba276eefa | ||
|
|
cec2301617 | ||
|
|
36ee67f86f | ||
|
|
4fa1f47565 | ||
|
|
b3fd726a95 | ||
|
|
127b2492a5 | ||
|
|
9727346ff1 | ||
|
|
b303d02492 | ||
|
|
ef22e91b07 | ||
|
|
94c92f7216 | ||
|
|
389590a04d | ||
|
|
1e283160cc | ||
|
|
bc272e14dd | ||
|
|
3f9e3e5d59 | ||
|
|
9c477e3980 | ||
|
|
aa6b1a3be3 | ||
|
|
3f1aa9f5cd | ||
|
|
e83afbda2c | ||
|
|
8a8d02eed6 | ||
|
|
ac3924aaf7 | ||
|
|
293cc93500 | ||
|
|
2eed0f7307 | ||
|
|
7ac848c9a0 | ||
|
|
9d7660d962 | ||
|
|
d7496f133c | ||
|
|
0e852e8a89 | ||
|
|
e1ad5d490b | ||
|
|
ae3c135660 | ||
|
|
16b113f7eb | ||
|
|
1810bcf3e2 | ||
|
|
293df015d6 | ||
|
|
3327cacabe | ||
|
|
467adcdd85 | ||
|
|
6f05e3bf03 | ||
|
|
b5fbb9db9e | ||
|
|
2954b4b2f5 | ||
|
|
04679ba1f6 | ||
|
|
7d432dc6f5 | ||
|
|
83b3204afa | ||
|
|
97c876c714 | ||
|
|
0538dd8f9c | ||
|
|
0109ef5bb2 | ||
|
|
2064eebdc6 | ||
|
|
99bf72ae60 | ||
|
|
b55dea13c1 | ||
|
|
f785e6da08 | ||
|
|
8b11f5da7f | ||
|
|
9295a8f163 | ||
|
|
95f06cf101 | ||
|
|
bf9a80db83 | ||
|
|
058c3ddcb3 | ||
|
|
0139f8d902 | ||
|
|
f5fbabc0ca | ||
|
|
51d2791c65 | ||
|
|
b84e4400ae | ||
|
|
dda4e60e58 | ||
|
|
a2fab4443d | ||
|
|
2420f56c7c | ||
|
|
0ff25607cb | ||
|
|
c2e9d0e581 | ||
|
|
b0048bf0ba | ||
|
|
4224678559 | ||
|
|
02c3a29390 | ||
|
|
229b2bda90 | ||
|
|
97407fa96f | ||
|
|
47943cc97a | ||
|
|
df817d7567 | ||
|
|
7017dbe8ee | ||
|
|
71b4113e0a | ||
|
|
295b5b67f8 | ||
|
|
021f15fd34 | ||
|
|
0d7829f628 | ||
|
|
db6f0e42dc | ||
|
|
9b43b5ddc2 | ||
|
|
01039fb990 | ||
|
|
f5ce18b0cc | ||
|
|
ab7f6cafbd | ||
|
|
5657a2e64d | ||
|
|
06350a58ce | ||
|
|
b288db9c04 | ||
|
|
2295de4f94 | ||
|
|
1aee984c75 | ||
|
|
4639c7b8af | ||
|
|
4184d7fd32 | ||
|
|
32a7fda42a | ||
|
|
eb195187db | ||
|
|
addb659cf3 | ||
|
|
c199e12be0 | ||
|
|
0e5cad4d1b | ||
|
|
9a1b9830a7 | ||
|
|
67a6eb2332 | ||
|
|
5712c869c3 | ||
|
|
a70ca6f48e | ||
|
|
2f8b15205e | ||
|
|
6a9d2fb9c6 | ||
|
|
ea8c5a4982 | ||
|
|
56a85c761d | ||
|
|
44d630220a | ||
|
|
d36c989af9 | ||
|
|
cd21579ef5 | ||
|
|
b4e79a4ac4 | ||
|
|
51cedb04b3 | ||
|
|
97bba2f113 | ||
|
|
32dadd7ef4 | ||
|
|
1841cf64fa | ||
|
|
59b2b7f078 | ||
|
|
4ff9798748 | ||
|
|
d06444ae01 | ||
|
|
4706a8debd | ||
|
|
90b4babe7f | ||
|
|
423528f0a6 | ||
|
|
9752ae0ea7 | ||
|
|
f3c54201f9 | ||
|
|
8dc3a2c67a | ||
|
|
440c79b818 | ||
|
|
e1eea82d7e | ||
|
|
103eb9f50d | ||
|
|
59503a425a | ||
|
|
874b911ee5 | ||
|
|
fdc0cbae66 | ||
|
|
b304d60e49 | ||
|
|
36bd7136e8 | ||
|
|
b87de92707 | ||
|
|
c478830ded | ||
|
|
fe67c73e78 | ||
|
|
935106e69d | ||
|
|
88daa4284d | ||
|
|
31685a313c | ||
|
|
616efbff56 | ||
|
|
4634fc5626 | ||
|
|
7264560294 | ||
|
|
8e7c0c6d42 | ||
|
|
92cfb28ba7 | ||
|
|
2617172cea | ||
|
|
ef844171cf | ||
|
|
46d5b28bcc | ||
|
|
b319bc8957 | ||
|
|
309c4b9380 | ||
|
|
17dfcc2706 | ||
|
|
329da19048 | ||
|
|
fbb71e07c4 | ||
|
|
0243441f36 | ||
|
|
ffa98fc7e2 | ||
|
|
95da2d425a | ||
|
|
1f19676282 | ||
|
|
16847eeec4 | ||
|
|
db43ffad8d | ||
|
|
d2f440bc9d | ||
|
|
e2cf767add | ||
|
|
23385437b5 | ||
|
|
d01d571415 | ||
|
|
f24c1d8bc5 | ||
|
|
81072e6621 | ||
|
|
ea7192c454 | ||
|
|
c0bad6fea3 | ||
|
|
149ad0d354 | ||
|
|
2c62904a05 | ||
|
|
48eb835ba7 | ||
|
|
7dc35c6487 | ||
|
|
f51277fd8e | ||
|
|
dd4f1d800d | ||
|
|
75a9b207f0 | ||
|
|
71866713e2 | ||
|
|
71ff5852c9 | ||
|
|
4a19ffd486 | ||
|
|
f19e1446e0 | ||
|
|
34ed909b18 | ||
|
|
c62887e809 | ||
|
|
c967d15189 | ||
|
|
2a4c193dc4 | ||
|
|
198918c066 | ||
|
|
fe7b5b3586 | ||
|
|
30130e5e99 | ||
|
|
03de2ecc3f | ||
|
|
b0e89ed1e6 | ||
|
|
e2973f6137 | ||
|
|
0bf5f22848 | ||
|
|
3f3042f354 | ||
|
|
caecee4a77 | ||
|
|
2caa88de07 | ||
|
|
75964bcbfe | ||
|
|
1535d4dad3 | ||
|
|
0924a8bbba | ||
|
|
353fdd591d | ||
|
|
ca306e8bcf | ||
|
|
f3f3ac67aa | ||
|
|
731a6b7297 | ||
|
|
0dff7e7812 | ||
|
|
5e8d4a34df | ||
|
|
82efe4865d | ||
|
|
eb97901fc8 | ||
|
|
c889e27f23 | ||
|
|
19ed4451d8 | ||
|
|
c43ce627bd | ||
|
|
440468a1b6 | ||
|
|
274e92a01b | ||
|
|
22d5212fc3 | ||
|
|
cbb9ad4be6 | ||
|
|
1c33d5ca67 | ||
|
|
fe45dcf309 | ||
|
|
8817ea630e | ||
|
|
b779902595 | ||
|
|
a977297183 | ||
|
|
d675e8ee53 | ||
|
|
9224ceda23 | ||
|
|
0cb78faf3d | ||
|
|
6923e849ef | ||
|
|
a9fd9add32 | ||
|
|
4b5869c2ed | ||
|
|
bba9542c75 | ||
|
|
86e084eab9 | ||
|
|
9d4f0a2125 | ||
|
|
67cb77ef8a | ||
|
|
530aafba20 |
6
.github/CODEOWNERS
vendored
@@ -1,2 +1,8 @@
|
||||
# Automatically assigned to any PRs
|
||||
* @davidcralph @alexsparkes
|
||||
|
||||
# Workflow files require maintainer approval
|
||||
/.github/workflows/ @davidcralph @alexsparkes
|
||||
|
||||
# Release process documentation
|
||||
/docs/RELEASE_PROCESS.md @davidcralph @alexsparkes
|
||||
|
||||
65
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
## Description
|
||||
<!-- Provide a brief description of your changes -->
|
||||
|
||||
|
||||
|
||||
## Type of Change
|
||||
<!-- Mark the relevant option with an 'x' -->
|
||||
|
||||
- [ ] 🐛 Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] ✨ New feature (non-breaking change which adds functionality)
|
||||
- [ ] 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] 📝 Documentation update
|
||||
- [ ] 🎨 UI/UX improvement
|
||||
- [ ] ⚡ Performance improvement
|
||||
- [ ] 🔧 Maintenance/refactoring
|
||||
|
||||
## Testing
|
||||
<!-- Describe the tests you ran and how to reproduce them -->
|
||||
|
||||
- [ ] Tested on Chrome/Edge
|
||||
- [ ] Tested on Firefox
|
||||
- [ ] Tested on Safari (if applicable)
|
||||
- [ ] Checked console for errors
|
||||
- [ ] Tested in different screen sizes
|
||||
|
||||
## Checklist
|
||||
|
||||
### General
|
||||
- [ ] My code follows the project's code style
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] My changes generate no new warnings or errors
|
||||
- [ ] I have tested my changes locally
|
||||
|
||||
### For Feature/Bug Fix PRs
|
||||
- [ ] I have added/updated tests that prove my fix is effective or that my feature works
|
||||
- [ ] New and existing unit tests pass locally with my changes
|
||||
- [ ] I have updated the documentation accordingly
|
||||
|
||||
### For Release PRs (beta → main)
|
||||
<!-- Only fill out if this is a release PR -->
|
||||
- [ ] Version has been bumped in all necessary files
|
||||
- [ ] Changelog has been updated
|
||||
- [ ] Beta testing period completed (minimum X days)
|
||||
- [ ] All critical bugs from beta have been resolved
|
||||
- [ ] Extension has been tested by at least X beta testers
|
||||
- [ ] No open P0/P1 issues blocking release
|
||||
- [ ] Release notes prepared
|
||||
- [ ] Store submission credentials verified
|
||||
|
||||
## Screenshots (if applicable)
|
||||
<!-- Add screenshots to help explain your changes -->
|
||||
|
||||
|
||||
|
||||
## Related Issues
|
||||
<!-- Link any related issues here -->
|
||||
|
||||
Closes #
|
||||
Relates to #
|
||||
|
||||
## Additional Notes
|
||||
<!-- Any additional information for reviewers -->
|
||||
|
||||
|
||||
18
.github/dependabot.yml
vendored
@@ -1,17 +1 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
ignore:
|
||||
- dependency-name: fontsource-lexend-deca
|
||||
versions:
|
||||
- ">= 4.a, < 5"
|
||||
- dependency-name: fontsource-roboto
|
||||
versions:
|
||||
- ">= 4.a, < 5"
|
||||
- dependency-name: react-modal
|
||||
versions:
|
||||
- 3.13.1
|
||||
|
||||
|
||||
15
.github/workflows/automerge.yml
vendored
@@ -5,14 +5,17 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
node-version: "20.x"
|
||||
bun-version: latest
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
run: bun install
|
||||
- name: Lint
|
||||
run: bun run lint
|
||||
continue-on-error: true
|
||||
- name: Build
|
||||
run: npm run build
|
||||
run: bun run build
|
||||
automerge:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
@@ -22,6 +25,6 @@ jobs:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: fastify/github-action-merge-dependabot@v3.0.0
|
||||
- uses: fastify/github-action-merge-dependabot@v3.10.1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
165
.github/workflows/beta-release.yml
vendored
Normal file
@@ -0,0 +1,165 @@
|
||||
name: Beta Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- beta
|
||||
tags:
|
||||
- 'v*-beta.*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
runs-on: ubuntu-latest
|
||||
environment: beta
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: '1.3.1'
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Build extension
|
||||
run: bun run build
|
||||
env:
|
||||
NODE_ENV: production
|
||||
|
||||
- name: Get version from package.json
|
||||
id: version
|
||||
run: |
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Building version: $VERSION"
|
||||
|
||||
- name: Generate changelog
|
||||
id: changelog
|
||||
run: |
|
||||
# Get the latest beta or production tag
|
||||
PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$PREVIOUS_TAG" ]; then
|
||||
echo "No previous tag found, using all commits"
|
||||
COMMITS=$(git log --pretty=format:"- %s (%h)" HEAD)
|
||||
else
|
||||
echo "Generating changelog from $PREVIOUS_TAG to HEAD"
|
||||
COMMITS=$(git log --pretty=format:"- %s (%h)" ${PREVIOUS_TAG}..HEAD)
|
||||
fi
|
||||
|
||||
# Create changelog with categorization
|
||||
FEATURES=$(echo "$COMMITS" | grep -i "^- feat" || echo "")
|
||||
FIXES=$(echo "$COMMITS" | grep -i "^- fix" || echo "")
|
||||
CHORES=$(echo "$COMMITS" | grep -i "^- chore\|^- docs\|^- style\|^- refactor" || echo "")
|
||||
OTHER=$(echo "$COMMITS" | grep -v -i "^- feat\|^- fix\|^- chore\|^- docs\|^- style\|^- refactor" || echo "")
|
||||
|
||||
{
|
||||
echo "changelog<<EOF"
|
||||
if [ -n "$FEATURES" ]; then
|
||||
echo "### ✨ Features"
|
||||
echo "$FEATURES"
|
||||
echo ""
|
||||
fi
|
||||
if [ -n "$FIXES" ]; then
|
||||
echo "### 🐛 Bug Fixes"
|
||||
echo "$FIXES"
|
||||
echo ""
|
||||
fi
|
||||
if [ -n "$CHORES" ]; then
|
||||
echo "### 🔧 Maintenance"
|
||||
echo "$CHORES"
|
||||
echo ""
|
||||
fi
|
||||
if [ -n "$OTHER" ]; then
|
||||
echo "### 📝 Other Changes"
|
||||
echo "$OTHER"
|
||||
fi
|
||||
echo "EOF"
|
||||
} >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check if release exists
|
||||
id: check_release
|
||||
run: |
|
||||
if gh release view "v${{ steps.version.outputs.version }}" >/dev/null 2>&1; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Create or Update GitHub Pre-Release
|
||||
run: |
|
||||
RELEASE_NOTES=$(cat <<EOF
|
||||
## 🧪 Mue Beta v${{ steps.version.outputs.version }}
|
||||
|
||||
**⚠️ This is a beta release for testing purposes only.**
|
||||
|
||||
### Testing Instructions
|
||||
1. Download the appropriate ZIP file below
|
||||
2. For Chrome: Load as unpacked extension or install from [unlisted link](https://chromewebstore.google.com/detail/mue/bngmbednanpcfochchhgbkookpiaiaid) (dev team only)
|
||||
3. For Firefox: Install via about:debugging → Load Temporary Add-on
|
||||
4. Report issues at https://github.com/mue/mue/issues
|
||||
|
||||
${{ steps.changelog.outputs.changelog }}
|
||||
|
||||
### Installation Files
|
||||
- **Chrome/Edge**: \`chrome-${{ steps.version.outputs.version }}.zip\`
|
||||
- **Firefox**: \`firefox-${{ steps.version.outputs.version }}.zip\`
|
||||
|
||||
---
|
||||
|
||||
**🔗 Demo**: [demo.muetab.com](https://demo.muetab.com)
|
||||
**📱 Beta Branch Demo**: [mue-git-beta-mue.vercel.app](https://mue-git-beta-mue.vercel.app)
|
||||
EOF
|
||||
)
|
||||
|
||||
if [ "${{ steps.check_release.outputs.exists }}" = "true" ]; then
|
||||
echo "Updating existing release..."
|
||||
gh release edit "v${{ steps.version.outputs.version }}" \
|
||||
--notes "$RELEASE_NOTES" \
|
||||
--prerelease
|
||||
|
||||
# Upload new files (will replace if they exist)
|
||||
gh release upload "v${{ steps.version.outputs.version }}" \
|
||||
"build/chrome-${{ steps.version.outputs.version }}.zip" \
|
||||
"build/firefox-${{ steps.version.outputs.version }}.zip" \
|
||||
--clobber
|
||||
else
|
||||
echo "Creating new release..."
|
||||
gh release create "v${{ steps.version.outputs.version }}" \
|
||||
"build/chrome-${{ steps.version.outputs.version }}.zip" \
|
||||
"build/firefox-${{ steps.version.outputs.version }}.zip" \
|
||||
--title "Beta v${{ steps.version.outputs.version }}" \
|
||||
--notes "$RELEASE_NOTES" \
|
||||
--prerelease
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Output release info
|
||||
run: |
|
||||
echo "## 🎉 Beta Release Created!" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Version**: v${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Release URL**: https://github.com/${{ github.repository }}/releases/tag/v${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 📦 Build Artifacts" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Chrome/Edge: \`chrome-${{ steps.version.outputs.version }}.zip\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Firefox: \`firefox-${{ steps.version.outputs.version }}.zip\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 🧪 Testing" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Share the release link with beta testers for feedback." >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### ⚠️ Next Steps" >> $GITHUB_STEP_SUMMARY
|
||||
echo "1. Test the beta release thoroughly" >> $GITHUB_STEP_SUMMARY
|
||||
echo "2. Gather feedback from testers" >> $GITHUB_STEP_SUMMARY
|
||||
echo "3. Fix any critical issues" >> $GITHUB_STEP_SUMMARY
|
||||
echo "4. When ready, create PR from \`beta\` → \`main\` for production release" >> $GITHUB_STEP_SUMMARY
|
||||
194
.github/workflows/hotfix-release.yml
vendored
Normal file
@@ -0,0 +1,194 @@
|
||||
name: Hotfix Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
description:
|
||||
description: 'Brief description of the hotfix'
|
||||
required: true
|
||||
branch_name:
|
||||
description: 'Hotfix branch name (e.g., hotfix/critical-security-fix)'
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
hotfix-release:
|
||||
runs-on: ubuntu-latest
|
||||
environment: production # Requires maintainer approval
|
||||
steps:
|
||||
- name: Validate branch name
|
||||
run: |
|
||||
if [[ ! "${{ github.event.inputs.branch_name }}" =~ ^hotfix/ ]]; then
|
||||
echo "❌ Branch name must start with 'hotfix/'" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout hotfix branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch_name }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: '1.3.1'
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Calculate hotfix version (auto-patch bump)
|
||||
id: version
|
||||
run: |
|
||||
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
||||
echo "Current version: $CURRENT_VERSION"
|
||||
|
||||
# Remove any pre-release suffix
|
||||
BASE_VERSION=$(echo $CURRENT_VERSION | sed 's/-.*$//')
|
||||
IFS='.' read -r -a VERSION_PARTS <<< "$BASE_VERSION"
|
||||
|
||||
MAJOR="${VERSION_PARTS[0]}"
|
||||
MINOR="${VERSION_PARTS[1]}"
|
||||
PATCH="${VERSION_PARTS[2]}"
|
||||
|
||||
# Hotfixes always bump patch version
|
||||
PATCH=$((PATCH + 1))
|
||||
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
|
||||
|
||||
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Hotfix version will be: $NEW_VERSION"
|
||||
|
||||
- name: Update version in all files
|
||||
run: |
|
||||
# Update package.json
|
||||
bun x json -I -f package.json -e "this.version='${{ steps.version.outputs.new_version }}'"
|
||||
|
||||
# Update manifests
|
||||
bun x json -I -f manifest/chrome.json -e "this.version='${{ steps.version.outputs.new_version }}'"
|
||||
bun x json -I -f manifest/firefox.json -e "this.version='${{ steps.version.outputs.new_version }}'"
|
||||
bun x json -I -f safari/Mue\ Extension/Resources/manifest.json -e "this.version='${{ steps.version.outputs.new_version }}'"
|
||||
|
||||
# Update Safari Xcode project
|
||||
sed -i "s/MARKETING_VERSION = [^;]*/MARKETING_VERSION = ${{ steps.version.outputs.new_version }}/g" safari/Mue.xcodeproj/project.pbxproj
|
||||
|
||||
# Update constants.js
|
||||
sed -i "s/export const VERSION = '[^']*'/export const VERSION = '${{ steps.version.outputs.new_version }}'/" src/config/constants.js
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Build extension
|
||||
run: bun run build
|
||||
env:
|
||||
NODE_ENV: production
|
||||
|
||||
- name: Commit version bump
|
||||
run: |
|
||||
git add package.json manifest/chrome.json manifest/firefox.json safari/Mue\ Extension/Resources/manifest.json safari/Mue.xcodeproj/project.pbxproj src/config/constants.js
|
||||
git commit -m "chore: hotfix version bump to ${{ steps.version.outputs.new_version }}"
|
||||
|
||||
- name: Merge hotfix to main
|
||||
run: |
|
||||
git fetch origin main
|
||||
git checkout main
|
||||
git merge --no-ff ${{ github.event.inputs.branch_name }} -m "fix: merge hotfix ${{ github.event.inputs.branch_name }} (#${{ steps.version.outputs.new_version }})"
|
||||
git tag -a "v${{ steps.version.outputs.new_version }}" -m "Hotfix v${{ steps.version.outputs.new_version }}: ${{ github.event.inputs.description }}"
|
||||
git push origin main
|
||||
git push origin "v${{ steps.version.outputs.new_version }}"
|
||||
|
||||
- name: Generate changelog
|
||||
id: changelog
|
||||
run: |
|
||||
# Get commits from hotfix branch
|
||||
git checkout ${{ github.event.inputs.branch_name }}
|
||||
COMMITS=$(git log --pretty=format:"- %s (%h)" origin/main..${{ github.event.inputs.branch_name }})
|
||||
|
||||
{
|
||||
echo "changelog<<EOF"
|
||||
echo "### 🚨 Hotfix"
|
||||
echo "${{ github.event.inputs.description }}"
|
||||
echo ""
|
||||
echo "### Changes"
|
||||
echo "$COMMITS"
|
||||
echo "EOF"
|
||||
} >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create GitHub Release
|
||||
run: |
|
||||
git checkout main
|
||||
|
||||
RELEASE_NOTES=$(cat <<EOF
|
||||
## 🚨 Mue Hotfix v${{ steps.version.outputs.new_version }}
|
||||
|
||||
**This is an emergency hotfix release.**
|
||||
|
||||
${{ steps.changelog.outputs.changelog }}
|
||||
|
||||
### 📦 Installation
|
||||
|
||||
**Browser Extensions (will be updated shortly):**
|
||||
- **Chrome**: [Chrome Web Store](https://chromewebstore.google.com/detail/mue/bngmbednanpcfochchhgbkookpiaiaid)
|
||||
- **Edge**: [Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/mue/aepnglgjfokepefimhbnibfjekidhmja)
|
||||
- **Firefox**: [Firefox Add-ons](https://addons.mozilla.org/en-GB/firefox/addon/mue/)
|
||||
|
||||
**Immediate Manual Installation:**
|
||||
- Download the ZIP file below for immediate deployment
|
||||
- Chrome/Edge: Load unpacked extension
|
||||
- Firefox: Install from about:debugging
|
||||
|
||||
---
|
||||
|
||||
**⚠️ This hotfix should be submitted to stores immediately.**
|
||||
EOF
|
||||
)
|
||||
|
||||
gh release create "v${{ steps.version.outputs.new_version }}" \
|
||||
"build/chrome-${{ steps.version.outputs.new_version }}.zip" \
|
||||
"build/firefox-${{ steps.version.outputs.new_version }}.zip" \
|
||||
--title "🚨 Hotfix v${{ steps.version.outputs.new_version }}" \
|
||||
--notes "$RELEASE_NOTES" \
|
||||
--latest
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Back-merge to beta
|
||||
run: |
|
||||
git fetch origin beta
|
||||
git checkout beta
|
||||
git merge --no-ff main -m "chore: back-merge hotfix v${{ steps.version.outputs.new_version }} from main"
|
||||
git push origin beta
|
||||
|
||||
- name: Back-merge to dev
|
||||
run: |
|
||||
git fetch origin dev
|
||||
git checkout dev
|
||||
git merge --no-ff main -m "chore: back-merge hotfix v${{ steps.version.outputs.new_version }} from main"
|
||||
git push origin dev
|
||||
|
||||
- name: Output success summary
|
||||
run: |
|
||||
echo "## 🚨 Hotfix Released!" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Version**: v${{ steps.version.outputs.new_version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Description**: ${{ github.event.inputs.description }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Release URL**: https://github.com/${{ github.repository }}/releases/tag/v${{ steps.version.outputs.new_version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### ✅ Completed Actions" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [x] Merged hotfix to \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [x] Created tag v${{ steps.version.outputs.new_version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [x] Created GitHub Release" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [x] Back-merged to \`beta\` branch" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [x] Back-merged to \`dev\` branch" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 🚨 URGENT: Manual Steps Required" >> $GITHUB_STEP_SUMMARY
|
||||
echo "1. **Submit to stores IMMEDIATELY**:" >> $GITHUB_STEP_SUMMARY
|
||||
echo " - Go to [Submit workflow](https://github.com/${{ github.repository }}/actions/workflows/submit.yml)" >> $GITHUB_STEP_SUMMARY
|
||||
echo " - Run with tag: \`${{ steps.version.outputs.new_version }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "2. Update [muetab.com/blog/changelog](https://muetab.com/blog/changelog)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "3. Notify users via Discord/social media" >> $GITHUB_STEP_SUMMARY
|
||||
echo "4. Delete hotfix branch: \`${{ github.event.inputs.branch_name }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
187
.github/workflows/production-release.yml
vendored
Normal file
@@ -0,0 +1,187 @@
|
||||
name: Production Release
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
types:
|
||||
- closed
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
# Only run if PR was merged (not just closed)
|
||||
check-merge:
|
||||
if: github.event.pull_request.merged == true && github.event.pull_request.head.ref == 'beta'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_release: ${{ steps.check.outputs.should_release }}
|
||||
steps:
|
||||
- name: Check if this is a beta to main merge
|
||||
id: check
|
||||
run: |
|
||||
echo "should_release=true" >> $GITHUB_OUTPUT
|
||||
echo "✅ This is a beta → main merge, proceeding with production release" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
build-and-release:
|
||||
needs: check-merge
|
||||
if: needs.check-merge.outputs.should_release == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
environment: production # Requires manual approval from maintainers
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: '1.3.1'
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Build extension
|
||||
run: bun run build
|
||||
env:
|
||||
NODE_ENV: production
|
||||
|
||||
- name: Get version from package.json
|
||||
id: version
|
||||
run: |
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
# Remove any pre-release suffix for production
|
||||
STABLE_VERSION=$(echo $VERSION | sed 's/-.*$//')
|
||||
echo "version=$STABLE_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "full_version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Building production version: $STABLE_VERSION"
|
||||
|
||||
- name: Generate production changelog
|
||||
id: changelog
|
||||
run: |
|
||||
# Get the latest production (non-beta) tag
|
||||
PREVIOUS_TAG=$(git tag -l 'v*' --sort=-v:refname | grep -v 'beta\|alpha\|rc' | head -n 1 || echo "")
|
||||
|
||||
if [ -z "$PREVIOUS_TAG" ]; then
|
||||
echo "No previous production tag found, using all commits"
|
||||
COMMITS=$(git log --pretty=format:"- %s (%h)" main)
|
||||
else
|
||||
echo "Generating changelog from $PREVIOUS_TAG to main"
|
||||
COMMITS=$(git log --pretty=format:"- %s (%h)" ${PREVIOUS_TAG}..main)
|
||||
fi
|
||||
|
||||
# Categorize commits
|
||||
FEATURES=$(echo "$COMMITS" | grep -i "^- feat" || echo "")
|
||||
FIXES=$(echo "$COMMITS" | grep -i "^- fix" || echo "")
|
||||
PERFORMANCE=$(echo "$COMMITS" | grep -i "^- perf" || echo "")
|
||||
BREAKING=$(echo "$COMMITS" | grep -i "BREAKING CHANGE" || echo "")
|
||||
|
||||
{
|
||||
echo "changelog<<EOF"
|
||||
if [ -n "$BREAKING" ]; then
|
||||
echo "### ⚠️ Breaking Changes"
|
||||
echo "$BREAKING"
|
||||
echo ""
|
||||
fi
|
||||
if [ -n "$FEATURES" ]; then
|
||||
echo "### ✨ New Features"
|
||||
echo "$FEATURES"
|
||||
echo ""
|
||||
fi
|
||||
if [ -n "$FIXES" ]; then
|
||||
echo "### 🐛 Bug Fixes"
|
||||
echo "$FIXES"
|
||||
echo ""
|
||||
fi
|
||||
if [ -n "$PERFORMANCE" ]; then
|
||||
echo "### ⚡ Performance Improvements"
|
||||
echo "$PERFORMANCE"
|
||||
fi
|
||||
echo "EOF"
|
||||
} >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check if tag exists
|
||||
id: check_tag
|
||||
run: |
|
||||
if git rev-parse "v${{ steps.version.outputs.version }}" >/dev/null 2>&1; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Create production tag
|
||||
if: steps.check_tag.outputs.exists == 'false'
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git tag -a "v${{ steps.version.outputs.version }}" -m "Release v${{ steps.version.outputs.version }}"
|
||||
git push origin "v${{ steps.version.outputs.version }}"
|
||||
|
||||
- name: Create GitHub Release
|
||||
run: |
|
||||
RELEASE_NOTES=$(cat <<EOF
|
||||
## 🎉 Mue v${{ steps.version.outputs.version }}
|
||||
|
||||
${{ steps.changelog.outputs.changelog }}
|
||||
|
||||
### 📦 Installation
|
||||
|
||||
**Browser Extensions:**
|
||||
- **Chrome**: [Chrome Web Store](https://chromewebstore.google.com/detail/mue/bngmbednanpcfochchhgbkookpiaiaid)
|
||||
- **Edge**: [Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/mue/aepnglgjfokepefimhbnibfjekidhmja)
|
||||
- **Firefox**: [Firefox Add-ons](https://addons.mozilla.org/en-GB/firefox/addon/mue/)
|
||||
|
||||
**Manual Installation:**
|
||||
- Download the appropriate ZIP file below
|
||||
- Chrome/Edge: Load unpacked extension from extracted folder
|
||||
- Firefox: Install from about:debugging → Load Temporary Add-on
|
||||
|
||||
### 🔗 Links
|
||||
- **Demo**: [demo.muetab.com](https://demo.muetab.com)
|
||||
- **Changelog**: [muetab.com/blog/changelog](https://muetab.com/blog/changelog)
|
||||
- **Documentation**: [github.com/mue/mue](https://github.com/mue/mue)
|
||||
|
||||
---
|
||||
|
||||
### 📝 Build Information
|
||||
- **Version**: ${{ steps.version.outputs.version }}
|
||||
- **Build Date**: $(date -u +"%Y-%m-%d %H:%M:%S UTC")
|
||||
- **Commit**: ${{ github.sha }}
|
||||
EOF
|
||||
)
|
||||
|
||||
gh release create "v${{ steps.version.outputs.version }}" \
|
||||
"build/chrome-${{ steps.version.outputs.version }}.zip" \
|
||||
"build/firefox-${{ steps.version.outputs.version }}.zip" \
|
||||
--title "Mue v${{ steps.version.outputs.version }}" \
|
||||
--notes "$RELEASE_NOTES" \
|
||||
--latest
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Output success summary
|
||||
run: |
|
||||
echo "## 🚀 Production Release Published!" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Version**: v${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Release URL**: https://github.com/${{ github.repository }}/releases/tag/v${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 📦 Build Artifacts" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Chrome/Edge: \`chrome-${{ steps.version.outputs.version }}.zip\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Firefox: \`firefox-${{ steps.version.outputs.version }}.zip\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### ⚠️ Manual Steps Required" >> $GITHUB_STEP_SUMMARY
|
||||
echo "1. Go to [GitHub Actions](https://github.com/${{ github.repository }}/actions/workflows/submit.yml)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "2. Click 'Run workflow'" >> $GITHUB_STEP_SUMMARY
|
||||
echo "3. Enter tag: \`${{ steps.version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "4. Click 'Run workflow' to submit to Chrome/Firefox/Edge stores" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 📢 Post-Release Checklist" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [ ] Submit to browser stores (see manual steps above)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [ ] Update [muetab.com/blog/changelog](https://muetab.com/blog/changelog)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [ ] Announce release on Discord/social media" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [ ] Monitor issue tracker for bug reports" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [ ] Merge \`main\` back to \`beta\` and \`dev\` to sync version" >> $GITHUB_STEP_SUMMARY
|
||||
57
.github/workflows/submit-beta.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
name: Submit to Browser Stores (Beta)
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [prereleased]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Pre-release tag to re-submit (e.g. v7.6.1-beta.3)"
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
submit-beta:
|
||||
runs-on: ubuntu-latest
|
||||
environment: beta
|
||||
|
||||
steps:
|
||||
- name: Resolve tag and version
|
||||
id: tag
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
TAG="${{ github.event.inputs.tag }}"
|
||||
else
|
||||
TAG="${{ github.event.release.tag_name }}"
|
||||
fi
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
echo "version=${TAG#v}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download release ZIPs
|
||||
run: |
|
||||
mkdir -p dist
|
||||
gh release download "${{ steps.tag.outputs.tag }}" \
|
||||
--pattern "chrome-*.zip" \
|
||||
--dir ./dist
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Verify ZIP
|
||||
run: |
|
||||
ls -la dist/
|
||||
test -n "$(ls dist/chrome-*.zip 2>/dev/null)" || \
|
||||
{ echo "chrome ZIP not found in dist/"; exit 1; }
|
||||
|
||||
- name: Submit to Chrome Web Store (Trusted Testers)
|
||||
uses: PlasmoHQ/bpp@v3
|
||||
with:
|
||||
keys: ${{ secrets.SUBMIT_KEYS_BETA }}
|
||||
chrome-zip: dist/chrome-${{ steps.tag.outputs.version }}.zip
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Beta Store Submission: ${{ steps.tag.outputs.tag }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Chrome Web Store (trustedTesters)" >> $GITHUB_STEP_SUMMARY
|
||||
56
.github/workflows/submit.yml
vendored
@@ -1,27 +1,57 @@
|
||||
name: Submit
|
||||
name: Submit to Browser Stores
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Release tag to submit, i.e 6.0.5"
|
||||
description: "Release tag to re-submit (e.g. v7.6.1)"
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
submit:
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
|
||||
steps:
|
||||
- name: Setup Chrome
|
||||
uses: browser-actions/setup-chrome@latest
|
||||
with:
|
||||
chrome-version: latest
|
||||
- name: Download Github Release Assets
|
||||
uses: PlasmoHQ/download-release-asset@v1.0.0
|
||||
with:
|
||||
tag: ${{ github.event.inputs.tag }}
|
||||
- name: Browser Plugin Publish
|
||||
uses: PlasmoHQ/bpp@v2
|
||||
- name: Resolve tag and version
|
||||
id: tag
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
TAG="${{ github.event.inputs.tag }}"
|
||||
else
|
||||
TAG="${{ github.event.release.tag_name }}"
|
||||
fi
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
echo "version=${TAG#v}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download release ZIPs
|
||||
run: |
|
||||
mkdir -p dist
|
||||
gh release download "${{ steps.tag.outputs.tag }}" \
|
||||
--pattern "chrome-*.zip" \
|
||||
--dir ./dist
|
||||
env:
|
||||
PUPPETEER_EXECUTABLE_PATH: /opt/hostedtoolcache/chromium/latest/x64/chrome
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Verify ZIP
|
||||
run: |
|
||||
ls -la dist/
|
||||
test -f "dist/chrome-${{ steps.tag.outputs.version }}.zip" || \
|
||||
{ echo "chrome ZIP not found"; exit 1; }
|
||||
|
||||
- name: Submit to Chrome Web Store
|
||||
uses: PlasmoHQ/bpp@v3
|
||||
with:
|
||||
keys: ${{ secrets.SUBMIT_KEYS }}
|
||||
chrome-zip: dist/chrome-${{ steps.tag.outputs.version }}.zip
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Store Submission: ${{ steps.tag.outputs.tag }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Chrome Web Store (listed)" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
136
.github/workflows/version-bump.yml
vendored
Normal file
@@ -0,0 +1,136 @@
|
||||
name: Version Bump
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
labels:
|
||||
- 'release'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
bump-version:
|
||||
if: github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: '1.3.1'
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Extract bump info from PR
|
||||
id: bump_info
|
||||
run: |
|
||||
BUMP_TYPE=$(echo "${{ github.event.pull_request.body }}" | grep -oP "bump: \K(major|minor|patch)" || echo "patch")
|
||||
IS_RELEASE=$(echo "${{ github.event.pull_request.body }}" | grep -q "is_release: true" && echo "true" || echo "false")
|
||||
|
||||
echo "bump_type=$BUMP_TYPE" >> $GITHUB_OUTPUT
|
||||
echo "is_release=$IS_RELEASE" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Calculate new version
|
||||
id: version
|
||||
run: |
|
||||
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
||||
BASE_VERSION=$(echo $CURRENT_VERSION | sed 's/-.*$//')
|
||||
IFS='.' read -r -a VERSION_PARTS <<< "$BASE_VERSION"
|
||||
|
||||
MAJOR="${VERSION_PARTS[0]}"
|
||||
MINOR="${VERSION_PARTS[1]}"
|
||||
PATCH="${VERSION_PARTS[2]}"
|
||||
|
||||
case "${{ steps.bump_info.outputs.bump_type }}" in
|
||||
major)
|
||||
MAJOR=$((MAJOR + 1))
|
||||
MINOR=0
|
||||
PATCH=0
|
||||
;;
|
||||
minor)
|
||||
MINOR=$((MINOR + 1))
|
||||
PATCH=0
|
||||
;;
|
||||
patch)
|
||||
PATCH=$((PATCH + 1))
|
||||
;;
|
||||
esac
|
||||
|
||||
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
|
||||
|
||||
# Add pre-release suffix if NOT a production release AND NOT on main branch
|
||||
if [ "${{ steps.bump_info.outputs.is_release }}" != "true" ] && [ "${{ github.ref_name }}" != "main" ]; then
|
||||
BETA_COUNT=$(git tag -l "v${NEW_VERSION}-beta.*" | wc -l)
|
||||
BETA_NUM=$((BETA_COUNT + 1))
|
||||
NEW_VERSION="${NEW_VERSION}-beta.${BETA_NUM}"
|
||||
fi
|
||||
|
||||
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update package.json
|
||||
run: bun x json -I -f package.json -e "this.version='${{ steps.version.outputs.new_version }}'"
|
||||
|
||||
- name: Update Chrome manifest
|
||||
run: bun x json -I -f manifest/chrome.json -e "this.version='${{ steps.version.outputs.new_version }}'"
|
||||
|
||||
- name: Update Firefox manifest
|
||||
run: bun x json -I -f manifest/firefox.json -e "this.version='${{ steps.version.outputs.new_version }}'"
|
||||
|
||||
- name: Update Safari manifest
|
||||
run: bun x json -I -f safari/Mue\ Extension/Resources/manifest.json -e "this.version='${{ steps.version.outputs.new_version }}'"
|
||||
|
||||
- name: Update Safari Xcode (4 MARKETING_VERSION entries)
|
||||
run: sed -i '' "s/MARKETING_VERSION = [^;]*/MARKETING_VERSION = ${{ steps.version.outputs.new_version }}/g" safari/Mue.xcodeproj/project.pbxproj
|
||||
|
||||
- name: Update constants.js
|
||||
run: sed -i '' "s/export const VERSION = '[^']*'/export const VERSION = '${{ steps.version.outputs.new_version }}'/" src/config/constants.js
|
||||
|
||||
- name: Commit and tag
|
||||
run: |
|
||||
git add package.json manifest/chrome.json manifest/firefox.json safari/Mue\ Extension/Resources/manifest.json safari/Mue.xcodeproj/project.pbxproj src/config/constants.js
|
||||
git commit -m "chore: bump version to ${{ steps.version.outputs.new_version }}"
|
||||
git tag -a "v${{ steps.version.outputs.new_version }}" -m "Release v${{ steps.version.outputs.new_version }}"
|
||||
git push origin ${{ github.ref_name }}
|
||||
git push origin "v${{ steps.version.outputs.new_version }}"
|
||||
|
||||
- name: Auto back-merge (main only)
|
||||
if: github.ref_name == 'main'
|
||||
run: |
|
||||
git fetch origin
|
||||
git checkout beta
|
||||
git merge --no-ff origin/main -m "chore: back-merge main into beta"
|
||||
git push origin beta
|
||||
|
||||
git checkout dev
|
||||
git merge --no-ff origin/beta -m "chore: back-merge beta into dev"
|
||||
git push origin dev
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "✅ Version bumped to ${{ steps.version.outputs.new_version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "📦 Tag: v${{ steps.version.outputs.new_version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "🔀 Branch: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Files updated:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- package.json" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- manifest/chrome.json" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- manifest/firefox.json" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- safari/Mue Extension/Resources/manifest.json" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- safari/Mue.xcodeproj/project.pbxproj" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- src/config/constants.js" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "${{ github.ref_name }}" = "main" ]; then
|
||||
echo "### 🔄 Auto back-merge:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- main → beta ✅" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- beta → dev ✅" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
22
.gitignore
vendored
@@ -6,6 +6,28 @@ build/
|
||||
.idea/
|
||||
dist/
|
||||
|
||||
# Safari Extension Build Files
|
||||
safari/Mue Extension/Resources/*.html
|
||||
safari/Mue Extension/Resources/*.js
|
||||
safari/Mue Extension/Resources/*.css
|
||||
safari/Mue Extension/Resources/assets/
|
||||
safari/Mue Extension/Resources/icons/
|
||||
safari/Mue Extension/Resources/src/
|
||||
safari/Mue Extension/Resources/_locales/
|
||||
!safari/Mue Extension/Resources/manifest.json
|
||||
|
||||
# Xcode User Data
|
||||
safari/Mue.xcodeproj/xcuserdata/
|
||||
safari/Mue.xcodeproj/project.xcworkspace/xcuserdata/
|
||||
*.xcuserstate
|
||||
*.xcbkptlist
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.perspectivev3
|
||||
*.pbxuser
|
||||
safari/DerivedData/
|
||||
safari/build/
|
||||
|
||||
# Files
|
||||
package-lock.json
|
||||
.stylelintcache
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
#!/bin/sh
|
||||
. "${dirname "$0"}/_/husky/husky.sh"
|
||||
|
||||
npx commitlint --edit $1
|
||||
bunx --bun commitlint --edit $1
|
||||
|
||||
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
||||
# bun run lint
|
||||
310
CONTRIBUTING.md
Normal file
@@ -0,0 +1,310 @@
|
||||
# Contributing to Mue
|
||||
|
||||
Thanks for your interest in contributing to Mue! We welcome contributions from the community.
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Development Workflow](#development-workflow)
|
||||
- [Branch Strategy](#branch-strategy)
|
||||
- [Making Changes](#making-changes)
|
||||
- [Commit Messages](#commit-messages)
|
||||
- [Pull Request Process](#pull-request-process)
|
||||
- [Release Process](#release-process)
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Please be respectful and constructive in your interactions with the community.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Bun](https://bun.sh/) >= 1.3.0
|
||||
- Node.js >= 20.0.0 (for some tooling)
|
||||
- Git
|
||||
|
||||
### Setup
|
||||
|
||||
1. Fork the repository
|
||||
2. Clone your fork:
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/mue.git
|
||||
cd mue
|
||||
```
|
||||
|
||||
3. Install dependencies:
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
4. Start development server:
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Scripts
|
||||
|
||||
- `bun run dev` - Start development server with hot reload
|
||||
- `bun run dev:host` - Start development server accessible on network
|
||||
- `bun run build` - Build production extension for all browsers
|
||||
- `bun run lint` - Run ESLint and Stylelint
|
||||
- `bun run lint:fix` - Auto-fix linting issues
|
||||
- `bun run pretty` - Format code with Prettier
|
||||
|
||||
### Testing Your Changes
|
||||
|
||||
1. Load the extension in your browser:
|
||||
- **Chrome/Edge**: Go to `chrome://extensions`, enable Developer mode, click "Load unpacked", select `dist` folder
|
||||
- **Firefox**: Go to `about:debugging#/runtime/this-firefox`, click "Load Temporary Add-on", select any file in `dist` folder
|
||||
|
||||
2. Test your changes thoroughly across different browsers
|
||||
|
||||
## Branch Strategy
|
||||
|
||||
Mue uses a three-branch workflow:
|
||||
|
||||
```
|
||||
dev (active development)
|
||||
↓
|
||||
beta (release candidates)
|
||||
↓
|
||||
main (production/stable)
|
||||
```
|
||||
|
||||
### Branches
|
||||
|
||||
- **`dev`** - Active development branch
|
||||
- All feature and bug fix PRs merge here first
|
||||
- Maintainers can push directly for small fixes
|
||||
- Contributors must create PRs
|
||||
- CI must pass before merge
|
||||
|
||||
- **`beta`** - Release candidate testing
|
||||
- PRs from `dev` → `beta` for release candidates
|
||||
- Triggers beta release workflow
|
||||
- Requires 2 maintainer approvals
|
||||
- Used for testing with beta testers before production
|
||||
|
||||
- **`main`** - Production/stable releases
|
||||
- PRs from `beta` → `main` only
|
||||
- Triggers production release workflow
|
||||
- Requires 2 maintainer approvals + manual environment approval
|
||||
- Represents current live extension version
|
||||
|
||||
### Special Branches
|
||||
|
||||
- **`hotfix/*`** - Emergency production fixes
|
||||
- Branch from `main` for critical bugs
|
||||
- Triggers hotfix workflow (auto-merges to all branches)
|
||||
- Maintainers only
|
||||
|
||||
## Making Changes
|
||||
|
||||
### For Contributors
|
||||
|
||||
1. Create a feature branch from `dev`:
|
||||
```bash
|
||||
git checkout dev
|
||||
git pull origin dev
|
||||
git checkout -b feature/your-feature-name
|
||||
```
|
||||
|
||||
2. Make your changes following our code style
|
||||
|
||||
3. Test your changes locally
|
||||
|
||||
4. Commit your changes (see [Commit Messages](#commit-messages))
|
||||
|
||||
5. Push to your fork:
|
||||
```bash
|
||||
git push origin feature/your-feature-name
|
||||
```
|
||||
|
||||
6. Create a Pull Request targeting the `dev` branch
|
||||
|
||||
### For Maintainers
|
||||
|
||||
Maintainers can push directly to `dev` for small fixes, or follow the contributor process for larger changes.
|
||||
|
||||
## Commit Messages
|
||||
|
||||
We use [Conventional Commits](https://www.conventionalcommits.org/) for automated changelog generation.
|
||||
|
||||
### Format
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer]
|
||||
```
|
||||
|
||||
### Types
|
||||
|
||||
- `feat:` - New feature
|
||||
- `fix:` - Bug fix
|
||||
- `perf:` - Performance improvement
|
||||
- `docs:` - Documentation changes
|
||||
- `style:` - Code style changes (formatting, etc.)
|
||||
- `refactor:` - Code refactoring
|
||||
- `test:` - Adding or updating tests
|
||||
- `chore:` - Maintenance tasks, dependency updates
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
feat(weather): add hourly forecast widget
|
||||
fix(greeting): resolve time zone display issue
|
||||
perf(background): optimize image loading
|
||||
docs(readme): update installation instructions
|
||||
chore: bump version to 7.6.0
|
||||
```
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
For breaking changes, add `BREAKING CHANGE:` in the commit body:
|
||||
|
||||
```bash
|
||||
feat(api): change settings storage format
|
||||
|
||||
BREAKING CHANGE: Settings format has changed. Users will need to reconfigure their settings.
|
||||
```
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
1. **Fill out the PR template** completely
|
||||
|
||||
2. **Ensure all checks pass**:
|
||||
- ✅ Build succeeds
|
||||
- ✅ Linting passes
|
||||
- ✅ No merge conflicts
|
||||
|
||||
3. **Get reviews**:
|
||||
- Contributors: 1 maintainer approval required
|
||||
- Beta → Main: 2 maintainer approvals required
|
||||
|
||||
4. **Address review feedback**
|
||||
|
||||
5. **Squash commits** if requested (maintainers can squash on merge)
|
||||
|
||||
6. **Wait for merge** - maintainers will merge when ready
|
||||
|
||||
### PR Guidelines
|
||||
|
||||
- Keep PRs focused on a single feature/fix
|
||||
- Include screenshots/videos for UI changes
|
||||
- Update documentation if needed
|
||||
- Link related issues
|
||||
- Test on multiple browsers
|
||||
|
||||
## Release Process
|
||||
|
||||
### Version Numbering
|
||||
|
||||
We follow [Semantic Versioning](https://semver.org/):
|
||||
|
||||
- **Major** (8.0.0): Breaking changes, major feature overhauls
|
||||
- **Minor** (7.6.0): New features, backward-compatible
|
||||
- **Patch** (7.5.1): Bug fixes, small improvements
|
||||
|
||||
### Release Workflow (Maintainers Only)
|
||||
|
||||
#### 1. Development Phase
|
||||
|
||||
Contributors and maintainers work on `dev` branch.
|
||||
|
||||
#### 2. Create Beta Release
|
||||
|
||||
When ready for testing:
|
||||
|
||||
1. Run version bump workflow:
|
||||
```
|
||||
Actions → Version Bump → Run workflow
|
||||
- Branch: dev
|
||||
- Bump type: minor/major/patch
|
||||
- Pre-release: beta
|
||||
```
|
||||
|
||||
2. Create PR from `dev` → `beta`
|
||||
|
||||
3. Merge PR (triggers beta release workflow)
|
||||
|
||||
4. Share beta release with testers
|
||||
|
||||
5. Gather feedback and fix issues on `dev`
|
||||
|
||||
6. Repeat until stable
|
||||
|
||||
#### 3. Promote to Production
|
||||
|
||||
When beta is stable:
|
||||
|
||||
1. Create PR from `beta` → `main` with checklist:
|
||||
- [ ] Beta tested for X days
|
||||
- [ ] All critical bugs resolved
|
||||
- [ ] Y+ testers approved
|
||||
- [ ] Release notes prepared
|
||||
|
||||
2. Get 2 maintainer approvals
|
||||
|
||||
3. Merge PR (triggers production release workflow)
|
||||
|
||||
4. Workflow pauses for manual approval (10 min wait)
|
||||
|
||||
5. Approve in GitHub Actions → Environments → production
|
||||
|
||||
6. Release is created on GitHub
|
||||
|
||||
7. Manually trigger store submission:
|
||||
```
|
||||
Actions → Submit → Run workflow
|
||||
- Enter version tag (e.g., 7.6.0)
|
||||
```
|
||||
|
||||
#### 4. Emergency Hotfix
|
||||
|
||||
For critical production bugs:
|
||||
|
||||
1. Create hotfix branch from `main`:
|
||||
```bash
|
||||
git checkout main
|
||||
git pull origin main
|
||||
git checkout -b hotfix/critical-bug-fix
|
||||
```
|
||||
|
||||
2. Fix the issue and commit
|
||||
|
||||
3. Push branch:
|
||||
```bash
|
||||
git push origin hotfix/critical-bug-fix
|
||||
```
|
||||
|
||||
4. Run hotfix workflow:
|
||||
```
|
||||
Actions → Hotfix Release → Run workflow
|
||||
- Description: Brief description
|
||||
- Branch name: hotfix/critical-bug-fix
|
||||
```
|
||||
|
||||
5. Workflow will:
|
||||
- Auto-bump patch version
|
||||
- Merge to `main`
|
||||
- Create release
|
||||
- Back-merge to `beta` and `dev`
|
||||
|
||||
6. Immediately submit to stores
|
||||
|
||||
## Questions?
|
||||
|
||||
- 💬 Join our [Discord](https://discord.gg/zv8C9F8) (if available)
|
||||
- 📧 Email: [contact info]
|
||||
- 🐛 Report bugs: [GitHub Issues](https://github.com/mue/mue/issues)
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under the BSD-3-Clause License.
|
||||
11
Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM oven/bun:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN bun install
|
||||
|
||||
EXPOSE 5173
|
||||
|
||||
CMD ["bun", "run", "dev:host"]
|
||||
4
LICENSE
@@ -1,7 +1,7 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2023-2024 Kaiso One Ltd
|
||||
Copyright (c) 2019-2024 The Mue Authors
|
||||
Copyright (c) 2019-2026 The Mue Authors
|
||||
Copyright (c) 2023-2025 Kaiso One Ltd
|
||||
Copyright (c) 2018-2019 David Ralph
|
||||
|
||||
All rights reserved.
|
||||
|
||||
53
README.md
@@ -4,13 +4,18 @@
|
||||
|
||||
<p align="center">A fast, open and free-to-use browser extension that gives a new, fresh and customisable tab page to modern browsers.</p>
|
||||
|
||||
<p align="center"><i>Managed by <a href="https://kaiso.one" target="_blank">Kaiso</a> and maintained by contributors all over the world.</i></p>
|
||||
|
||||
<p align="center"><a href="https://muetab.com">muetab.com</a></p>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://chromewebstore.google.com/detail/mue/bngmbednanpcfochchhgbkookpiaiaid)
|
||||
[](https://microsoftedge.microsoft.com/addons/detail/mue/aepnglgjfokepefimhbnibfjekidhmja)
|
||||
[](https://addons.mozilla.org/en-GB/firefox/addon/mue/)
|
||||
|
||||
</div>
|
||||
|
||||
## 🤔 Why Mue?
|
||||
|
||||
## Why Mue?
|
||||
- Beautiful and Minimalist Design
|
||||
- Customisable Layout
|
||||
- Widgets (such as weather, notes, bookmarks and more)
|
||||
@@ -18,7 +23,8 @@
|
||||
- Extensible with the Mue Marketplace
|
||||
- Open Source under the BSD-3 License
|
||||
|
||||
## Installation
|
||||
## 🌶️ Installation
|
||||
|
||||
Mue can be downloaded on the following browsers:
|
||||
|
||||
- [Chrome](https://chromewebstore.google.com/detail/mue/bngmbednanpcfochchhgbkookpiaiaid)
|
||||
@@ -28,23 +34,54 @@ Mue can be downloaded on the following browsers:
|
||||
|
||||
and can be manually sideloaded on others using the files on [GitHub Releases](https://github.com/mue/mue/releases)
|
||||
|
||||
## 🚀 Demo
|
||||
|
||||
## Demo
|
||||
A fully-featured demo of the tab extension is available in-browser at [demo.muetab.com](https://demo.muetab.com)
|
||||
|
||||
## 💻 Development
|
||||
|
||||
## Development
|
||||
Install dependencies with ``bun install``, and then you can run any of the following scripts as needed:
|
||||
Install dependencies with `bun install`, and then you can run any of the following scripts as needed:
|
||||
|
||||
- `bun run dev[:host]` - start development server
|
||||
- `bun run build` - build production copy of Mue
|
||||
- `bun run lint[:fix]` - run linter
|
||||
- `bun run pretty` - run prettier
|
||||
- `bun run translations` - migrate old translation format to new
|
||||
- `bun run translations:percentages` - update translation completion percentages from Weblate
|
||||
|
||||
### Contributing
|
||||
|
||||
We welcome contributions! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on:
|
||||
- Development workflow
|
||||
- Branch strategy (dev → beta → main)
|
||||
- Commit message format
|
||||
- Pull request process
|
||||
|
||||
For maintainers, see [docs/RELEASE_PROCESS.md](docs/RELEASE_PROCESS.md) for release procedures.
|
||||
|
||||
## 🐳 Docker development
|
||||
|
||||
Hot reload is available while coding.
|
||||
|
||||
- `docker build -t mue-app .` - build the image
|
||||
|
||||
- `docker volume create dev-bun-app` - create a volume for the app
|
||||
|
||||
- `docker run -p 5173:5173 mue-app` - run the container
|
||||
|
||||
- `docker run -p 5173:5173 \
|
||||
-v $(pwd):/app \
|
||||
-v dev-bun-app:/app/node_modules \
|
||||
mue-app
|
||||
` - run the container
|
||||
|
||||
Navigate to http://localhost:5173
|
||||
|
||||
## 🌍 Translations
|
||||
|
||||
## Translations
|
||||
We use [Weblate](https://weblate.org) for translations. To get started, please visit the [project](https://hosted.weblate.org/projects/mue/) and look for the latest version to start translating Mue into your langauge.
|
||||
|
||||
## Attribution
|
||||
|
||||
[Pexels](https://pexels.com), [Unsplash](https://unsplash.com) - Stock photos used for offline mode <br/>
|
||||
[Undraw](https://undraw.co) - Welcome modal images
|
||||
|
||||
66
assets/chrome.svg
Normal file
|
After Width: | Height: | Size: 27 KiB |
71
assets/edge.svg
Normal file
|
After Width: | Height: | Size: 27 KiB |
46
assets/firefox.svg
Normal file
|
After Width: | Height: | Size: 109 KiB |
469
docs/RELEASE_PROCESS.md
Normal file
@@ -0,0 +1,469 @@
|
||||
# Mue Release Process
|
||||
|
||||
This document outlines the complete release process for Mue maintainers.
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Version Numbering](#version-numbering)
|
||||
- [Pre-Release Checklist](#pre-release-checklist)
|
||||
- [Beta Release Process](#beta-release-process)
|
||||
- [Production Release Process](#production-release-process)
|
||||
- [Hotfix Release Process](#hotfix-release-process)
|
||||
- [Post-Release Tasks](#post-release-tasks)
|
||||
- [Store Submission](#store-submission)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Overview
|
||||
|
||||
Mue uses a three-branch release workflow:
|
||||
|
||||
```
|
||||
dev → beta → main
|
||||
```
|
||||
|
||||
- **`dev`**: Active development and feature integration
|
||||
- **`beta`**: Release candidates for community testing
|
||||
- **`main`**: Production-ready stable releases
|
||||
|
||||
## Version Numbering
|
||||
|
||||
We follow [Semantic Versioning](https://semver.org/): `MAJOR.MINOR.PATCH`
|
||||
|
||||
### When to Bump
|
||||
|
||||
| Type | When | Example |
|
||||
|------|------|---------|
|
||||
| **Major** (x.0.0) | Breaking changes, API changes, major UI overhaul | 7.5.0 → 8.0.0 |
|
||||
| **Minor** (0.x.0) | New features, backward-compatible changes | 7.5.0 → 7.6.0 |
|
||||
| **Patch** (0.0.x) | Bug fixes, small improvements | 7.5.0 → 7.5.1 |
|
||||
|
||||
### Beta Versions
|
||||
|
||||
Beta versions follow the format: `MAJOR.MINOR.PATCH-beta.X`
|
||||
|
||||
Example: `7.6.0-beta.1`, `7.6.0-beta.2`
|
||||
|
||||
## Pre-Release Checklist
|
||||
|
||||
Before starting any release:
|
||||
|
||||
- [ ] All intended features/fixes are merged to `dev`
|
||||
- [ ] No critical bugs in issue tracker
|
||||
- [ ] `dev` branch builds successfully
|
||||
- [ ] All CI checks passing
|
||||
- [ ] Translation updates synced from Weblate (if applicable)
|
||||
- [ ] Breaking changes documented
|
||||
|
||||
## Beta Release Process
|
||||
|
||||
### Step 1: Version Bump
|
||||
|
||||
1. Go to **Actions** → **Version Bump** → **Run workflow**
|
||||
|
||||
2. Configure:
|
||||
- **Branch**: `dev`
|
||||
- **Bump type**: Choose `patch`, `minor`, or `major`
|
||||
- **Pre-release**: Select `beta`
|
||||
|
||||
3. Click **Run workflow**
|
||||
|
||||
4. Workflow will:
|
||||
- Calculate new version (e.g., `7.6.0-beta.1`)
|
||||
- Update all 6 version files
|
||||
- Create git tag
|
||||
- Push to `dev`
|
||||
|
||||
### Step 2: Create Beta PR
|
||||
|
||||
1. Go to **Pull Requests** → **New pull request**
|
||||
|
||||
2. Configure:
|
||||
- Base: `beta`
|
||||
- Compare: `dev`
|
||||
|
||||
3. Fill in PR template:
|
||||
- Add changelog preview
|
||||
- List major changes
|
||||
- Add testing notes
|
||||
|
||||
4. Get 2 maintainer approvals
|
||||
|
||||
### Step 3: Merge and Release
|
||||
|
||||
1. **Merge PR** to `beta` branch
|
||||
|
||||
2. **Beta Release Workflow** auto-triggers:
|
||||
- Builds extension for all browsers
|
||||
- Creates GitHub pre-release
|
||||
- Uploads Chrome/Firefox ZIPs
|
||||
- Generates changelog
|
||||
|
||||
3. **Verify release**:
|
||||
- Check [Releases page](https://github.com/mue/mue/releases)
|
||||
- Download and test ZIPs
|
||||
- Verify version numbers
|
||||
|
||||
### Step 4: Beta Testing
|
||||
|
||||
1. **Share with testers**:
|
||||
- Post release link in Discord/testing channel
|
||||
- Include installation instructions
|
||||
- Provide feedback form/issue template
|
||||
|
||||
2. **Monitor feedback**:
|
||||
- Track issues tagged with beta version
|
||||
- Prioritize critical bugs
|
||||
- Document all feedback
|
||||
|
||||
3. **Fix issues**:
|
||||
- Fix bugs on `dev` branch
|
||||
- Create new beta (repeat from Step 1)
|
||||
- Increment beta number (7.6.0-beta.2, etc.)
|
||||
|
||||
4. **Minimum beta period**: 3-7 days (depending on changes)
|
||||
|
||||
5. **Stability criteria**:
|
||||
- No P0/P1 bugs reported
|
||||
- Positive feedback from 5+ testers
|
||||
- All critical features tested
|
||||
|
||||
## Production Release Process
|
||||
|
||||
### Step 1: Pre-Production Checks
|
||||
|
||||
- [ ] Beta has been stable for minimum period
|
||||
- [ ] All critical beta bugs resolved
|
||||
- [ ] Release notes prepared
|
||||
- [ ] Store credentials verified
|
||||
- [ ] Team notified of pending release
|
||||
|
||||
### Step 2: Version Bump to Stable
|
||||
|
||||
1. Go to **Actions** → **Version Bump** → **Run workflow**
|
||||
|
||||
2. Configure:
|
||||
- **Branch**: `beta`
|
||||
- **Bump type**: Usually same as beta (minor/major/patch)
|
||||
- **Pre-release**: Leave empty (stable release)
|
||||
|
||||
3. This updates `7.6.0-beta.X` → `7.6.0`
|
||||
|
||||
### Step 3: Create Production PR
|
||||
|
||||
1. Go to **Pull Requests** → **New pull request**
|
||||
|
||||
2. Configure:
|
||||
- Base: `main`
|
||||
- Compare: `beta`
|
||||
|
||||
3. Fill in **release PR checklist**:
|
||||
- [ ] Beta tested for X days
|
||||
- [ ] All critical bugs resolved
|
||||
- [ ] 5+ beta testers approved
|
||||
- [ ] Release notes prepared
|
||||
- [ ] Store submission ready
|
||||
- [ ] Changelog updated on website
|
||||
|
||||
4. Get 2 maintainer approvals
|
||||
|
||||
### Step 4: Merge and Release
|
||||
|
||||
1. **Merge PR** to `main`
|
||||
|
||||
2. **Production Release Workflow** starts:
|
||||
- Builds extension
|
||||
- Creates production tag
|
||||
- Generates full changelog
|
||||
- Creates GitHub release
|
||||
- **Pauses for manual approval**
|
||||
|
||||
3. **Review in GitHub**:
|
||||
- Go to **Actions** → **Production Release** → running workflow
|
||||
- Review release notes
|
||||
- Check build artifacts
|
||||
- **Approve deployment** in Environments → production
|
||||
|
||||
4. **Wait 10 minutes** (cooldown period)
|
||||
|
||||
5. **Release completes**:
|
||||
- GitHub release published
|
||||
- ZIPs uploaded
|
||||
- Tag created
|
||||
|
||||
### Step 5: Store Submission
|
||||
|
||||
**Manual submission required** (for now):
|
||||
|
||||
1. Go to **Actions** → **Submit** → **Run workflow**
|
||||
|
||||
2. Enter version tag: `7.6.0` (no 'v' prefix)
|
||||
|
||||
3. Click **Run workflow**
|
||||
|
||||
4. Monitor submission workflow:
|
||||
- Chrome Web Store submission
|
||||
- Firefox Add-ons submission
|
||||
- Edge Add-ons submission
|
||||
|
||||
5. **Verify store listings**:
|
||||
- Chrome: https://chromewebstore.google.com/detail/mue/bngmbednanpcfochchhgbkookpiaiaid
|
||||
- Edge: https://microsoftedge.microsoft.com/addons/detail/mue/aepnglgjfokepefimhbnibfjekidhmja
|
||||
- Firefox: https://addons.mozilla.org/en-GB/firefox/addon/mue/
|
||||
|
||||
6. **Store review times**:
|
||||
- Chrome: 1-3 days
|
||||
- Edge: 1-2 days
|
||||
- Firefox: hours to days
|
||||
|
||||
### Step 6: Sync Branches
|
||||
|
||||
After production release, sync version to other branches:
|
||||
|
||||
```bash
|
||||
# Update dev and beta with main
|
||||
git checkout dev
|
||||
git pull origin dev
|
||||
git merge main
|
||||
git push origin dev
|
||||
|
||||
git checkout beta
|
||||
git pull origin beta
|
||||
git merge main
|
||||
git push origin beta
|
||||
```
|
||||
|
||||
## Hotfix Release Process
|
||||
|
||||
### When to Use Hotfix
|
||||
|
||||
**Only for critical production bugs:**
|
||||
- Security vulnerabilities
|
||||
- Data loss bugs
|
||||
- Extension completely broken
|
||||
- Critical functionality broken for all users
|
||||
|
||||
### Process
|
||||
|
||||
1. **Create hotfix branch** from `main`:
|
||||
```bash
|
||||
git checkout main
|
||||
git pull origin main
|
||||
git checkout -b hotfix/brief-description
|
||||
```
|
||||
|
||||
2. **Fix the bug**:
|
||||
- Make minimal changes (hotfix only)
|
||||
- Test thoroughly
|
||||
- Commit with conventional format
|
||||
|
||||
3. **Push branch**:
|
||||
```bash
|
||||
git push origin hotfix/brief-description
|
||||
```
|
||||
|
||||
4. **Run hotfix workflow**:
|
||||
- Go to **Actions** → **Hotfix Release** → **Run workflow**
|
||||
- **Description**: Brief bug description
|
||||
- **Branch name**: `hotfix/brief-description`
|
||||
- Click **Run workflow**
|
||||
|
||||
5. **Approve deployment**:
|
||||
- Workflow pauses for approval
|
||||
- Review changes carefully
|
||||
- Approve in **Environments** → **production**
|
||||
|
||||
6. **Workflow automatically**:
|
||||
- Bumps patch version (7.6.0 → 7.6.1)
|
||||
- Merges to `main`
|
||||
- Creates release tag
|
||||
- Builds and releases
|
||||
- Back-merges to `beta` and `dev`
|
||||
|
||||
7. **Submit to stores immediately**:
|
||||
- Go to **Actions** → **Submit** → **Run workflow**
|
||||
- Enter new version (e.g., `7.6.1`)
|
||||
|
||||
8. **Notify users**:
|
||||
- Post urgent update notice
|
||||
- Update website changelog
|
||||
- Notify via social media if critical
|
||||
|
||||
9. **Clean up**:
|
||||
```bash
|
||||
git push origin --delete hotfix/brief-description
|
||||
```
|
||||
|
||||
## Post-Release Tasks
|
||||
|
||||
After any production release:
|
||||
|
||||
### Immediate (within 24 hours)
|
||||
|
||||
- [ ] Verify store submissions completed
|
||||
- [ ] Update https://muetab.com/blog/changelog
|
||||
- [ ] Announce on Discord/social media
|
||||
- [ ] Monitor issue tracker for new reports
|
||||
- [ ] Verify demo site (demo.muetab.com) is updated
|
||||
|
||||
### Within 1 Week
|
||||
|
||||
- [ ] Review analytics for adoption rate
|
||||
- [ ] Address any quick-fix bugs as patch release
|
||||
- [ ] Update roadmap/milestones
|
||||
- [ ] Thank beta testers and contributors
|
||||
|
||||
### Ongoing
|
||||
|
||||
- [ ] Monitor store reviews/ratings
|
||||
- [ ] Respond to user feedback
|
||||
- [ ] Plan next release cycle
|
||||
|
||||
## Store Submission
|
||||
|
||||
### Required Credentials
|
||||
|
||||
Stored in GitHub Secrets as `SUBMIT_KEYS`:
|
||||
|
||||
```json
|
||||
{
|
||||
"chrome": {
|
||||
"extId": "bngmbednanpcfochchhgbkookpiaiaid",
|
||||
"clientId": "...",
|
||||
"clientSecret": "...",
|
||||
"refreshToken": "..."
|
||||
},
|
||||
"firefox": {
|
||||
"extId": "{ac143a20-4b61-4c81-abdd-4bff77032972}",
|
||||
"jwtIssuer": "...",
|
||||
"jwtSecret": "..."
|
||||
},
|
||||
"edge": {
|
||||
"productId": "...",
|
||||
"clientId": "...",
|
||||
"clientSecret": "...",
|
||||
"accessTokenUrl": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Beta Distribution
|
||||
|
||||
**Chrome/Edge Beta**:
|
||||
- Use unlisted listing (share link with testers)
|
||||
- Or use trusted testers group (max 1000)
|
||||
|
||||
**Firefox Beta**:
|
||||
- Upload as unlisted to AMO
|
||||
- Share download link from GitHub Releases
|
||||
|
||||
**Safari Beta**:
|
||||
- Currently manual sideload from GitHub Releases
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Build Fails
|
||||
|
||||
**Issue**: Build fails in workflow
|
||||
|
||||
**Solutions**:
|
||||
1. Check CI logs for specific error
|
||||
2. Run `bun run build` locally to reproduce
|
||||
3. Ensure all dependencies installed
|
||||
4. Check for linting errors: `bun run lint`
|
||||
|
||||
### Version Mismatch
|
||||
|
||||
**Issue**: Version numbers don't match across files
|
||||
|
||||
**Solutions**:
|
||||
1. Re-run Version Bump workflow
|
||||
2. Manually verify all 6 files:
|
||||
- package.json
|
||||
- manifest/chrome.json
|
||||
- manifest/firefox.json
|
||||
- safari/Mue Extension/Resources/manifest.json
|
||||
- safari/Mue.xcodeproj/project.pbxproj
|
||||
- src/config/constants.js
|
||||
|
||||
### Tag Already Exists
|
||||
|
||||
**Issue**: Git tag already exists for version
|
||||
|
||||
**Solutions**:
|
||||
1. Delete existing tag:
|
||||
```bash
|
||||
git tag -d v7.6.0
|
||||
git push origin :refs/tags/v7.6.0
|
||||
```
|
||||
2. Re-run workflow
|
||||
|
||||
### Store Submission Fails
|
||||
|
||||
**Issue**: PlasmoHQ BPP submission fails
|
||||
|
||||
**Solutions**:
|
||||
1. Check workflow logs for specific error
|
||||
2. Verify credentials in `SUBMIT_KEYS` secret
|
||||
3. Check store developer console for issues
|
||||
4. Try manual submission as fallback
|
||||
|
||||
### Merge Conflicts
|
||||
|
||||
**Issue**: Conflicts when merging beta → main
|
||||
|
||||
**Solutions**:
|
||||
1. Update beta with main first:
|
||||
```bash
|
||||
git checkout beta
|
||||
git merge main
|
||||
git push origin beta
|
||||
```
|
||||
2. Create new PR from beta → main
|
||||
|
||||
## Emergency Rollback
|
||||
|
||||
If a production release has critical bugs:
|
||||
|
||||
### Option 1: Hotfix (Preferred)
|
||||
|
||||
Follow [Hotfix Process](#hotfix-release-process) to quickly patch and release.
|
||||
|
||||
### Option 2: Store Rollback
|
||||
|
||||
Each store allows rolling back to previous version:
|
||||
|
||||
**Chrome Web Store**:
|
||||
1. Go to Developer Dashboard
|
||||
2. Select Mue extension
|
||||
3. Package → Select previous version
|
||||
4. Publish
|
||||
|
||||
**Firefox Add-ons**:
|
||||
1. Go to Developer Hub
|
||||
2. Select Mue add-on
|
||||
3. Manage Status & Versions
|
||||
4. Enable previous version
|
||||
|
||||
**Edge Add-ons**:
|
||||
1. Go to Partner Center
|
||||
2. Select Mue extension
|
||||
3. Packages → Restore previous
|
||||
|
||||
### Option 3: Revert and Re-release
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git revert <commit-hash>
|
||||
git push origin main
|
||||
```
|
||||
|
||||
Then follow production release process.
|
||||
|
||||
## Questions?
|
||||
|
||||
Contact maintainers:
|
||||
- @davidcralph
|
||||
- @alexsparkes
|
||||
|
||||
Or open a discussion: https://github.com/mue/mue/discussions
|
||||
84
eslint.config.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import js from '@eslint/js';
|
||||
import react from 'eslint-plugin-react';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import jsxA11y from 'eslint-plugin-jsx-a11y';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
|
||||
export default [
|
||||
// Ignore patterns
|
||||
{
|
||||
ignores: [
|
||||
'**/node_modules/**',
|
||||
'**/build/**',
|
||||
'**/dist/**',
|
||||
'**/coverage/**',
|
||||
'**/*.scss',
|
||||
'**/*.css',
|
||||
'**/*.json',
|
||||
],
|
||||
},
|
||||
|
||||
// Base config for all JS/JSX files
|
||||
{
|
||||
files: ['**/*.{js,jsx,mjs}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2024,
|
||||
sourceType: 'module',
|
||||
parserOptions: { ecmaFeatures: { jsx: true } },
|
||||
globals: {
|
||||
// Browser globals
|
||||
window: 'readonly',
|
||||
document: 'readonly',
|
||||
navigator: 'readonly',
|
||||
console: 'readonly',
|
||||
localStorage: 'readonly',
|
||||
fetch: 'readonly',
|
||||
setTimeout: 'readonly',
|
||||
setInterval: 'readonly',
|
||||
clearInterval: 'readonly',
|
||||
clearTimeout: 'readonly',
|
||||
Image: 'readonly',
|
||||
FileReader: 'readonly',
|
||||
Blob: 'readonly',
|
||||
URL: 'readonly',
|
||||
TextEncoder: 'readonly',
|
||||
TextDecoder: 'readonly',
|
||||
DOMParser: 'readonly',
|
||||
CustomEvent: 'readonly',
|
||||
AbortController: 'readonly',
|
||||
btoa: 'readonly',
|
||||
atob: 'readonly',
|
||||
// Node globals for scripts
|
||||
process: 'readonly',
|
||||
__dirname: 'readonly',
|
||||
__filename: 'readonly',
|
||||
module: 'readonly',
|
||||
require: 'readonly',
|
||||
},
|
||||
},
|
||||
plugins: { react, 'react-hooks': reactHooks, 'jsx-a11y': jsxA11y },
|
||||
settings: { react: { version: 'detect' } },
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...react.configs.recommended.rules,
|
||||
...react.configs['jsx-runtime'].rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
|
||||
// React specific rules
|
||||
'react/prop-types': 'off', // Using PropTypes is optional
|
||||
'react/react-in-jsx-scope': 'off', // Not needed with React 17+
|
||||
'react/jsx-uses-react': 'off', // Not needed with React 17+
|
||||
|
||||
// General rules
|
||||
'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
||||
|
||||
// Modern JS
|
||||
'prefer-const': 'warn',
|
||||
'no-var': 'error',
|
||||
},
|
||||
},
|
||||
|
||||
// Prettier config (must be last to override other formatting rules)
|
||||
prettier,
|
||||
];
|
||||
@@ -1,9 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"baseUrl": "src"
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"baseUrl": "src",
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"allowImportingTsExtensions": false,
|
||||
"checkJs": false,
|
||||
"paths": {
|
||||
"@/*": ["./*"],
|
||||
"i18n/*": ["./i18n/*"],
|
||||
"components/*": ["./components/*"],
|
||||
"assets/*": ["./assets/*"],
|
||||
"config/*": ["./config/*"],
|
||||
"features/*": ["./features/*"],
|
||||
"lib/*": ["./lib/*"],
|
||||
"scss/*": ["./scss/*"],
|
||||
"translations/*": ["./i18n/locales/*"],
|
||||
"utils/*": ["./utils/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "build"]
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
"default_locale": "en",
|
||||
"name": "__MSG_name__",
|
||||
"description": "__MSG_description__",
|
||||
"version": "7.1.2",
|
||||
"version": "7.6.0",
|
||||
"homepage_url": "https://muetab.com",
|
||||
"permissions": ["search"],
|
||||
"action": {
|
||||
"default_icon": "icons/128x128.png"
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "Mue",
|
||||
"description": "Fast, open and free-to-use new tab page for modern browsers.",
|
||||
"version": "7.1.2",
|
||||
"version": "7.6.0",
|
||||
"homepage_url": "https://muetab.com",
|
||||
"action": {
|
||||
"default_icon": "icons/128x128.png"
|
||||
|
||||
76
package.json
@@ -9,56 +9,66 @@
|
||||
"homepage": "https://muetab.com",
|
||||
"bugs": "https://github.com/mue/mue/issues/new?assignees=&labels=bug&template=bug-report.md&title=%5BBUG%5D",
|
||||
"license": "BSD-3-Clause",
|
||||
"version": "7.1.2",
|
||||
"version": "7.6.0",
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.3.1",
|
||||
"engines": {
|
||||
"node": ">=20.0.0",
|
||||
"bun": ">=1.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@eartharoid/i18n": "1.2.1",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@floating-ui/react-dom": "2.1.1",
|
||||
"@floating-ui/react-dom": "2.1.8",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@fontsource/lexend-deca": "5.0.14",
|
||||
"@fontsource/montserrat": "5.0.19",
|
||||
"@muetab/react-sortable-hoc": "^2.0.1",
|
||||
"@mui/material": "6.1.0",
|
||||
"@sentry/react": "^8.30.0",
|
||||
"embla-carousel-autoplay": "8.2.1",
|
||||
"embla-carousel-react": "8.3.0",
|
||||
"@sentry/react": "^10.36.0",
|
||||
"blurhash": "^2.0.5",
|
||||
"fast-blurhash": "^1.1.4",
|
||||
"image-conversion": "^2.1.1",
|
||||
"react": "^18.3.1",
|
||||
"react-best-gradient-color-picker": "^3.0.10",
|
||||
"react-clock": "5.0.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-modal": "3.16.1",
|
||||
"react-toastify": "10.0.5",
|
||||
"use-debounce": "^10.0.3"
|
||||
"react": "^19.2.3",
|
||||
"react-best-gradient-color-picker": "^3.0.14",
|
||||
"react-clock": "6.0.0",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-modal": "3.16.3",
|
||||
"react-toastify": "11.0.5",
|
||||
"use-debounce": "^10.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.5.0",
|
||||
"@commitlint/config-conventional": "^19.5.0",
|
||||
"@commitlint/cli": "^20.3.1",
|
||||
"@commitlint/config-conventional": "^20.3.1",
|
||||
"@eartharoid/deep-merge": "^0.0.2",
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"adm-zip": "0.5.16",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"husky": "^9.1.6",
|
||||
"prettier": "^3.3.3",
|
||||
"sass": "^1.78.0",
|
||||
"stylelint": "^16.9.0",
|
||||
"stylelint-config-standard-scss": "^13.1.0",
|
||||
"stylelint-scss": "^6.6.0",
|
||||
"vite": "5.4.4",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@vitejs/plugin-react-swc": "^4.2.2",
|
||||
"adm-zip": "0.5.17",
|
||||
"eslint": "^10.2.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"husky": "^9.1.7",
|
||||
"prettier": "^3.8.1",
|
||||
"sass": "^1.97.3",
|
||||
"stylelint": "^17.0.0",
|
||||
"stylelint-config-standard-scss": "^17.0.0",
|
||||
"stylelint-scss": "^7.0.0",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-progress": "^0.0.7"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev:host": "vite --host",
|
||||
"translations": "cd scripts && node updatetranslations.js",
|
||||
"translations": "cd scripts && node updatetranslations.cjs",
|
||||
"translations:percentages": "node scripts/updateTranslationPercentages.cjs",
|
||||
"build": "vite build",
|
||||
"pretty": "prettier --write \"./**/*.{js,jsx,json,scss,css}\"",
|
||||
"lint": "eslint \"./src/**/*.{js,jsx}\" && stylelint \"./src/**/*.{scss,css}\"",
|
||||
"lint:fix": "eslint \"./src/**/*.{js,jsx}\" --fix && stylelint \"./src/**/*.{scss,css}\" --fix",
|
||||
"postinstall": "husky"
|
||||
"postinstall": "husky",
|
||||
"prepare": "husky"
|
||||
}
|
||||
}
|
||||
|
||||
7265
pnpm-lock.yaml
generated
13
safari/Mue Extension/Info.plist
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.Safari.web-extension</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SafariWebExtensionHandler</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
23
safari/Mue Extension/Resources/manifest.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"offline_enabled": true,
|
||||
"default_locale": "en",
|
||||
"name": "__MSG_name__",
|
||||
"description": "__MSG_description__",
|
||||
"version": "7.6.0",
|
||||
"homepage_url": "https://muetab.com",
|
||||
"permissions": ["search"],
|
||||
"chrome_url_overrides": {
|
||||
"newtab": "index.html"
|
||||
},
|
||||
"icons": {
|
||||
"16": "icons/16x16.png",
|
||||
"48": "icons/48x48.png",
|
||||
"128": "icons/128x128.png"
|
||||
},
|
||||
"background": {
|
||||
"scripts": ["background.js"],
|
||||
"type": "module",
|
||||
"persistent": false
|
||||
}
|
||||
}
|
||||
42
safari/Mue Extension/SafariWebExtensionHandler.swift
Normal file
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// SafariWebExtensionHandler.swift
|
||||
// Mue Extension
|
||||
//
|
||||
// Created by David Ralph on 02/01/2026.
|
||||
//
|
||||
|
||||
import SafariServices
|
||||
import os.log
|
||||
|
||||
class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
|
||||
|
||||
func beginRequest(with context: NSExtensionContext) {
|
||||
let request = context.inputItems.first as? NSExtensionItem
|
||||
|
||||
let profile: UUID?
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
profile = request?.userInfo?[SFExtensionProfileKey] as? UUID
|
||||
} else {
|
||||
profile = request?.userInfo?["profile"] as? UUID
|
||||
}
|
||||
|
||||
let message: Any?
|
||||
if #available(iOS 15.0, macOS 11.0, *) {
|
||||
message = request?.userInfo?[SFExtensionMessageKey]
|
||||
} else {
|
||||
message = request?.userInfo?["message"]
|
||||
}
|
||||
|
||||
os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@ (profile: %@)", String(describing: message), profile?.uuidString ?? "none")
|
||||
|
||||
let response = NSExtensionItem()
|
||||
if #available(iOS 15.0, macOS 11.0, *) {
|
||||
response.userInfo = [ SFExtensionMessageKey: [ "echo": message ] ]
|
||||
} else {
|
||||
response.userInfo = [ "message": [ "echo": message ] ]
|
||||
}
|
||||
|
||||
context.completeRequest(returningItems: [ response ], completionHandler: nil)
|
||||
}
|
||||
|
||||
}
|
||||
541
safari/Mue.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,541 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
6373522A2F087B5E002EF039 /* Mue Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 637352292F087B5E002EF039 /* Mue Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
6373522B2F087B5E002EF039 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 637352072F087B5D002EF039 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 637352282F087B5E002EF039;
|
||||
remoteInfo = "Mue Extension";
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
637352482F087B5E002EF039 /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
6373522A2F087B5E002EF039 /* Mue Extension.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed Foundation Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
6373520F2F087B5D002EF039 /* Mue.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mue.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
637352292F087B5E002EF039 /* Mue Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Mue Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
637352422F087B5E002EF039 /* Exceptions for "Mue Extension" folder in "Mue Extension" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
);
|
||||
target = 637352282F087B5E002EF039 /* Mue Extension */;
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
637352112F087B5D002EF039 /* Mue */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = Mue;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6373522D2F087B5E002EF039 /* Mue Extension */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
637352422F087B5E002EF039 /* Exceptions for "Mue Extension" folder in "Mue Extension" target */,
|
||||
);
|
||||
explicitFolders = (
|
||||
Resources/_locales,
|
||||
Resources/assets,
|
||||
Resources/icons,
|
||||
Resources/src,
|
||||
);
|
||||
path = "Mue Extension";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
6373520C2F087B5D002EF039 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
637352262F087B5E002EF039 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
637352062F087B5D002EF039 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
637352112F087B5D002EF039 /* Mue */,
|
||||
6373522D2F087B5E002EF039 /* Mue Extension */,
|
||||
637352102F087B5D002EF039 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
637352102F087B5D002EF039 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6373520F2F087B5D002EF039 /* Mue.app */,
|
||||
637352292F087B5E002EF039 /* Mue Extension.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
6373520E2F087B5D002EF039 /* Mue */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 637352492F087B5E002EF039 /* Build configuration list for PBXNativeTarget "Mue" */;
|
||||
buildPhases = (
|
||||
6373520B2F087B5D002EF039 /* Sources */,
|
||||
6373520C2F087B5D002EF039 /* Frameworks */,
|
||||
6373520D2F087B5D002EF039 /* Resources */,
|
||||
637352482F087B5E002EF039 /* Embed Foundation Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
6373522C2F087B5E002EF039 /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
637352112F087B5D002EF039 /* Mue */,
|
||||
);
|
||||
name = Mue;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = Mue;
|
||||
productReference = 6373520F2F087B5D002EF039 /* Mue.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
637352282F087B5E002EF039 /* Mue Extension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 637352432F087B5E002EF039 /* Build configuration list for PBXNativeTarget "Mue Extension" */;
|
||||
buildPhases = (
|
||||
637352252F087B5E002EF039 /* Sources */,
|
||||
637352262F087B5E002EF039 /* Frameworks */,
|
||||
637352272F087B5E002EF039 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
6373522D2F087B5E002EF039 /* Mue Extension */,
|
||||
);
|
||||
name = "Mue Extension";
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = "Mue Extension";
|
||||
productReference = 637352292F087B5E002EF039 /* Mue Extension.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
637352072F087B5D002EF039 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 2620;
|
||||
LastUpgradeCheck = 2620;
|
||||
TargetAttributes = {
|
||||
6373520E2F087B5D002EF039 = {
|
||||
CreatedOnToolsVersion = 26.2;
|
||||
};
|
||||
637352282F087B5E002EF039 = {
|
||||
CreatedOnToolsVersion = 26.2;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 6373520A2F087B5D002EF039 /* Build configuration list for PBXProject "Mue" */;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 637352062F087B5D002EF039;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 637352102F087B5D002EF039 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
6373520E2F087B5D002EF039 /* Mue */,
|
||||
637352282F087B5E002EF039 /* Mue Extension */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
6373520D2F087B5D002EF039 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
637352272F087B5E002EF039 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
6373520B2F087B5D002EF039 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
637352252F087B5E002EF039 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
6373522C2F087B5E002EF039 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 637352282F087B5E002EF039 /* Mue Extension */;
|
||||
targetProxy = 6373522B2F087B5E002EF039 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
637352442F087B5E002EF039 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_USER_SELECTED_FILES = readonly;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Mue Extension/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Mue Extension";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright (c) 2019-2026 The Mue Authors";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 7.6.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = mueauthors.mue.extension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
637352452F087B5E002EF039 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_USER_SELECTED_FILES = readonly;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Mue Extension/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Mue Extension";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright (c) 2019-2026 The Mue Authors";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 7.6.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = mueauthors.mue.extension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
637352462F087B5E002EF039 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
637352472F087B5E002EF039 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
6373524A2F087B5E002EF039 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||
ENABLE_USER_SELECTED_FILES = readonly;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Mue;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright (c) 2019-2026 The Mue Authors";
|
||||
INFOPLIST_KEY_NSMainStoryboardFile = Main;
|
||||
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 7.6.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
"-framework",
|
||||
WebKit,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = mueauthors.mue;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
6373524B2F087B5E002EF039 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||
ENABLE_USER_SELECTED_FILES = readonly;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Mue;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright (c) 2019-2026 The Mue Authors";
|
||||
INFOPLIST_KEY_NSMainStoryboardFile = Main;
|
||||
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 7.6.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
"-framework",
|
||||
WebKit,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = mueauthors.mue;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
6373520A2F087B5D002EF039 /* Build configuration list for PBXProject "Mue" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
637352462F087B5E002EF039 /* Debug */,
|
||||
637352472F087B5E002EF039 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
637352432F087B5E002EF039 /* Build configuration list for PBXNativeTarget "Mue Extension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
637352442F087B5E002EF039 /* Debug */,
|
||||
637352452F087B5E002EF039 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
637352492F087B5E002EF039 /* Build configuration list for PBXNativeTarget "Mue" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
6373524A2F087B5E002EF039 /* Debug */,
|
||||
6373524B2F087B5E002EF039 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 637352072F087B5D002EF039 /* Project object */;
|
||||
}
|
||||
7
safari/Mue.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
21
safari/Mue/AppDelegate.swift
Normal file
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// Mue
|
||||
//
|
||||
// Created by David Ralph on 02/01/2026.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
@main
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
// Override point for customization after application launch.
|
||||
}
|
||||
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
68
safari/Mue/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon_16x16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_16x16@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_32x32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_32x32@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_128x128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_128x128@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_256x256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_256x256@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_512x512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_512x512@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
safari/Mue/Assets.xcassets/AppIcon.appiconset/icon_128x128.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 56 KiB |
BIN
safari/Mue/Assets.xcassets/AppIcon.appiconset/icon_16x16.png
Normal file
|
After Width: | Height: | Size: 804 B |
BIN
safari/Mue/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
safari/Mue/Assets.xcassets/AppIcon.appiconset/icon_256x256.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 142 KiB |
BIN
safari/Mue/Assets.xcassets/AppIcon.appiconset/icon_32x32.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
safari/Mue/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
safari/Mue/Assets.xcassets/AppIcon.appiconset/icon_512x512.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 332 KiB |
6
safari/Mue/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
20
safari/Mue/Assets.xcassets/LargeIcon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
124
safari/Mue/Base.lproj/Main.storyboard
Normal file
@@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="24506" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24506"/>
|
||||
<plugIn identifier="com.apple.WebKit2IBPlugin" version="24506"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Application-->
|
||||
<scene sceneID="JPo-4y-FX3">
|
||||
<objects>
|
||||
<application id="hnw-xV-0zn" sceneMemberID="viewController">
|
||||
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||
<items>
|
||||
<menuItem title="Mue" id="1Xt-HY-uBw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Mue" systemMenu="apple" id="uQy-DD-JDr">
|
||||
<items>
|
||||
<menuItem title="About Mue" id="5kV-Vb-QxS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
|
||||
<menuItem title="Hide Mue" keyEquivalent="h" id="Olw-nP-bQN">
|
||||
<connections>
|
||||
<action selector="hide:" target="Ady-hI-5gd" id="PnN-Uc-m68"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="hideOtherApplications:" target="Ady-hI-5gd" id="VT4-aY-XCT"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Show All" id="Kd2-mp-pUS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="unhideAllApplications:" target="Ady-hI-5gd" id="Dhg-Le-xox"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
|
||||
<menuItem title="Quit Mue" keyEquivalent="q" id="4sb-4s-VLi">
|
||||
<connections>
|
||||
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Help" id="wpr-3q-Mcd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
|
||||
<items>
|
||||
<menuItem title="Mue Help" keyEquivalent="?" id="FKE-Sm-Kum">
|
||||
<connections>
|
||||
<action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
|
||||
</connections>
|
||||
</application>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Mue" customModuleProvider="target"/>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="76" y="-134"/>
|
||||
</scene>
|
||||
<!--Window Controller-->
|
||||
<scene sceneID="R2V-B0-nI4">
|
||||
<objects>
|
||||
<windowController showSeguePresentationStyle="single" id="B8D-0N-5wS" sceneMemberID="viewController">
|
||||
<window key="window" title="Mue" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" releasedWhenClosed="NO" animationBehavior="default" id="IQv-IB-iLA">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
|
||||
<windowCollectionBehavior key="collectionBehavior" fullScreenNone="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="600" height="500"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="B8D-0N-5wS" id="98r-iN-zZc"/>
|
||||
</connections>
|
||||
</window>
|
||||
<connections>
|
||||
<segue destination="XfG-lQ-9wD" kind="relationship" relationship="window.shadowedContentViewController" id="cq2-FE-JQM"/>
|
||||
</connections>
|
||||
</windowController>
|
||||
<customObject id="Oky-zY-oP4" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="250"/>
|
||||
</scene>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="hIz-AP-VOD">
|
||||
<objects>
|
||||
<viewController id="XfG-lQ-9wD" customClass="ViewController" customModule="Mue" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" id="m2S-Jp-Qdl">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="500"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<wkWebView wantsLayer="YES" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="eOr-cG-IQY">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="500"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<wkWebViewConfiguration key="configuration">
|
||||
<audiovisualMediaTypes key="mediaTypesRequiringUserActionForPlayback" none="YES"/>
|
||||
<wkPreferences key="preferences"/>
|
||||
</wkWebViewConfiguration>
|
||||
</wkWebView>
|
||||
</subviews>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="webView" destination="eOr-cG-IQY" id="GFe-mU-dBY"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="655"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
31
safari/Mue/Resources/Base.lproj/Main.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self' https://fonts.googleapis.com https://fonts.gstatic.com; style-src 'self' https://fonts.googleapis.com; font-src https://fonts.gstatic.com">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Lexend+Deca:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../Style.css">
|
||||
<script src="../Script.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="gradient-bg"></div>
|
||||
<div class="content">
|
||||
<img src="../Icon.png" width="128" height="128" alt="Mue Icon">
|
||||
<h1 class="title">Mue</h1>
|
||||
<p class="subtitle">Fast, open and free-to-use new tab page</p>
|
||||
|
||||
<div class="status-container">
|
||||
<p class="state-unknown">Enable Mue in Safari Extensions preferences to get started.</p>
|
||||
<p class="state-on">✓ Extension is enabled and ready to use!</p>
|
||||
<p class="state-off">Extension is disabled. Enable it in Safari Extensions preferences.</p>
|
||||
</div>
|
||||
|
||||
<button class="open-preferences">Open Safari Extensions Preferences</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
BIN
safari/Mue/Resources/Icon.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
22
safari/Mue/Resources/Script.js
Normal file
@@ -0,0 +1,22 @@
|
||||
function show(enabled, useSettingsInsteadOfPreferences) {
|
||||
if (useSettingsInsteadOfPreferences) {
|
||||
document.getElementsByClassName('state-on')[0].innerText = "Extension is enabled and ready to use!";
|
||||
document.getElementsByClassName('state-off')[0].innerText = "Extension is disabled. Enable it in Safari Settings.";
|
||||
document.getElementsByClassName('state-unknown')[0].innerText = "Enable Mue in Safari Settings to get started.";
|
||||
document.getElementsByClassName('open-preferences')[0].innerText = "Open Safari Settings";
|
||||
}
|
||||
|
||||
if (typeof enabled === "boolean") {
|
||||
document.body.classList.toggle(`state-on`, enabled);
|
||||
document.body.classList.toggle(`state-off`, !enabled);
|
||||
} else {
|
||||
document.body.classList.remove(`state-on`);
|
||||
document.body.classList.remove(`state-off`);
|
||||
}
|
||||
}
|
||||
|
||||
function openPreferences() {
|
||||
webkit.messageHandlers.controller.postMessage("open-preferences");
|
||||
}
|
||||
|
||||
document.querySelector("button.open-preferences").addEventListener("click", openPreferences);
|
||||
124
safari/Mue/Resources/Style.css
Normal file
@@ -0,0 +1,124 @@
|
||||
* {
|
||||
-webkit-user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
cursor: default;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--spacing: 20px;
|
||||
--text-color: #ffffff;
|
||||
--shadow: 0 4px 20px rgb(0 0 0 / 30%);
|
||||
--background-color: #0A0A0A;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
font-family: 'Lexend Deca', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
}
|
||||
|
||||
.gradient-bg {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--background-color);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--spacing) * 0.5);
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
color: var(--text-color);
|
||||
padding: var(--spacing);
|
||||
}
|
||||
|
||||
img {
|
||||
filter: drop-shadow(var(--shadow));
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 3em;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.2em;
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
opacity: 0.95;
|
||||
text-shadow: 0 1px 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.status-container {
|
||||
margin: 10px 0;
|
||||
min-height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.status-container p {
|
||||
font-size: 1.1em;
|
||||
margin: 0;
|
||||
padding: 12px 24px;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
body:not(.state-on, .state-off) :is(.state-on, .state-off) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.state-on :is(.state-off, .state-unknown) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.state-off :is(.state-on, .state-unknown) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
padding: 12px 28px;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: var(--shadow);
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #f0f0f0;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 25px rgb(0 0 0 / 40%);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
57
safari/Mue/ViewController.swift
Normal file
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// ViewController.swift
|
||||
// Mue
|
||||
//
|
||||
// Created by David Ralph on 02/01/2026.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import SafariServices
|
||||
import WebKit
|
||||
|
||||
let extensionBundleIdentifier = "mueauthors.mue.Extension"
|
||||
|
||||
class ViewController: NSViewController, WKNavigationDelegate, WKScriptMessageHandler {
|
||||
|
||||
@IBOutlet var webView: WKWebView!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
self.webView.navigationDelegate = self
|
||||
|
||||
self.webView.configuration.userContentController.add(self, name: "controller")
|
||||
|
||||
self.webView.loadFileURL(Bundle.main.url(forResource: "Main", withExtension: "html")!, allowingReadAccessTo: Bundle.main.resourceURL!)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
SFSafariExtensionManager.getStateOfSafariExtension(withIdentifier: extensionBundleIdentifier) { (state, error) in
|
||||
guard let state = state, error == nil else {
|
||||
// Insert code to inform the user that something went wrong.
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if #available(macOS 13, *) {
|
||||
webView.evaluateJavaScript("show(\(state.isEnabled), true)")
|
||||
} else {
|
||||
webView.evaluateJavaScript("show(\(state.isEnabled), false)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
if (message.body as! String != "open-preferences") {
|
||||
return;
|
||||
}
|
||||
|
||||
SFSafariApplication.showPreferencesForExtension(withIdentifier: extensionBundleIdentifier) { error in
|
||||
DispatchQueue.main.async {
|
||||
NSApplication.shared.terminate(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
91
scripts/updateTranslationPercentages.cjs
Normal file
@@ -0,0 +1,91 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const https = require('https');
|
||||
|
||||
// Language code mappings between Weblate and Mue
|
||||
const CODE_MAPPINGS = {
|
||||
de: 'de_DE',
|
||||
id: 'id_ID',
|
||||
nb_NO: 'no',
|
||||
tr: 'tr_TR',
|
||||
zh_Hans: 'zh_CN',
|
||||
pt: 'pt_PT',
|
||||
};
|
||||
|
||||
const WEBLATE_API_URL = 'https://hosted.weblate.org/api/components/mue/mue-tab-7-1/statistics/';
|
||||
|
||||
function fetchWeblateStats() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
headers: {
|
||||
'User-Agent': 'Mue-Translation-Update-Script',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
https
|
||||
.get(WEBLATE_API_URL, options, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
resolve(json);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
})
|
||||
.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function mapLanguageCode(weblateCode) {
|
||||
return CODE_MAPPINGS[weblateCode] || weblateCode;
|
||||
}
|
||||
|
||||
async function updateTranslationPercentages() {
|
||||
try {
|
||||
console.log('Fetching translation statistics from Weblate...');
|
||||
const data = await fetchWeblateStats();
|
||||
|
||||
const percentages = {};
|
||||
|
||||
data.results.forEach((lang) => {
|
||||
const code = mapLanguageCode(lang.code);
|
||||
percentages[code] = {
|
||||
percent: Math.round(lang.translated_percent * 10) / 10, // Round to 1 decimal place
|
||||
lastChange: lang.last_change,
|
||||
};
|
||||
});
|
||||
|
||||
const outputPath = path.join(__dirname, '../src/i18n/translationPercentages.json');
|
||||
fs.writeFileSync(outputPath, JSON.stringify(percentages, null, 2));
|
||||
fs.appendFileSync(outputPath, '\n');
|
||||
|
||||
console.log(`✓ Translation percentages updated successfully!`);
|
||||
console.log(` Total languages: ${Object.keys(percentages).length}`);
|
||||
console.log(` Output: ${outputPath}`);
|
||||
|
||||
// Show some examples
|
||||
const sortedByPercent = Object.entries(percentages)
|
||||
.sort((a, b) => b[1].percent - a[1].percent)
|
||||
.slice(0, 5);
|
||||
|
||||
console.log('\nTop 5 translated languages:');
|
||||
sortedByPercent.forEach(([code, data]) => {
|
||||
console.log(` ${code}: ${data.percent}%`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating translation percentages:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
updateTranslationPercentages();
|
||||
@@ -6,6 +6,7 @@ import Modals from 'features/misc/modals/Modals';
|
||||
import { loadSettings, moveSettings } from 'utils/settings';
|
||||
import EventBus from 'utils/eventbus';
|
||||
import variables from 'config/variables';
|
||||
import { TranslationProvider } from 'contexts/TranslationContext';
|
||||
|
||||
const useAppSetup = () => {
|
||||
useEffect(() => {
|
||||
@@ -54,8 +55,10 @@ const App = () => {
|
||||
|
||||
useAppSetup();
|
||||
|
||||
const languagecode = localStorage.getItem('language') || 'en_GB';
|
||||
|
||||
return (
|
||||
<>
|
||||
<TranslationProvider initialLanguage={languagecode}>
|
||||
{showBackground && <Background />}
|
||||
<ToastContainer
|
||||
position="top-center"
|
||||
@@ -68,7 +71,7 @@ const App = () => {
|
||||
<Widgets />
|
||||
<Modals />
|
||||
</div>
|
||||
</>
|
||||
</TranslationProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 308 KiB After Width: | Height: | Size: 274 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 274 KiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 118 KiB |
@@ -1,7 +1,6 @@
|
||||
import variables from 'config/variables';
|
||||
|
||||
import { useState, memo } from 'react';
|
||||
import { TextareaAutosize } from '@mui/material';
|
||||
import { MdAddLink, MdClose } from 'react-icons/md';
|
||||
import { Tooltip } from 'components/Elements';
|
||||
import { Button } from 'components/Elements';
|
||||
@@ -26,22 +25,24 @@ function AddModal({ urlError, iconError, addLink, closeModal, edit, editData, ed
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="quicklinkModalTextbox">
|
||||
<TextareaAutosize
|
||||
maxRows={1}
|
||||
<input
|
||||
type="text"
|
||||
className="text-field-input"
|
||||
placeholder={variables.getMessage('widgets.quicklinks.name')}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value.replace(/(\r\n|\n|\r)/gm, ''))}
|
||||
style={{ gridColumn: 'span 2' }}
|
||||
/>
|
||||
<TextareaAutosize
|
||||
maxRows={10}
|
||||
<input
|
||||
type="text"
|
||||
className="text-field-input"
|
||||
placeholder={variables.getMessage('widgets.quicklinks.url')}
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value.replace(/(\r\n|\n|\r)/gm, ''))}
|
||||
/>
|
||||
<TextareaAutosize
|
||||
maxRows={10}
|
||||
maxLines={1}
|
||||
<input
|
||||
type="text"
|
||||
className="text-field-input"
|
||||
placeholder={variables.getMessage('widgets.quicklinks.icon')}
|
||||
value={icon}
|
||||
onChange={(e) => setIcon(e.target.value.replace(/(\r\n|\n|\r)/gm, ''))}
|
||||
|
||||
@@ -3,7 +3,7 @@ import Tooltip from 'components/Elements/Tooltip/Tooltip';
|
||||
|
||||
const Button = forwardRef(
|
||||
(
|
||||
{ icon, label, type, iconPlacement, onClick, active, disabled, tooltipTitle, tooltipKey, href },
|
||||
{ icon, label, type, iconPlacement, onClick, active, disabled, tooltipTitle, tooltipKey, href, style },
|
||||
ref,
|
||||
) => {
|
||||
let className;
|
||||
@@ -43,14 +43,17 @@ const Button = forwardRef(
|
||||
}
|
||||
|
||||
const button = (
|
||||
<button className={className} onClick={onClick} ref={ref} disabled={disabled}>
|
||||
<button className={className} onClick={onClick} ref={ref} disabled={disabled} style={style}>
|
||||
{icon}
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
|
||||
const linkButton = (
|
||||
<a className={className} onClick={onClick} ref={ref} disabled={disabled} href={href}>
|
||||
<a className={className} onClick={onClick} ref={ref} disabled={disabled} href={href} style={style}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
</a>
|
||||
@@ -66,6 +69,7 @@ const Button = forwardRef(
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={style}
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
@@ -90,4 +94,6 @@ const Button = forwardRef(
|
||||
},
|
||||
);
|
||||
|
||||
Button.displayName = 'Button';
|
||||
|
||||
export { Button as default, Button };
|
||||
|
||||
@@ -1,64 +1,264 @@
|
||||
import { Suspense, lazy, useState, memo, useEffect } from 'react';
|
||||
import variables from 'config/variables';
|
||||
import { Suspense, lazy, useState, memo } from 'react';
|
||||
import { MdClose } from 'react-icons/md';
|
||||
|
||||
import './scss/index.scss';
|
||||
import { Tooltip } from 'components/Elements';
|
||||
import ModalLoader from './components/ModalLoader';
|
||||
import ModalTopBar from './components/ModalTopBar';
|
||||
import { TAB_TYPES } from './constants/tabConfig';
|
||||
import { updateHash, parseDeepLink } from 'utils/deepLinking';
|
||||
|
||||
const Settings = lazy(() => import('../../../features/misc/views/Settings'));
|
||||
const Addons = lazy(() => import('../../../features/misc/views/Addons'));
|
||||
const Marketplace = lazy(() => import('../../../features/misc/views/Marketplace'));
|
||||
const Library = lazy(() => import('../../../features/misc/views/Library'));
|
||||
const Discover = lazy(() => import('../../../features/misc/views/Discover'));
|
||||
|
||||
const renderLoader = () => (
|
||||
<div style={{ display: 'flex', width: '100%', minHeight: '100%' }}>
|
||||
<div className="modalSidebar">
|
||||
<span className="mainTitle">Mue</span>
|
||||
</div>
|
||||
<div className="modalTabContent">
|
||||
<div className="emptyItems">
|
||||
<div className="emptyMessage">
|
||||
<div className="loaderHolder">
|
||||
<div id="loader"></div>
|
||||
<span className="subtitle">{variables.getMessage('modals.main.loading')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
// Map tab types to their corresponding components
|
||||
const TAB_COMPONENTS = {
|
||||
[TAB_TYPES.SETTINGS]: Settings,
|
||||
[TAB_TYPES.LIBRARY]: Library,
|
||||
[TAB_TYPES.DISCOVER]: Discover,
|
||||
};
|
||||
|
||||
function MainModal({ modalClose }) {
|
||||
const [currentTab, setCurrentTab] = useState('settings');
|
||||
function MainModal({ modalClose, deepLinkData }) {
|
||||
// Initialize with deep link tab if provided, otherwise default to settings
|
||||
const initialTab = deepLinkData?.tab || TAB_TYPES.SETTINGS;
|
||||
const [currentTab, setCurrentTab] = useState(initialTab);
|
||||
const [currentSection, setCurrentSection] = useState('');
|
||||
const [currentSectionName, setCurrentSectionName] = useState('');
|
||||
const [currentSubSection, setCurrentSubSection] = useState(deepLinkData?.subSection || null);
|
||||
const [productView, setProductView] = useState(null);
|
||||
const [resetDiscoverToAll, setResetDiscoverToAll] = useState(false);
|
||||
const [navigationTrigger, setNavigationTrigger] = useState(null);
|
||||
const [iframeBreadcrumbs, setIframeBreadcrumbs] = useState([]);
|
||||
|
||||
const changeTab = (type) => {
|
||||
setCurrentTab(type);
|
||||
};
|
||||
// Clear product view when changing tabs
|
||||
useEffect(() => {
|
||||
setProductView(null);
|
||||
}, [currentTab]);
|
||||
|
||||
const renderTab = () => {
|
||||
switch (currentTab) {
|
||||
case 'addons':
|
||||
return <Addons changeTab={changeTab} />;
|
||||
case 'marketplace':
|
||||
return <Marketplace changeTab={changeTab} />;
|
||||
default:
|
||||
return <Settings changeTab={changeTab} />;
|
||||
// Handle deep link updates (when modal opens via EventBus with new deep link)
|
||||
useEffect(() => {
|
||||
if (deepLinkData) {
|
||||
// Update tab if different
|
||||
if (deepLinkData.tab && deepLinkData.tab !== currentTab) {
|
||||
setCurrentTab(deepLinkData.tab);
|
||||
}
|
||||
|
||||
// Handle settings section navigation with subsection
|
||||
if (deepLinkData.tab === TAB_TYPES.SETTINGS && deepLinkData.section) {
|
||||
setNavigationTrigger({
|
||||
type: 'settings-section',
|
||||
data: deepLinkData.section,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
// Set sub-section if present
|
||||
if (deepLinkData.subSection) {
|
||||
setCurrentSubSection(deepLinkData.subSection);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [deepLinkData]);
|
||||
|
||||
// Clear hash when modal closes
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// When modal unmounts, clear the hash
|
||||
if (window.location.hash) {
|
||||
window.history.replaceState(null, null, window.location.pathname);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Listen for browser back/forward navigation via popstate
|
||||
const handlePopState = () => {
|
||||
const linkData = window.location.hash ? parseDeepLink(window.location.hash) : null;
|
||||
|
||||
if (linkData) {
|
||||
// Update tab if different
|
||||
if (linkData.tab && linkData.tab !== currentTab) {
|
||||
setCurrentTab(linkData.tab);
|
||||
}
|
||||
|
||||
// Handle settings section navigation
|
||||
if (linkData.tab === TAB_TYPES.SETTINGS && linkData.section) {
|
||||
setNavigationTrigger({
|
||||
type: 'settings-section',
|
||||
data: linkData.section,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
// Set sub-section if present in hash
|
||||
setCurrentSubSection(linkData.subSection || null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle product and collection navigation
|
||||
if (linkData.itemId && linkData.collection && linkData.fromCollection) {
|
||||
// Product viewed from within a collection
|
||||
// First set collection state, then navigate to product
|
||||
setNavigationTrigger({
|
||||
type: 'collection',
|
||||
data: linkData.collection,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
// Small delay to ensure collection state is set before navigating to product
|
||||
setTimeout(() => {
|
||||
setNavigationTrigger({
|
||||
type: 'product',
|
||||
data: {
|
||||
id: linkData.itemId,
|
||||
type: linkData.category,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}, 100);
|
||||
} else if (linkData.itemId) {
|
||||
// Product navigation (standalone)
|
||||
setNavigationTrigger({
|
||||
type: 'product',
|
||||
data: {
|
||||
id: linkData.itemId,
|
||||
type: linkData.category,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} else if (linkData.collection) {
|
||||
// Collection page navigation
|
||||
setNavigationTrigger({
|
||||
type: 'collection',
|
||||
data: linkData.collection,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} else {
|
||||
// Back to main view (clear collection state)
|
||||
setProductView(null);
|
||||
setNavigationTrigger({
|
||||
type: 'main',
|
||||
data: { clearCollection: true },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', handlePopState);
|
||||
return () => window.removeEventListener('popstate', handlePopState);
|
||||
}, [currentTab]);
|
||||
|
||||
const handleChangeTab = (newTab) => {
|
||||
setCurrentTab(newTab);
|
||||
// Update URL hash when tab changes
|
||||
if (newTab === TAB_TYPES.DISCOVER) {
|
||||
updateHash(`#${newTab}/all`);
|
||||
} else if (newTab === TAB_TYPES.LIBRARY) {
|
||||
updateHash(`#${newTab}/added`);
|
||||
} else {
|
||||
updateHash(`#${newTab}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSectionChange = (section, sectionName) => {
|
||||
setCurrentSection(section);
|
||||
setCurrentSectionName(sectionName);
|
||||
// Clear sub-section when changing sections
|
||||
setCurrentSubSection(null);
|
||||
// Update URL hash when section changes
|
||||
if (currentTab === TAB_TYPES.DISCOVER) {
|
||||
// For Discover tab, update with the section type
|
||||
const sectionMap = {
|
||||
[variables.getMessage('modals.main.marketplace.all')]: 'all',
|
||||
[variables.getMessage('modals.main.marketplace.photo_packs')]: 'photo_packs',
|
||||
[variables.getMessage('modals.main.marketplace.quote_packs')]: 'quote_packs',
|
||||
[variables.getMessage('modals.main.marketplace.preset_settings')]: 'preset_settings',
|
||||
[variables.getMessage('modals.main.marketplace.collections')]: 'collections',
|
||||
};
|
||||
const sectionKey = sectionMap[section];
|
||||
if (sectionKey) {
|
||||
updateHash(`#${currentTab}/${sectionKey}`);
|
||||
}
|
||||
} else if (currentTab === TAB_TYPES.SETTINGS && sectionName) {
|
||||
// For Settings tab, update with the section name
|
||||
updateHash(`#${currentTab}/${sectionName}`, false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubSectionChange = (subSection, sectionName) => {
|
||||
setCurrentSubSection(subSection);
|
||||
// Update URL hash when sub-section changes
|
||||
if (currentTab === TAB_TYPES.SETTINGS && sectionName) {
|
||||
if (subSection) {
|
||||
updateHash(`#${currentTab}/${sectionName}/${subSection}`);
|
||||
} else {
|
||||
// Going back to section, remove sub-section from hash
|
||||
updateHash(`#${currentTab}/${sectionName}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleProductView = (product) => {
|
||||
setProductView(product);
|
||||
// URL hash is already updated by child components (Browse.jsx)
|
||||
// Browser history automatically tracks hash changes
|
||||
};
|
||||
|
||||
const handleResetDiscoverToAll = () => {
|
||||
setResetDiscoverToAll(true);
|
||||
setTimeout(() => setResetDiscoverToAll(false), 100);
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
// Clear iframe breadcrumbs when navigating back
|
||||
setIframeBreadcrumbs([]);
|
||||
window.history.back();
|
||||
};
|
||||
|
||||
const handleForward = () => {
|
||||
window.history.forward();
|
||||
};
|
||||
|
||||
// Browser manages history state, so we always show buttons enabled
|
||||
// Browser will handle whether there's actually history to go back/forward
|
||||
const canGoBack = true;
|
||||
const canGoForward = true;
|
||||
|
||||
const TabComponent = TAB_COMPONENTS[currentTab] || Settings;
|
||||
|
||||
return (
|
||||
<div className="frame">
|
||||
<Tooltip
|
||||
style={{ position: 'absolute', top: '1rem', right: '1rem' }}
|
||||
title={variables.getMessage('modals.welcome.buttons.close')}
|
||||
key="closeTooltip"
|
||||
>
|
||||
<span className="closeModal" onClick={modalClose}>
|
||||
<MdClose />
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Suspense fallback={renderLoader()}>{renderTab()}</Suspense>
|
||||
<ModalTopBar
|
||||
currentTab={currentTab}
|
||||
currentSection={currentSection}
|
||||
currentSectionName={currentSectionName}
|
||||
currentSubSection={currentSubSection}
|
||||
productView={productView}
|
||||
iframeBreadcrumbs={iframeBreadcrumbs}
|
||||
onTabChange={handleChangeTab}
|
||||
onSubSectionChange={handleSubSectionChange}
|
||||
onClose={modalClose}
|
||||
onBack={handleBack}
|
||||
onForward={handleForward}
|
||||
canGoBack={canGoBack}
|
||||
canGoForward={canGoForward}
|
||||
/>
|
||||
<div style={{ flex: 1, display: 'flex', overflow: 'hidden', minHeight: 0 }}>
|
||||
<Suspense fallback={<ModalLoader currentTab={currentTab} />}>
|
||||
<TabComponent
|
||||
key={currentTab}
|
||||
changeTab={handleChangeTab}
|
||||
deepLinkData={deepLinkData}
|
||||
currentTab={currentTab}
|
||||
onSectionChange={handleSectionChange}
|
||||
onSubSectionChange={handleSubSectionChange}
|
||||
currentSubSection={currentSubSection}
|
||||
onProductView={handleProductView}
|
||||
onBreadcrumbsChange={setIframeBreadcrumbs}
|
||||
resetToAll={resetDiscoverToAll}
|
||||
onResetToAll={handleResetDiscoverToAll}
|
||||
navigationTrigger={navigationTrigger}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const MemoizedMainModal = memo(MainModal);
|
||||
export { MemoizedMainModal as default, MemoizedMainModal as MainModal };
|
||||
export default memo(MainModal);
|
||||
|
||||
@@ -1,103 +1,38 @@
|
||||
import variables from 'config/variables';
|
||||
import { memo, useState, useEffect } from 'react';
|
||||
import {
|
||||
MdSettings as Settings,
|
||||
MdWidgets as Addons,
|
||||
MdShoppingBasket as Marketplace,
|
||||
MdMenu as Navbar,
|
||||
MdEmojiPeople as Greeting,
|
||||
MdAccessAlarm as Time,
|
||||
MdOutlineFormatQuote as Quote,
|
||||
MdLink as QuickLinks,
|
||||
MdDateRange as Date,
|
||||
MdOutlineTextsms as Message,
|
||||
MdOutlinePhoto as Background,
|
||||
MdSearch,
|
||||
MdCloudQueue as Weather,
|
||||
MdFormatPaint as Appearance,
|
||||
MdTranslate as Language,
|
||||
MdOutlineSettings as Advanced,
|
||||
MdBugReport as Experimental,
|
||||
MdOutlineAssessment as Stats,
|
||||
MdOutlineNewReleases as Changelog,
|
||||
MdInfoOutline as About,
|
||||
MdOutlineExtension as Added,
|
||||
MdAddCircleOutline as Create,
|
||||
MdViewAgenda as Overview,
|
||||
MdCollectionsBookmark as Collections,
|
||||
} from 'react-icons/md';
|
||||
|
||||
const iconMapping = {
|
||||
[variables.getMessage('modals.main.marketplace.product.overview')]: <Overview />,
|
||||
[variables.getMessage('modals.main.navbar.settings')]: <Settings />,
|
||||
[variables.getMessage('modals.main.navbar.addons')]: <Addons />,
|
||||
[variables.getMessage('modals.main.navbar.marketplace')]: <Marketplace />,
|
||||
[variables.getMessage('modals.main.settings.sections.appearance.navbar.title')]: <Navbar />,
|
||||
[variables.getMessage('modals.main.settings.sections.greeting.title')]: <Greeting />,
|
||||
[variables.getMessage('modals.main.settings.sections.time.title')]: <Time />,
|
||||
[variables.getMessage('modals.main.settings.sections.quicklinks.title')]: <QuickLinks />,
|
||||
[variables.getMessage('modals.main.settings.sections.quote.title')]: <Quote />,
|
||||
[variables.getMessage('modals.main.settings.sections.date.title')]: <Date />,
|
||||
[variables.getMessage('modals.main.settings.sections.message.title')]: <Message />,
|
||||
[variables.getMessage('modals.main.settings.sections.background.title')]: <Background />,
|
||||
[variables.getMessage('modals.main.settings.sections.search.title')]: <MdSearch />,
|
||||
[variables.getMessage('modals.main.settings.sections.weather.title')]: <Weather />,
|
||||
[variables.getMessage('modals.main.settings.sections.appearance.title')]: <Appearance />,
|
||||
[variables.getMessage('modals.main.settings.sections.language.title')]: <Language />,
|
||||
[variables.getMessage('modals.main.settings.sections.advanced.title')]: <Advanced />,
|
||||
[variables.getMessage('modals.main.settings.sections.stats.title')]: <Stats />,
|
||||
[variables.getMessage('modals.main.settings.sections.experimental.title')]: <Experimental />,
|
||||
[variables.getMessage('modals.main.settings.sections.changelog.title')]: <Changelog />,
|
||||
[variables.getMessage('modals.main.settings.sections.about.title')]: <About />,
|
||||
[variables.getMessage('modals.main.addons.added')]: <Added />,
|
||||
[variables.getMessage('modals.main.addons.create.title')]: <Create />,
|
||||
[variables.getMessage('modals.main.marketplace.all')]: <Addons />,
|
||||
[variables.getMessage('modals.main.marketplace.photo_packs')]: <Background />,
|
||||
[variables.getMessage('modals.main.marketplace.quote_packs')]: <Quote />,
|
||||
[variables.getMessage('modals.main.marketplace.preset_settings')]: <Advanced />,
|
||||
[variables.getMessage('modals.main.marketplace.collections')]: <Collections />,
|
||||
};
|
||||
import { useT } from 'contexts/TranslationContext';
|
||||
import { getIconComponent, DIVIDER_LABELS } from '../constants/tabConfig';
|
||||
|
||||
function Tab({ label, currentTab, onClick, navbarTab }) {
|
||||
const t = useT();
|
||||
const [isExperimental, setIsExperimental] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setIsExperimental(localStorage.getItem('experimental') !== 'false');
|
||||
}, []);
|
||||
|
||||
let className = navbarTab ? 'navbar-item' : 'tab-list-item';
|
||||
if (currentTab === label) {
|
||||
className += navbarTab ? ' navbar-item-active' : ' tab-list-active';
|
||||
}
|
||||
// Get the icon component for this label (label is already translated)
|
||||
const IconComponent = getIconComponent(label, { getMessage: t });
|
||||
|
||||
const icon = iconMapping[label];
|
||||
const divider = [
|
||||
variables.getMessage('modals.main.settings.sections.weather.title'),
|
||||
variables.getMessage('modals.main.settings.sections.language.title'),
|
||||
variables.getMessage('modals.main.marketplace.all'),
|
||||
variables.getMessage('modals.main.settings.sections.experimental.title'),
|
||||
].includes(label);
|
||||
// Determine if this label should have a divider after it
|
||||
const hasDivider = DIVIDER_LABELS.some((key) => t(key) === label);
|
||||
|
||||
const mue = [
|
||||
variables.getMessage('modals.main.marketplace.product.overview'),
|
||||
variables.getMessage('modals.main.addons.added'),
|
||||
variables.getMessage('modals.main.marketplace.all'),
|
||||
].includes(label);
|
||||
// Build className
|
||||
const baseClass = navbarTab ? 'navbar-item' : 'tab-list-item';
|
||||
const activeClass = navbarTab ? 'navbar-item-active' : 'tab-list-active';
|
||||
const className = `${baseClass}${currentTab === label ? ` ${activeClass}` : ''}`;
|
||||
|
||||
if (
|
||||
label === variables.getMessage('modals.main.settings.sections.experimental.title') &&
|
||||
!isExperimental
|
||||
) {
|
||||
// Hide experimental tab if experimental mode is disabled
|
||||
const isExperimentalTab = label === t('modals.main.settings.sections.experimental.title');
|
||||
if (isExperimentalTab && !isExperimental) {
|
||||
return <hr />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{mue && <span className="mainTitle">Mue</span>}
|
||||
<button className={className} onClick={() => onClick(label)}>
|
||||
{icon} <span>{label}</span>
|
||||
{IconComponent && <IconComponent />} <span>{label}</span>
|
||||
</button>
|
||||
{divider && <hr />}
|
||||
{hasDivider && <hr />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,100 +1,142 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useT } from 'contexts/TranslationContext';
|
||||
import variables from 'config/variables';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
MdSettings,
|
||||
MdOutlineShoppingBasket,
|
||||
MdOutlineExtension,
|
||||
MdRefresh,
|
||||
MdClose,
|
||||
} from 'react-icons/md';
|
||||
import Tab from './Tab';
|
||||
import { Button } from 'components/Elements';
|
||||
import ReminderInfo from '../components/ReminderInfo';
|
||||
import ErrorBoundary from '../../../../features/misc/modals/ErrorBoundary';
|
||||
import { TAB_TYPES } from '../constants/tabConfig';
|
||||
|
||||
const Tabs = (props) => {
|
||||
const [currentTab, setCurrentTab] = useState(props.children[0].props.label);
|
||||
const [currentName, setCurrentName] = useState(props.children[0].props.name);
|
||||
const Tabs = ({
|
||||
children,
|
||||
navbar = false,
|
||||
currentTab: activeTab,
|
||||
onSectionChange,
|
||||
resetToFirst,
|
||||
deepLinkData,
|
||||
navigationTrigger,
|
||||
sections,
|
||||
}) => {
|
||||
const t = useT();
|
||||
|
||||
const onClick = (tab, name) => {
|
||||
// Find initial section from deep link if available
|
||||
const getInitialSection = () => {
|
||||
if (deepLinkData?.section && sections) {
|
||||
const section = sections.find((s) => s.name === deepLinkData.section);
|
||||
if (section) {
|
||||
return {
|
||||
label: t(section.label),
|
||||
name: section.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
label: children[0]?.props.label,
|
||||
name: children[0]?.props.name,
|
||||
};
|
||||
};
|
||||
|
||||
const initial = getInitialSection();
|
||||
const [currentTab, setCurrentTab] = useState(initial.label);
|
||||
const [currentName, setCurrentName] = useState(initial.name);
|
||||
const [showReminder, setShowReminder] = useState(localStorage.getItem('showReminder') === 'true');
|
||||
const contentRef = useRef(null);
|
||||
|
||||
const handleTabClick = (tab, name) => {
|
||||
if (name !== currentName) {
|
||||
variables.stats.postEvent('tab', `Opened ${name}`);
|
||||
}
|
||||
|
||||
setCurrentTab(tab);
|
||||
setCurrentName(name);
|
||||
|
||||
// Scroll content to top when changing tabs
|
||||
if (contentRef.current) {
|
||||
contentRef.current.scrollTop = 0;
|
||||
}
|
||||
|
||||
// Notify parent of section change with both label and name
|
||||
if (onSectionChange) {
|
||||
onSectionChange(tab, name);
|
||||
}
|
||||
};
|
||||
|
||||
const hideReminder = () => {
|
||||
localStorage.setItem('showReminder', false);
|
||||
document.querySelector('.reminder-info').style.display = 'none';
|
||||
// Notify parent of initial section on mount
|
||||
useEffect(() => {
|
||||
if (onSectionChange && currentTab) {
|
||||
onSectionChange(currentTab, currentName);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Update labels when language changes
|
||||
useEffect(() => {
|
||||
if (sections && currentName) {
|
||||
const section = sections.find((s) => s.name === currentName);
|
||||
if (section) {
|
||||
const newLabel = t(section.label);
|
||||
setCurrentTab(newLabel);
|
||||
}
|
||||
}
|
||||
}, [t, sections, currentName]);
|
||||
|
||||
// Handle navigation trigger for settings sections (popstate)
|
||||
useEffect(() => {
|
||||
if (navigationTrigger?.type === 'settings-section' && sections) {
|
||||
const section = sections.find((s) => s.name === navigationTrigger.data);
|
||||
if (section) {
|
||||
const label = t(section.label);
|
||||
setCurrentTab(label);
|
||||
setCurrentName(section.name);
|
||||
// Scroll content to top when navigating via browser history
|
||||
if (contentRef.current) {
|
||||
contentRef.current.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [navigationTrigger, sections, t]);
|
||||
|
||||
// Reset to first tab when requested
|
||||
useEffect(() => {
|
||||
if (resetToFirst) {
|
||||
setCurrentTab(children[0]?.props.label);
|
||||
setCurrentName(children[0]?.props.name);
|
||||
// Scroll content to top when resetting to first tab
|
||||
if (contentRef.current) {
|
||||
contentRef.current.scrollTop = 0;
|
||||
}
|
||||
if (onSectionChange) {
|
||||
onSectionChange(children[0]?.props.label, children[0]?.props.name);
|
||||
}
|
||||
}
|
||||
}, [resetToFirst]);
|
||||
|
||||
const handleHideReminder = () => {
|
||||
localStorage.setItem('showReminder', 'false');
|
||||
setShowReminder(false);
|
||||
};
|
||||
|
||||
const navbarButtons = [
|
||||
{
|
||||
tab: 'settings',
|
||||
icon: <MdSettings />,
|
||||
},
|
||||
{
|
||||
tab: 'addons',
|
||||
icon: <MdOutlineExtension />,
|
||||
},
|
||||
{
|
||||
tab: 'marketplace',
|
||||
icon: <MdOutlineShoppingBasket />,
|
||||
},
|
||||
];
|
||||
|
||||
const reminderInfo = (
|
||||
<div
|
||||
className="reminder-info"
|
||||
style={{ display: localStorage.getItem('showReminder') === 'true' ? 'flex' : 'none' }}
|
||||
>
|
||||
<div className="shareHeader">
|
||||
<span className="title">{variables.getMessage('modals.main.settings.reminder.title')}</span>
|
||||
<span className="closeModal" onClick={hideReminder}>
|
||||
<MdClose />
|
||||
</span>
|
||||
</div>
|
||||
<span className="subtitle">
|
||||
{variables.getMessage('modals.main.settings.reminder.message')}
|
||||
</span>
|
||||
<button onClick={() => window.location.reload()}>
|
||||
<MdRefresh />
|
||||
{variables.getMessage('modals.main.error_boundary.refresh')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
// Show sidebar for Settings and Discover tabs
|
||||
const showSidebar = activeTab === TAB_TYPES.SETTINGS || activeTab === TAB_TYPES.DISCOVER;
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', width: '100%', minHeight: '100%' }}>
|
||||
<div className="modalSidebar">
|
||||
{props.children.map((tab, index) => (
|
||||
<Tab
|
||||
currentTab={currentTab}
|
||||
key={index}
|
||||
label={tab.props.label}
|
||||
onClick={(nextTab) => onClick(nextTab, tab.props.name)}
|
||||
navbarTab={props.navbar || false}
|
||||
/>
|
||||
))}
|
||||
{reminderInfo}
|
||||
</div>
|
||||
<div className="modalTabContent">
|
||||
<div className="modalNavbar">
|
||||
{navbarButtons.map(({ tab, icon }, index) => (
|
||||
<Button
|
||||
type="navigation"
|
||||
onClick={() => props.changeTab(tab)}
|
||||
icon={icon}
|
||||
label={variables.getMessage(`modals.main.navbar.${tab}`)}
|
||||
active={props.current === tab}
|
||||
key={`${tab}-${index}`}
|
||||
<div style={{ display: 'flex', width: '100%', height: '100%', overflow: 'hidden' }}>
|
||||
{showSidebar ? (
|
||||
<div className="modalSidebar">
|
||||
{children.map((tab, index) => (
|
||||
<Tab
|
||||
key={index}
|
||||
currentTab={currentTab}
|
||||
label={tab.props.label}
|
||||
onClick={(nextTab) => handleTabClick(nextTab, tab.props.name)}
|
||||
navbarTab={navbar}
|
||||
/>
|
||||
))}
|
||||
<ReminderInfo isVisible={showReminder} onHide={handleHideReminder} />
|
||||
</div>
|
||||
{props.children.map((tab, index) => {
|
||||
) : null}
|
||||
<div className="modalTabContent" ref={contentRef}>
|
||||
{children.map((tab, index) => {
|
||||
if (tab.props.label !== currentTab) {
|
||||
return undefined;
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
22
src/components/Elements/MainModal/components/ModalLoader.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import variables from 'config/variables';
|
||||
import SidebarSkeleton from './SidebarSkeleton';
|
||||
|
||||
const ModalLoader = ({ currentTab }) => (
|
||||
<div style={{ display: 'flex', width: '100%', minHeight: '100%' }}>
|
||||
<div className="modalSidebar">
|
||||
<SidebarSkeleton currentTab={currentTab} />
|
||||
</div>
|
||||
<div className="modalTabContent">
|
||||
<div className="emptyItems">
|
||||
<div className="emptyMessage">
|
||||
<div className="loaderHolder">
|
||||
<div id="loader"></div>
|
||||
<span className="subtitle">{variables.getMessage('modals.main.loading')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ModalLoader;
|
||||
20
src/components/Elements/MainModal/components/ModalNavbar.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import variables from 'config/variables';
|
||||
import { Button } from 'components/Elements';
|
||||
import { NAVBAR_BUTTONS } from '../constants/tabConfig';
|
||||
|
||||
const ModalNavbar = ({ currentTab, onChangeTab }) => (
|
||||
<div className="modalNavbar">
|
||||
{NAVBAR_BUTTONS.map(({ tab, icon: Icon, messageKey }) => (
|
||||
<Button
|
||||
key={tab}
|
||||
type="navigation"
|
||||
onClick={() => onChangeTab(tab)}
|
||||
icon={<Icon />}
|
||||
label={variables.getMessage(messageKey)}
|
||||
active={currentTab === tab}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ModalNavbar;
|
||||
228
src/components/Elements/MainModal/components/ModalTopBar.jsx
Normal file
@@ -0,0 +1,228 @@
|
||||
import { useT } from 'contexts/TranslationContext';
|
||||
import { MdClose, MdChevronRight, MdArrowBack, MdArrowForward } from 'react-icons/md';
|
||||
import { Tooltip, Button } from 'components/Elements';
|
||||
import { NAVBAR_BUTTONS } from '../constants/tabConfig';
|
||||
import mueAboutIcon from 'assets/icons/mue_about.png';
|
||||
|
||||
// Map marketplace types to translation keys
|
||||
const MARKETPLACE_TYPE_TO_KEY = {
|
||||
photo_packs: 'modals.main.marketplace.photo_packs',
|
||||
photos: 'modals.main.marketplace.photo_packs',
|
||||
quote_packs: 'modals.main.marketplace.quote_packs',
|
||||
quotes: 'modals.main.marketplace.quote_packs',
|
||||
preset_settings: 'modals.main.marketplace.preset_settings',
|
||||
settings: 'modals.main.marketplace.preset_settings',
|
||||
collections: 'modals.main.marketplace.collections',
|
||||
all: 'modals.main.marketplace.all',
|
||||
};
|
||||
|
||||
function ModalTopBar({
|
||||
currentTab,
|
||||
currentSection,
|
||||
currentSectionName,
|
||||
currentSubSection,
|
||||
productView,
|
||||
iframeBreadcrumbs,
|
||||
onTabChange,
|
||||
onSubSectionChange,
|
||||
onClose,
|
||||
onBack,
|
||||
onForward,
|
||||
canGoBack,
|
||||
canGoForward,
|
||||
}) {
|
||||
const t = useT();
|
||||
|
||||
// Get the current tab label
|
||||
const currentTabButton = NAVBAR_BUTTONS.find(({ tab }) => tab === currentTab);
|
||||
const currentTabLabel = currentTabButton ? t(currentTabButton.messageKey) : '';
|
||||
|
||||
// Utility function to get translated sub-section label
|
||||
const getSubSectionLabel = (subSection, sectionName) => {
|
||||
if (!subSection || !sectionName) return subSection;
|
||||
|
||||
// Use the same translation pattern as the section components
|
||||
const translationKey = `modals.main.settings.sections.${sectionName}.${subSection}.title`;
|
||||
const translated = t(translationKey);
|
||||
|
||||
// If translation key is returned as-is or empty, it means translation doesn't exist
|
||||
// Fall back to capitalized sub-section name
|
||||
if (!translated || translated === translationKey) {
|
||||
return subSection.charAt(0).toUpperCase() + subSection.slice(1);
|
||||
}
|
||||
|
||||
return translated;
|
||||
};
|
||||
|
||||
// Determine breadcrumb path with click handlers
|
||||
const breadcrumbPath = [];
|
||||
|
||||
if (currentTabLabel) {
|
||||
breadcrumbPath.push({
|
||||
label: currentTabLabel,
|
||||
onClick: productView ? productView.onBackToAll : null, // Clickable if viewing a product
|
||||
});
|
||||
|
||||
// Check if we have iframe breadcrumbs (from Discover iframe)
|
||||
// If so, only use the last item (the item name) and keep our section
|
||||
if (iframeBreadcrumbs && iframeBreadcrumbs.length > 0) {
|
||||
// Get the last breadcrumb item (the item name)
|
||||
const lastCrumb = iframeBreadcrumbs[iframeBreadcrumbs.length - 1];
|
||||
|
||||
// Add current section if available and different from the last crumb
|
||||
if (currentSection && currentSection !== lastCrumb.label) {
|
||||
breadcrumbPath.push({
|
||||
label: currentSection,
|
||||
onClick: () => onBack(), // Clickable to go back
|
||||
});
|
||||
}
|
||||
|
||||
// Add the item name from iframe
|
||||
breadcrumbPath.push({
|
||||
label: lastCrumb.label,
|
||||
onClick: null, // Current item - not clickable
|
||||
});
|
||||
} else if (productView) {
|
||||
// If viewing a collection page itself (not a product within it)
|
||||
if (productView.isCollection) {
|
||||
// Show: Discover > Collection Name
|
||||
breadcrumbPath.push({
|
||||
label: productView.collectionTitle || productView.name,
|
||||
onClick: null, // Current page - not clickable
|
||||
});
|
||||
} else {
|
||||
// Viewing a product
|
||||
// Show: Discover > Collection/Category > Product
|
||||
if (productView.fromCollection && productView.collectionTitle) {
|
||||
// If from a collection, show collection name
|
||||
breadcrumbPath.push({
|
||||
label: productView.collectionTitle,
|
||||
onClick: productView.onBack || null,
|
||||
});
|
||||
} else {
|
||||
// Otherwise show category
|
||||
const categoryKey = MARKETPLACE_TYPE_TO_KEY[productView.type];
|
||||
if (categoryKey) {
|
||||
breadcrumbPath.push({
|
||||
label: t(categoryKey),
|
||||
onClick: productView.onBack || null,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Add product name as final breadcrumb
|
||||
breadcrumbPath.push({
|
||||
label: productView.name,
|
||||
onClick: null, // Current item - not clickable
|
||||
});
|
||||
}
|
||||
} else if (currentSection) {
|
||||
// Show: Tab > Section or Tab > Section > Sub-Section
|
||||
breadcrumbPath.push({
|
||||
label: currentSection,
|
||||
onClick: currentSubSection ? () => onSubSectionChange(null) : null, // Clickable if sub-section is active
|
||||
});
|
||||
|
||||
// Add sub-section if present
|
||||
if (currentSubSection) {
|
||||
breadcrumbPath.push({
|
||||
label: getSubSectionLabel(currentSubSection, currentSectionName),
|
||||
onClick: null, // Current sub-section - not clickable
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="modalTopBar">
|
||||
<div className="topBarLeft">
|
||||
<div className="navigationButtons">
|
||||
<Tooltip title="Back" key="backTooltip">
|
||||
<button
|
||||
className="navButton"
|
||||
onClick={onBack}
|
||||
disabled={!canGoBack}
|
||||
aria-label="Navigate back"
|
||||
>
|
||||
<MdArrowBack />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip title="Forward" key="forwardTooltip">
|
||||
<button
|
||||
className="navButton"
|
||||
onClick={onForward}
|
||||
disabled={!canGoForward}
|
||||
aria-label="Navigate forward"
|
||||
>
|
||||
<MdArrowForward />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<img
|
||||
src={mueAboutIcon}
|
||||
alt="Mue"
|
||||
className="topBarLogo"
|
||||
draggable={false}
|
||||
/>
|
||||
{breadcrumbPath.length > 0 && (
|
||||
<nav className="breadcrumbs" aria-label="Breadcrumb navigation">
|
||||
{breadcrumbPath.map((item, index) => {
|
||||
const isLast = index === breadcrumbPath.length - 1;
|
||||
const isClickable = item.onClick !== null;
|
||||
|
||||
return (
|
||||
<span key={index} className="breadcrumb-segment">
|
||||
{isClickable ? (
|
||||
<span
|
||||
className={`breadcrumb-item breadcrumb-clickable`}
|
||||
onClick={item.onClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
item.onClick();
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`Navigate to ${item.label}`}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className={`breadcrumb-item ${isLast ? 'breadcrumb-current' : ''}`}
|
||||
aria-current={isLast ? 'page' : undefined}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
{!isLast && <MdChevronRight className="breadcrumb-separator" aria-hidden="true" />}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
<div className="topBarRight">
|
||||
<div className="topBarNavigation">
|
||||
{NAVBAR_BUTTONS.map(({ tab, icon: Icon, messageKey }) => (
|
||||
<Button
|
||||
key={tab}
|
||||
type="navigation"
|
||||
onClick={() => onTabChange(tab)}
|
||||
active={currentTab === tab}
|
||||
icon={<Icon />}
|
||||
label={t(messageKey)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Tooltip title={t('modals.welcome.buttons.close')} key="closeTooltip">
|
||||
<span className="closeModal" onClick={onClose}>
|
||||
<MdClose />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ModalTopBar;
|
||||
@@ -0,0 +1,28 @@
|
||||
import variables from 'config/variables';
|
||||
import { MdRefresh, MdClose } from 'react-icons/md';
|
||||
|
||||
const ReminderInfo = ({ isVisible, onHide }) => {
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="reminder-info">
|
||||
<div className="shareHeader">
|
||||
<span className="title">{variables.getMessage('modals.main.settings.reminder.title')}</span>
|
||||
<span className="closeModal" onClick={onHide}>
|
||||
<MdClose />
|
||||
</span>
|
||||
</div>
|
||||
<span className="subtitle">
|
||||
{variables.getMessage('modals.main.settings.reminder.message')}
|
||||
</span>
|
||||
<button onClick={() => window.location.reload()}>
|
||||
<MdRefresh />
|
||||
{variables.getMessage('modals.main.error_boundary.refresh')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReminderInfo;
|
||||
@@ -0,0 +1,50 @@
|
||||
import { TAB_TYPES } from '../constants/tabConfig';
|
||||
|
||||
// Tab-specific configurations with exact divider positions
|
||||
const TAB_CONFIGS = {
|
||||
[TAB_TYPES.SETTINGS]: {
|
||||
itemCount: 16, // Excluding experimental
|
||||
dividerPositions: [10, 12], // After Weather, Language
|
||||
textWidths: [80, 100, 70, 90, 85, 75, 80, 95, 90, 75, 85, 90, 85, 80, 70, 95], // Fixed widths in pixels
|
||||
},
|
||||
[TAB_TYPES.DISCOVER]: {
|
||||
itemCount: 5,
|
||||
dividerPositions: [0], // After "All"
|
||||
textWidths: [60, 95, 95, 110, 90], // Fixed widths
|
||||
},
|
||||
[TAB_TYPES.LIBRARY]: {
|
||||
itemCount: 0, // Library doesn't show sidebar
|
||||
dividerPositions: [],
|
||||
textWidths: [],
|
||||
},
|
||||
};
|
||||
|
||||
const SidebarSkeleton = ({ currentTab = TAB_TYPES.SETTINGS }) => {
|
||||
const config = TAB_CONFIGS[currentTab] || TAB_CONFIGS[TAB_TYPES.SETTINGS];
|
||||
|
||||
// Library tab doesn't show sidebar
|
||||
if (config.itemCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="sidebarSkeleton">
|
||||
{Array.from({ length: config.itemCount }).map((_, index) => {
|
||||
const hasDivider = config.dividerPositions.includes(index);
|
||||
const textWidth = config.textWidths[index] || 80;
|
||||
|
||||
return (
|
||||
<div key={index}>
|
||||
<div className="skeletonItem">
|
||||
<div className="iconPlaceholder pulse" />
|
||||
<div className="textPlaceholder pulse" style={{ width: `${textWidth}px` }} />
|
||||
</div>
|
||||
{hasDivider && <hr className="skeletonDivider" />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidebarSkeleton;
|
||||
3
src/components/Elements/MainModal/components/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as ModalLoader } from './ModalLoader';
|
||||
export { default as ModalNavbar } from './ModalNavbar';
|
||||
export { default as ReminderInfo } from './ReminderInfo';
|
||||
160
src/components/Elements/MainModal/constants/tabConfig.js
Normal file
@@ -0,0 +1,160 @@
|
||||
import {
|
||||
MdSettings,
|
||||
MdWidgets,
|
||||
MdShoppingBasket,
|
||||
MdTune,
|
||||
MdBookmarks,
|
||||
MdExplore,
|
||||
MdMenu,
|
||||
MdEmojiPeople,
|
||||
MdAccessAlarm,
|
||||
MdOutlineFormatQuote,
|
||||
MdLink,
|
||||
MdDateRange,
|
||||
MdOutlineTextsms,
|
||||
MdOutlinePhoto,
|
||||
MdSearch,
|
||||
MdCloudQueue,
|
||||
MdFormatPaint,
|
||||
MdTranslate,
|
||||
MdOutlineSettings,
|
||||
MdBugReport,
|
||||
MdOutlineAssessment,
|
||||
MdOutlineNewReleases,
|
||||
MdInfoOutline,
|
||||
MdOutlineExtension,
|
||||
MdAddCircleOutline,
|
||||
MdViewAgenda,
|
||||
MdCollectionsBookmark,
|
||||
} from 'react-icons/md';
|
||||
|
||||
// Tab type constants
|
||||
export const TAB_TYPES = {
|
||||
SETTINGS: 'settings',
|
||||
LIBRARY: 'library',
|
||||
DISCOVER: 'discover',
|
||||
};
|
||||
|
||||
// Icon component mapping - using component references instead of elements
|
||||
export const ICON_COMPONENTS = {
|
||||
SETTINGS: MdTune,
|
||||
LIBRARY: MdBookmarks,
|
||||
DISCOVER: MdExplore,
|
||||
NAVBAR: MdMenu,
|
||||
GREETING: MdEmojiPeople,
|
||||
TIME: MdAccessAlarm,
|
||||
QUOTE: MdOutlineFormatQuote,
|
||||
QUICKLINKS: MdLink,
|
||||
DATE: MdDateRange,
|
||||
MESSAGE: MdOutlineTextsms,
|
||||
BACKGROUND: MdOutlinePhoto,
|
||||
SEARCH: MdSearch,
|
||||
WEATHER: MdCloudQueue,
|
||||
APPEARANCE: MdFormatPaint,
|
||||
LANGUAGE: MdTranslate,
|
||||
ADVANCED: MdOutlineSettings,
|
||||
EXPERIMENTAL: MdBugReport,
|
||||
STATS: MdOutlineAssessment,
|
||||
CHANGELOG: MdOutlineNewReleases,
|
||||
ABOUT: MdInfoOutline,
|
||||
ADDED: MdOutlineExtension,
|
||||
CREATE: MdAddCircleOutline,
|
||||
OVERVIEW: MdViewAgenda,
|
||||
COLLECTIONS: MdCollectionsBookmark,
|
||||
};
|
||||
|
||||
// Message keys for icon mapping
|
||||
export const MESSAGE_KEYS = {
|
||||
OVERVIEW: 'modals.main.marketplace.product.overview',
|
||||
SETTINGS: 'modals.main.navbar.settings',
|
||||
LIBRARY: 'modals.main.navbar.library',
|
||||
DISCOVER: 'modals.main.navbar.discover',
|
||||
NAVBAR: 'modals.main.settings.sections.appearance.navbar.title',
|
||||
GREETING: 'modals.main.settings.sections.greeting.title',
|
||||
TIME: 'modals.main.settings.sections.time.title',
|
||||
QUICKLINKS: 'modals.main.settings.sections.quicklinks.title',
|
||||
QUOTE: 'modals.main.settings.sections.quote.title',
|
||||
DATE: 'modals.main.settings.sections.date.title',
|
||||
MESSAGE: 'modals.main.settings.sections.message.title',
|
||||
BACKGROUND: 'modals.main.settings.sections.background.title',
|
||||
SEARCH: 'modals.main.settings.sections.search.title',
|
||||
WEATHER: 'modals.main.settings.sections.weather.title',
|
||||
APPEARANCE: 'modals.main.settings.sections.appearance.title',
|
||||
LANGUAGE: 'modals.main.settings.sections.language.title',
|
||||
ADVANCED: 'modals.main.settings.sections.advanced.title',
|
||||
STATS: 'modals.main.settings.sections.stats.title',
|
||||
EXPERIMENTAL: 'modals.main.settings.sections.experimental.title',
|
||||
CHANGELOG: 'modals.main.settings.sections.changelog.title',
|
||||
ABOUT: 'modals.main.settings.sections.about.title',
|
||||
ADDED: 'modals.main.addons.added',
|
||||
CREATE: 'modals.main.addons.create.title',
|
||||
ALL_MARKETPLACE: 'modals.main.marketplace.all',
|
||||
PHOTO_PACKS: 'modals.main.marketplace.photo_packs',
|
||||
QUOTE_PACKS: 'modals.main.marketplace.quote_packs',
|
||||
PRESET_SETTINGS: 'modals.main.marketplace.preset_settings',
|
||||
COLLECTIONS: 'modals.main.marketplace.collections',
|
||||
};
|
||||
|
||||
// Helper to get icon component by translated label
|
||||
// This function builds a map at runtime using variables.getMessage
|
||||
export const getIconComponent = (label, variables) => {
|
||||
const iconMap = {
|
||||
[variables.getMessage(MESSAGE_KEYS.OVERVIEW)]: ICON_COMPONENTS.OVERVIEW,
|
||||
[variables.getMessage(MESSAGE_KEYS.SETTINGS)]: ICON_COMPONENTS.SETTINGS,
|
||||
[variables.getMessage(MESSAGE_KEYS.LIBRARY)]: ICON_COMPONENTS.LIBRARY,
|
||||
[variables.getMessage(MESSAGE_KEYS.DISCOVER)]: ICON_COMPONENTS.DISCOVER,
|
||||
[variables.getMessage(MESSAGE_KEYS.NAVBAR)]: ICON_COMPONENTS.NAVBAR,
|
||||
[variables.getMessage(MESSAGE_KEYS.GREETING)]: ICON_COMPONENTS.GREETING,
|
||||
[variables.getMessage(MESSAGE_KEYS.TIME)]: ICON_COMPONENTS.TIME,
|
||||
[variables.getMessage(MESSAGE_KEYS.QUICKLINKS)]: ICON_COMPONENTS.QUICKLINKS,
|
||||
[variables.getMessage(MESSAGE_KEYS.QUOTE)]: ICON_COMPONENTS.QUOTE,
|
||||
[variables.getMessage(MESSAGE_KEYS.DATE)]: ICON_COMPONENTS.DATE,
|
||||
[variables.getMessage(MESSAGE_KEYS.MESSAGE)]: ICON_COMPONENTS.MESSAGE,
|
||||
[variables.getMessage(MESSAGE_KEYS.BACKGROUND)]: ICON_COMPONENTS.BACKGROUND,
|
||||
[variables.getMessage(MESSAGE_KEYS.SEARCH)]: ICON_COMPONENTS.SEARCH,
|
||||
[variables.getMessage(MESSAGE_KEYS.WEATHER)]: ICON_COMPONENTS.WEATHER,
|
||||
[variables.getMessage(MESSAGE_KEYS.APPEARANCE)]: ICON_COMPONENTS.APPEARANCE,
|
||||
[variables.getMessage(MESSAGE_KEYS.LANGUAGE)]: ICON_COMPONENTS.LANGUAGE,
|
||||
[variables.getMessage(MESSAGE_KEYS.ADVANCED)]: ICON_COMPONENTS.ADVANCED,
|
||||
[variables.getMessage(MESSAGE_KEYS.STATS)]: ICON_COMPONENTS.STATS,
|
||||
[variables.getMessage(MESSAGE_KEYS.EXPERIMENTAL)]: ICON_COMPONENTS.EXPERIMENTAL,
|
||||
[variables.getMessage(MESSAGE_KEYS.CHANGELOG)]: ICON_COMPONENTS.CHANGELOG,
|
||||
[variables.getMessage(MESSAGE_KEYS.ABOUT)]: ICON_COMPONENTS.ABOUT,
|
||||
[variables.getMessage(MESSAGE_KEYS.ADDED)]: ICON_COMPONENTS.ADDED,
|
||||
[variables.getMessage(MESSAGE_KEYS.CREATE)]: ICON_COMPONENTS.CREATE,
|
||||
[variables.getMessage(MESSAGE_KEYS.ALL_MARKETPLACE)]: ICON_COMPONENTS.LIBRARY,
|
||||
[variables.getMessage(MESSAGE_KEYS.PHOTO_PACKS)]: ICON_COMPONENTS.BACKGROUND,
|
||||
[variables.getMessage(MESSAGE_KEYS.QUOTE_PACKS)]: ICON_COMPONENTS.QUOTE,
|
||||
[variables.getMessage(MESSAGE_KEYS.PRESET_SETTINGS)]: ICON_COMPONENTS.ADVANCED,
|
||||
[variables.getMessage(MESSAGE_KEYS.COLLECTIONS)]: ICON_COMPONENTS.COLLECTIONS,
|
||||
};
|
||||
|
||||
return iconMap[label];
|
||||
};
|
||||
|
||||
// Navbar configuration
|
||||
export const NAVBAR_BUTTONS = [
|
||||
{
|
||||
tab: TAB_TYPES.SETTINGS,
|
||||
icon: ICON_COMPONENTS.SETTINGS,
|
||||
messageKey: 'modals.main.navbar.settings',
|
||||
},
|
||||
{
|
||||
tab: TAB_TYPES.LIBRARY,
|
||||
icon: ICON_COMPONENTS.LIBRARY,
|
||||
messageKey: 'modals.main.navbar.library',
|
||||
},
|
||||
{
|
||||
tab: TAB_TYPES.DISCOVER,
|
||||
icon: ICON_COMPONENTS.DISCOVER,
|
||||
messageKey: 'modals.main.navbar.discover',
|
||||
},
|
||||
];
|
||||
|
||||
// Labels that should have dividers after them
|
||||
export const DIVIDER_LABELS = [
|
||||
'modals.main.settings.sections.weather.title',
|
||||
'modals.main.settings.sections.language.title',
|
||||
'modals.main.marketplace.all',
|
||||
'modals.main.settings.sections.experimental.title',
|
||||
];
|
||||
@@ -1 +1 @@
|
||||
export * from './Main';
|
||||
export { default as MainModal } from './Main';
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
@import 'scss/variables';
|
||||
@import 'modules/sidebar';
|
||||
@import 'modules/navbar';
|
||||
@import 'modules/modalTabContent';
|
||||
@import 'modules/links';
|
||||
@import 'modules/scrollbars';
|
||||
@import 'settings/main';
|
||||
@import 'marketplace/main';
|
||||
@use 'sass:map';
|
||||
@use 'scss/variables' as *;
|
||||
@use 'modules/topBar' as *;
|
||||
@use 'modules/sidebar' as *;
|
||||
@use 'modules/navbar' as *;
|
||||
@use 'modules/buttons' as *;
|
||||
@use 'modules/modalTabContent' as *;
|
||||
@use 'modules/links' as *;
|
||||
@use 'modules/scrollbars' as *;
|
||||
@use 'settings/main' as *;
|
||||
@use 'marketplace/main' as *;
|
||||
// Fixed: Added sass:map module
|
||||
|
||||
.Overlay {
|
||||
position: fixed;
|
||||
@@ -26,9 +30,9 @@
|
||||
opacity: 1;
|
||||
z-index: -2;
|
||||
transition-timing-function: ease-in;
|
||||
border-radius: map-get($modal, 'border-radius');
|
||||
border-radius: map.get($modal, 'border-radius');
|
||||
user-select: none;
|
||||
overflow-y: auto;
|
||||
overflow: hidden;
|
||||
transform: scale(0);
|
||||
transition: all 0.3s cubic-bezier(0.47, 1.64, 0.41, 0.8);
|
||||
|
||||
@@ -54,11 +58,18 @@
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: 0.5s;
|
||||
outline: none;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
svg {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
@include themed {
|
||||
color: t($color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include themed {
|
||||
background: t($modal-sidebarActive);
|
||||
@@ -86,6 +97,11 @@
|
||||
height: 80vh;
|
||||
width: clamp(60vw, 1400px, 90vw);
|
||||
|
||||
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
||||
backdrop-filter: blur(16px) saturate(180%);
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
|
||||
@include themed {
|
||||
background-color: t($modal-background);
|
||||
}
|
||||
@@ -198,7 +214,7 @@ h5 {
|
||||
}
|
||||
|
||||
.languageSettings {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 50px;
|
||||
|
||||
.MuiFormGroup-root {
|
||||
gap: 5px;
|
||||
|
||||
@@ -1,34 +1,10 @@
|
||||
// this file is too long
|
||||
@import 'modules/item';
|
||||
@import 'modules/buttons';
|
||||
@import 'modules/lightbox';
|
||||
@import 'scss/variables';
|
||||
|
||||
.creatorItems {
|
||||
.item {
|
||||
flex-flow: row !important;
|
||||
}
|
||||
|
||||
.item-back {
|
||||
margin: 0 !important;
|
||||
filter: blur(40px) saturate(180%) brightness(90%) !important;
|
||||
height: 15px !important;
|
||||
width: 200px !important;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.card-details {
|
||||
margin: 0 !important;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
@use 'modules/lightbox' as *;
|
||||
@use 'scss/variables' as *;
|
||||
|
||||
.items {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 280px));
|
||||
grid-gap: 1.5rem;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 30px;
|
||||
@@ -38,26 +14,24 @@
|
||||
border-radius: 12px;
|
||||
width: auto;
|
||||
cursor: pointer;
|
||||
transition: 0.5s;
|
||||
transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
padding: 1.5rem;
|
||||
will-change: transform;
|
||||
transform: translate3d(0, 0, 0); // Force GPU acceleration
|
||||
|
||||
@include themed {
|
||||
background-color: t($modal-secondaryColour);
|
||||
box-shadow: 0 0 0 1px t($modal-sidebarActive);
|
||||
|
||||
&:hover {
|
||||
background-color: t($modal-sidebarActive);
|
||||
|
||||
img {
|
||||
background-color: t($modal-sidebarActive);
|
||||
}
|
||||
transform: translate3d(0, -5px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,34 +39,33 @@
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
.item-back {
|
||||
filter: blur(60px) saturate(180%) brightness(90%);
|
||||
position: absolute;
|
||||
object-fit: cover !important;
|
||||
height: 90px;
|
||||
width: 100px;
|
||||
border-radius: 100px;
|
||||
transition: 0.5s;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
object-fit: cover !important;
|
||||
height: 60px !important;
|
||||
width: 60px !important;
|
||||
border-radius: 12px;
|
||||
transition: 0.5s;
|
||||
margin-top: 25px;
|
||||
|
||||
&.item-icon-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
|
||||
@include themed {
|
||||
background-color: t($modal-sidebarActive);
|
||||
color: t($color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-details {
|
||||
z-index: 1;
|
||||
padding: 10px;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
|
||||
.card-title {
|
||||
font-size: 18px;
|
||||
@@ -106,8 +79,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.card-type {
|
||||
margin-top: 8px;
|
||||
.card-chips {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.card-type,
|
||||
.card-collection {
|
||||
font-size: 12px;
|
||||
font-weight: bolder;
|
||||
|
||||
@@ -120,6 +101,85 @@
|
||||
background-color: rgb(255 255 255 / 10%);
|
||||
border: 1px solid rgb(209 213 219 / 30%);
|
||||
}
|
||||
|
||||
.card-collection {
|
||||
@include themed {
|
||||
background-color: rgb(255 255 255 / 15%);
|
||||
border: 1px solid rgb(209 213 219 / 40%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-uninstall-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
svg {
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(220, 50, 50, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.item-installed-badge {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(255, 255, 255, 0.4);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
will-change: transform;
|
||||
|
||||
svg {
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.item-sideload-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(100, 100, 100, 0.9);
|
||||
cursor: help;
|
||||
|
||||
svg {
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .item-installed-badge {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
&.item-sideloaded {
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,84 +188,55 @@
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 30px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.itemShowcase {
|
||||
.itemContent {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 25px;
|
||||
width: 60%;
|
||||
max-width: 650px;
|
||||
|
||||
.description {
|
||||
max-lines: 3;
|
||||
font-size: 16px;
|
||||
}
|
||||
flex: 1;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
max-width: 650px !important;
|
||||
word-wrap: break-word !important;
|
||||
font-size: 16px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
}
|
||||
|
||||
.itemInfo {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
border-radius: 15px;
|
||||
width: 30%;
|
||||
max-width: 300px;
|
||||
max-height: 700px;
|
||||
|
||||
.front {
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
.itemTop {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 15px;
|
||||
width: 100%;
|
||||
box-sizing: border-box !important;
|
||||
border-radius: 12px 12px 0 0;
|
||||
backdrop-filter: blur(40px) saturate(150%) brightness(75%);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(100, 100, 100, 0.9);
|
||||
cursor: help;
|
||||
|
||||
@include themed {
|
||||
background-image: linear-gradient(to bottom, transparent, t($modal-background));
|
||||
svg {
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 5px 25px black;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: contain;
|
||||
&:hover .item-installed-badge {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.divider {
|
||||
text-transform: uppercase;
|
||||
&.item-sideloaded {
|
||||
cursor: default;
|
||||
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
}
|
||||
}
|
||||
|
||||
.iconButtons {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
grid-gap: 20px;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
&:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -252,6 +283,7 @@
|
||||
flex-flow: column;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
img {
|
||||
@@ -294,35 +326,6 @@ p.author {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.returnButton {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgb(121 121 121 / 22.6%);
|
||||
}
|
||||
}
|
||||
|
||||
.flexTopMarketplace {
|
||||
display: flex;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.tooltip {
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
||||
.mainTitle {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.filter {
|
||||
display: flex;
|
||||
@@ -346,145 +349,6 @@ p.author {
|
||||
}
|
||||
}
|
||||
|
||||
.collectionPage {
|
||||
// height: 200px;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
@include themed {
|
||||
border-radius: t($borderRadius);
|
||||
}
|
||||
|
||||
.nice-tag {
|
||||
border-radius: 150px;
|
||||
padding: 1px 12px;
|
||||
backdrop-filter: blur(16px) saturate(180%);
|
||||
background-color: rgb(255 255 255 / 10%);
|
||||
border: 1px solid rgb(209 213 219 / 30%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
text-align: center;
|
||||
text-shadow: #000 0 0 15px;
|
||||
|
||||
.mainTitle {
|
||||
justify-content: center;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #ccc !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.collection {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 36px 48px;
|
||||
margin: 15px 0;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
align-items: center;
|
||||
|
||||
@include themed {
|
||||
box-shadow: 0 0 0 1px t($modal-sidebarActive);
|
||||
border-radius: t($borderRadius);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 15px;
|
||||
max-width: 250px;
|
||||
text-shadow: #000 0 0 15px;
|
||||
|
||||
.title {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #ccc !important;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 5;
|
||||
}
|
||||
}
|
||||
|
||||
.items {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.marketplaceRefresh {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.marketplaceSearch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 30px;
|
||||
border-radius: 10px;
|
||||
font-size: 18px;
|
||||
|
||||
@include themed {
|
||||
box-shadow: 0 0 0 3px t($modal-sidebarActive);
|
||||
background: t($modal-sidebar);
|
||||
}
|
||||
|
||||
input {
|
||||
all: unset;
|
||||
}
|
||||
|
||||
@include themed {
|
||||
&:focus-within {
|
||||
background: t($modal-sidebarActive);
|
||||
box-shadow: 0 0 0 1px t($color);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: t($modal-sidebarActive);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inCollection {
|
||||
// background-image: linear-gradient(to left, transparent, #000),
|
||||
// url('https://external-preview.redd.it/JyhsEoGMhKIMi3kvfBS24L0IllAO_KrIm4UI-dA1Ax4.jpg?auto=webp&s=b5adf9859b2c1855a5b3085f9453a6e878548505');
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 5px;
|
||||
padding: 5px;
|
||||
margin: 10px 0;
|
||||
|
||||
@include themed {
|
||||
// background-color: t($modal-secondaryColour);
|
||||
// box-shadow: 0 0 0 1px t($modal-sidebarActive);
|
||||
border-radius: t($borderRadius);
|
||||
}
|
||||
|
||||
.title:hover {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.createYourOwn {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
@@ -517,3 +381,61 @@ p.author {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-options-container {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
gap: 20px;
|
||||
margin: 20px 0;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.filter-chips {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
|
||||
.filter-chip {
|
||||
all: unset;
|
||||
padding: 8px 20px;
|
||||
border-radius: 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
@include themed {
|
||||
background-color: t($modal-sidebarActive);
|
||||
color: t($color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include themed {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
@include themed {
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include themed {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid rgba(255, 255, 255, 0.5);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
.side {
|
||||
float: right;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
p.description {
|
||||
margin-top: 0;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.moreInfo {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 30px;
|
||||
|
||||
.items {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.item {
|
||||
flex: 1 0 40% !important;
|
||||
}
|
||||
|
||||
.infoItem {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
|
||||
svg {
|
||||
@include themed {
|
||||
background-image: t($slightGradient);
|
||||
box-shadow: t($boxShadow);
|
||||
}
|
||||
|
||||
font-size: 18px;
|
||||
padding: 7px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
font-size: medium;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
// text-transform: uppercase;
|
||||
font-size: small;
|
||||
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
@include themed {
|
||||
color: t($color);
|
||||
}
|
||||
}
|
||||
|
||||
@include themed {
|
||||
background: t($modal-secondaryColour);
|
||||
box-shadow: 0 0 0 1px t($modal-sidebarActive);
|
||||
border-radius: t($borderRadius);
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.subHeader {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
justify-content: space-between;
|
||||
gap: 25px;
|
||||
|
||||
.itemWarning {
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
svg {
|
||||
@include themed {
|
||||
background-image: t($slightGradient);
|
||||
box-shadow: t($boxShadow);
|
||||
}
|
||||
|
||||
padding: 7px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
@include themed {
|
||||
background: t($modal-sidebar);
|
||||
border-radius: t($borderRadius);
|
||||
box-shadow: 0 0 0 1px t($modal-sidebarActive);
|
||||
}
|
||||
}
|
||||
|
||||
.items {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.item {
|
||||
flex: 1 0 40% !important;
|
||||
}
|
||||
|
||||
.infoItem {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
flex: 1 0 44%;
|
||||
|
||||
svg {
|
||||
@include themed {
|
||||
background-image: t($slightGradient);
|
||||
box-shadow: t($boxShadow);
|
||||
}
|
||||
|
||||
font-size: 18px;
|
||||
padding: 7px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: medium;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: small;
|
||||
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
@include themed {
|
||||
color: t($color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.showMoreItems {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.marketplaceDescription {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 15px;
|
||||
|
||||
.subtitle {
|
||||
user-select: text !important;
|
||||
}
|
||||
}
|
||||
|
||||
.moreFromCurator {
|
||||
margin-top: 50px;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 15px;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
@use 'scss/variables' as *;
|
||||
|
||||
.lightBoxModal {
|
||||
margin: auto;
|
||||
max-width: 60%;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@use 'scss/variables' as *;
|
||||
|
||||
.updateCheck {
|
||||
flex-flow: row !important;
|
||||
}
|
||||
@@ -23,25 +25,39 @@
|
||||
.btn-navigation {
|
||||
@include modal-button(standard);
|
||||
|
||||
padding: 0 15px;
|
||||
padding: 10px 20px;
|
||||
border-radius: 12px !important;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
|
||||
@include themed {
|
||||
background: t($modal-secondaryColour) !important;
|
||||
border-radius: t($borderRadius) !important;
|
||||
box-shadow: t($boxShadow) !important;
|
||||
border: 0 !important;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: t($modal-sidebarActive) !important;
|
||||
}
|
||||
.light & {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.dark & {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
svg {
|
||||
background: var(--tab-active);
|
||||
@include themed {
|
||||
background: rgba(0, 0, 0, 0.04) !important;
|
||||
}
|
||||
|
||||
color: var(--modal-text);
|
||||
.light & {
|
||||
background: rgba(0, 0, 0, 0.04) !important;
|
||||
}
|
||||
|
||||
.dark & {
|
||||
background: rgba(255, 255, 255, 0.06) !important;
|
||||
}
|
||||
}
|
||||
|
||||
span,
|
||||
@@ -51,7 +67,10 @@
|
||||
|
||||
svg {
|
||||
font-size: 1.2em !important;
|
||||
color: var(--photo-info);
|
||||
|
||||
@include themed {
|
||||
color: t($color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +83,18 @@
|
||||
|
||||
.btn-navigation-active {
|
||||
@include themed {
|
||||
background: t($modal-sidebarActive) !important;
|
||||
background: rgba(0, 0, 0, 0.06) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.light & {
|
||||
background: rgba(0, 0, 0, 0.06) !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.dark & {
|
||||
background: rgba(255, 255, 255, 0.1) !important;
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +161,7 @@ a.btn-collection {
|
||||
height: 40px;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
border-radius: 8px !important;
|
||||
|
||||
@include modal-button(standard);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
@import 'scss/variables';
|
||||
@use 'scss/variables' as *;
|
||||
|
||||
.modalTabContent {
|
||||
width: 100% !important;
|
||||
@@ -8,12 +8,17 @@
|
||||
} */
|
||||
|
||||
@include themed {
|
||||
padding: 1rem 3rem 3rem;
|
||||
padding: 1rem 2rem 5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background: t($modal-background);
|
||||
|
||||
margin: 0;
|
||||
border-radius: t($borderRadius);
|
||||
|
||||
@extend %tabText;
|
||||
|
||||
hr {
|
||||
@@ -25,7 +30,6 @@
|
||||
.settingsRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 100px;
|
||||
justify-content: space-between;
|
||||
transition: 0.4s ease-in-out;
|
||||
|
||||
@@ -38,6 +42,10 @@
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
@@ -163,6 +171,12 @@ table {
|
||||
gap: 25px;
|
||||
padding: 25px;
|
||||
justify-content: space-between;
|
||||
cursor: default !important;
|
||||
user-select: auto;
|
||||
|
||||
&:active {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
@include themed {
|
||||
background: t($modal-sidebar);
|
||||
@@ -199,6 +213,30 @@ table {
|
||||
}
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
height: 56px;
|
||||
padding: 0 16px;
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
transition: 0.2s ease;
|
||||
|
||||
@include themed {
|
||||
background: t($modal-sidebar);
|
||||
border: 1px solid t($modal-sidebarActive);
|
||||
border-radius: t($borderRadius);
|
||||
color: t($color);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: t($color);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: t($subColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
@@ -250,7 +288,8 @@ table {
|
||||
padding-left: 10px;
|
||||
padding-right: 5px;
|
||||
|
||||
input[type='tel'] {
|
||||
input[type='tel'],
|
||||
input[type='number'] {
|
||||
background: none;
|
||||
outline: none;
|
||||
border: none;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import 'scss/variables';
|
||||
@use 'scss/variables' as *;
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
@import 'scss/variables';
|
||||
@use 'scss/variables' as *;
|
||||
|
||||
.modalSidebar {
|
||||
@include themed {
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: sticky;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 0 5px;
|
||||
// padding: 1rem 1.5rem 4rem 1.5rem;
|
||||
padding: 0.5rem 0 0 0.5rem;
|
||||
background: t($modal-sidebar);
|
||||
border-radius: 12px 0 0 12px;
|
||||
overflow: hidden auto;
|
||||
height: 80vh;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
height: 100%;
|
||||
min-width: 250px;
|
||||
|
||||
.mainTitle {
|
||||
text-align: center;
|
||||
font-size: 35px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 25px;
|
||||
}
|
||||
flex-shrink: 0;
|
||||
|
||||
svg {
|
||||
margin-left: 20px;
|
||||
@@ -52,6 +45,10 @@
|
||||
min-width: calc(100% - 1.2em);
|
||||
text-align: left;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: t($modal-sidebarActive);
|
||||
}
|
||||
@@ -77,3 +74,54 @@
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
// Sidebar skeleton loader
|
||||
.sidebarSkeleton {
|
||||
padding: 0.5rem 0.2rem;
|
||||
|
||||
.skeletonItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
margin: 0.2rem;
|
||||
pointer-events: none;
|
||||
|
||||
.iconPlaceholder {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
border-radius: 6px;
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
flex-shrink: 0;
|
||||
|
||||
@include themed {
|
||||
background: t($modal-sidebarActive);
|
||||
}
|
||||
}
|
||||
|
||||
.textPlaceholder {
|
||||
height: 18px;
|
||||
border-radius: 6px;
|
||||
|
||||
@include themed {
|
||||
background: t($modal-sidebarActive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.skeletonDivider {
|
||||
@include themed {
|
||||
height: 1px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
t($modal-sidebarActive) 20%,
|
||||
t($modal-sidebarActive) 80%,
|
||||
transparent 100%
|
||||
);
|
||||
margin: 0.5rem 1.75rem;
|
||||
border: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
147
src/components/Elements/MainModal/scss/modules/_topBar.scss
Normal file
@@ -0,0 +1,147 @@
|
||||
@use 'scss/variables' as *;
|
||||
|
||||
.modalTopBar {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1.5rem 1.5rem;
|
||||
// width: 100%;
|
||||
|
||||
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
||||
backdrop-filter: blur(16px) saturate(180%);
|
||||
|
||||
@include themed {
|
||||
background-color: t($modal-background);
|
||||
border-bottom: 1px solid t($modal-sidebarActive);
|
||||
}
|
||||
|
||||
.topBarLeft {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
gap: 1rem;
|
||||
|
||||
.navigationButtons {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
|
||||
.navButton {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0.5rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
transition: 0.3s;
|
||||
|
||||
@include themed {
|
||||
color: t($color);
|
||||
}
|
||||
|
||||
svg {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
@include themed {
|
||||
background: t($modal-sidebarActive);
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.topBarLogo {
|
||||
height: 32px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.breadcrumbs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
|
||||
.breadcrumb-segment {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// gap: 0.5rem;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
}
|
||||
white-space: nowrap;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.breadcrumb-clickable {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
}
|
||||
opacity: 0.5;
|
||||
font-size: 1.2rem;
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
|
||||
.breadcrumb-current {
|
||||
@include themed {
|
||||
color: t($color);
|
||||
}
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.topBarRight {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-shrink: 0;
|
||||
|
||||
.topBarNavigation {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.closeModal {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0.4em;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: 0.5s;
|
||||
flex-shrink: 0;
|
||||
|
||||
svg {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include themed {
|
||||
background: t($modal-sidebarActive);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
@import 'scss/variables';
|
||||
@import 'modules/material-ui';
|
||||
@import 'modules/tabs/about';
|
||||
@import 'modules/tabs/changelog';
|
||||
@import 'modules/tabs/order';
|
||||
@import 'modules/tabs/stats';
|
||||
@use 'scss/variables' as *;
|
||||
@use 'modules/material-ui' as *;
|
||||
@use 'modules/tabs/about' as *;
|
||||
@use 'modules/tabs/changelog' as *;
|
||||
@use 'modules/tabs/order' as *;
|
||||
@use 'modules/tabs/stats' as *;
|
||||
|
||||
input {
|
||||
/* colour picker */
|
||||
@@ -92,13 +92,20 @@ h4 {
|
||||
}
|
||||
|
||||
.imagesTopBar {
|
||||
padding-top: 25px;
|
||||
position: sticky;
|
||||
top: -20px;
|
||||
z-index: 90;
|
||||
padding: 25px 0 15px 0;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
div:nth-child(1) {
|
||||
@include themed {
|
||||
background: t($modal-background);
|
||||
}
|
||||
|
||||
.imagesTopBarTitle {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
@@ -121,16 +128,137 @@ h4 {
|
||||
.topbarbuttons {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
gap: 25px;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
button {
|
||||
button:not(.MuiButtonBase-root) {
|
||||
padding: 0 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.imagesControlBar {
|
||||
position: sticky;
|
||||
top: 68px;
|
||||
z-index: 89;
|
||||
padding: 12px 0;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
@include themed {
|
||||
background: t($modal-background);
|
||||
border-bottom: 1px solid t($modal-sidebarActive);
|
||||
}
|
||||
|
||||
.controlBarLeft {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 14px;
|
||||
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
}
|
||||
|
||||
.image-count {
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
|
||||
@include themed {
|
||||
color: t($color);
|
||||
}
|
||||
|
||||
.storage-info {
|
||||
font-weight: 400;
|
||||
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
}
|
||||
|
||||
.request-storage-link {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin-left: 5px;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
font-size: 13px;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
@include themed {
|
||||
color: #ff5c25;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.selection-separator {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.selected-count {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.delete-link {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin-left: 5px;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
font-size: 14px;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
@include themed {
|
||||
color: rgb(255 71 87);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.select-all-link {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin-left: 5px;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
font-size: 14px;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
|
||||
@include themed {
|
||||
color: t($color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.controlBarRight {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.customcss textarea {
|
||||
font-family: Consolas, 'Andale Mono WT', 'Andale Mono', 'Lucida Console', 'Lucida Sans Typewriter',
|
||||
font-family:
|
||||
Consolas, 'Andale Mono WT', 'Andale Mono', 'Lucida Console', 'Lucida Sans Typewriter',
|
||||
'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Liberation Mono', 'Nimbus Mono L', Monaco,
|
||||
'Courier New', Courier, monospace !important;
|
||||
}
|
||||
@@ -144,3 +272,40 @@ h4 {
|
||||
pointer-events: none;
|
||||
transition: 0.4s ease-in-out;
|
||||
}
|
||||
|
||||
// Warning banner (used in Search settings and potentially others)
|
||||
.itemWarning {
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
|
||||
.header {
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
@include themed {
|
||||
background-image: t($slightGradient);
|
||||
box-shadow: t($boxShadow);
|
||||
}
|
||||
|
||||
padding: 7px;
|
||||
border-radius: 100%;
|
||||
font-size: 24px;
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
@include themed {
|
||||
background: t($modal-sidebar);
|
||||
border-radius: t($borderRadius);
|
||||
box-shadow: 0 0 0 1px t($modal-sidebarActive);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* these are overrides for the material ui default styles */
|
||||
|
||||
@import 'scss/variables';
|
||||
@use 'scss/variables' as *;
|
||||
|
||||
.MuiCheckbox-colorPrimary.Mui-checked,
|
||||
.MuiSwitch-colorPrimary.Mui-checked,
|
||||
@@ -47,6 +47,10 @@ legend {
|
||||
margin-top: 10px !important;
|
||||
}
|
||||
|
||||
.settingsRow .action .MuiFormControlLabel-root {
|
||||
margin-top: 10px !important;
|
||||
}
|
||||
|
||||
.checkbox svg {
|
||||
@include themed {
|
||||
fill: t($color) !important;
|
||||
@@ -158,6 +162,12 @@ legend,
|
||||
.MuiFormControlLabel-root {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
p {
|
||||
@include themed {
|
||||
color: t($subColor) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.css-w66kx-MuiChip-root {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@use 'scss/variables' as *;
|
||||
|
||||
.aboutLink {
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
@@ -29,6 +31,8 @@
|
||||
img {
|
||||
width: 75px;
|
||||
height: auto;
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.5s ease-in-out forwards;
|
||||
|
||||
@include themed {
|
||||
border-radius: t($borderRadius);
|
||||
@@ -36,6 +40,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle-photographers {
|
||||
font-size: 16px;
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@use 'scss/variables' as *;
|
||||
|
||||
.updateChangelog {
|
||||
max-width: 75%;
|
||||
margin-top: 15px;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import 'scss/variables';
|
||||
@use 'scss/variables' as *;
|
||||
|
||||
.sortableItem {
|
||||
cursor: move;
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
.images-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
padding: 20px;
|
||||
grid-gap: 20px;
|
||||
|
||||
@@ -95,6 +95,413 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced custom images grid
|
||||
.images-grid {
|
||||
display: grid;
|
||||
padding: 1px;
|
||||
|
||||
// Show all checkboxes when in selection mode (any image selected)
|
||||
&.selection-mode {
|
||||
.image-checkbox {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
|
||||
@include themed {
|
||||
.image-card {
|
||||
position: relative;
|
||||
border-radius: t($borderRadius);
|
||||
background: t($modal-secondaryColour);
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: t($boxShadow);
|
||||
cursor: pointer;
|
||||
|
||||
&.selected {
|
||||
outline: 3px solid #ff5c25;
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
|
||||
|
||||
.image-nav-buttons {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.image-checkbox {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.image-checkbox {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
z-index: 12;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
input[type='checkbox'] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
backdrop-filter: blur(4px);
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
|
||||
&:checked {
|
||||
background: #ff5c25;
|
||||
border-color: #ff5c25;
|
||||
opacity: 1;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 2px;
|
||||
width: 5px;
|
||||
height: 10px;
|
||||
border: solid white;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #ff5c25;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
// Keep checkbox visible when checked
|
||||
&:has(input:checked) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
background: t($modal-sidebar);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.video-icon-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: t($modal-sidebar);
|
||||
|
||||
.customvideoicon {
|
||||
font-size: 60px;
|
||||
color: t($subColor);
|
||||
}
|
||||
}
|
||||
|
||||
.blur-placeholder {
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.image-nav-buttons {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 8px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
|
||||
.nav-button {
|
||||
pointer-events: all;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
backdrop-filter: blur(8px);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
svg {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-metadata {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
.image-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: t($color);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.image-details {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: t($subColor);
|
||||
|
||||
.detail {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.folder-tag {
|
||||
padding: 2px 8px;
|
||||
background: t($modal-sidebarActive);
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(255, 71, 87, 0.9);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.2s;
|
||||
z-index: 11;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
|
||||
svg {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgb(255 71 87);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
// Show delete button when card is hovered or has checkbox visible
|
||||
&:hover .delete-button,
|
||||
.image-checkbox:has(input:checked) ~ * .delete-button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Storage quota display
|
||||
.storage-quota {
|
||||
padding: 15px 20px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 50px;
|
||||
|
||||
@include themed {
|
||||
background: t($modal-secondaryColour);
|
||||
border-top: 1px solid t($modal-sidebarActive);
|
||||
}
|
||||
|
||||
.quota-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.quota-text {
|
||||
font-size: 13px;
|
||||
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
}
|
||||
}
|
||||
|
||||
.quota-info-button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
|
||||
&:hover {
|
||||
background: t($modal-sidebarActive);
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quota-bar {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
|
||||
@include themed {
|
||||
background: t($modal-sidebar);
|
||||
}
|
||||
|
||||
.quota-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Folder tagging modal styles
|
||||
.taggingModalContent {
|
||||
padding: 20px;
|
||||
|
||||
p.subtitle {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.taggingInput {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
|
||||
@include themed {
|
||||
background: t($modal-background);
|
||||
color: t($color);
|
||||
border-color: t($modal-sidebarActive);
|
||||
|
||||
&:focus {
|
||||
border-color: #ff5c25;
|
||||
box-shadow: 0 0 0 3px rgba(255, 92, 37, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropzone {
|
||||
margin-bottom: 100px;
|
||||
|
||||
@include themed {
|
||||
background: t($modal-background);
|
||||
}
|
||||
|
||||
.dropzone-content {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.photosEmpty {
|
||||
padding: 60px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.emptyNewMessage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
text-align: center;
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
|
||||
@include themed {
|
||||
color: t($color);
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overviewGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@use 'scss/variables' as *;
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import 'scss/variables';
|
||||
@use 'scss/variables' as *;
|
||||
|
||||
.smallModal {
|
||||
@extend %tabText;
|
||||
|
||||