3 Commits
Author SHA1 Message Date
Sansan f972b579fc Release automatically when package.json version changes
Build and Release / build (push) Successful in 23s
Build and Release / release (push) Successful in 6s
Build and release now run in one workflow on push to main: after the
build passes, if no tag exists for the package.json version, the image
is pushed and the tag and Gitea release are created. Running them in
sequence avoids the action-cache race between the separate build and
tag-triggered release workflows.
2026-10-04 21:56:15 +02:00
Sansan 71de395a20 Create Gitea release on tag and bump version to 1.1.2
Build and Test / build (push) Failing after 5s
Release Docker Image / build-and-push (push) Successful in 3m18s
2026-10-04 21:49:11 +02:00
Sansan e6d71fc77c Give modals the dark app theme and close them on outside click
Build and Test / build (push) Successful in 58s
Release Docker Image / build-and-push (push) Successful in 3m49s
- Restyle the shared modal styles to the dark app palette with orange accents
- Remove light-theme overrides in Search, Sync and Settings modals
- Add outside-click close and fade transition to the Sync modal
- Use @click.self for outside-click close in all modals
- Let tall modals scroll instead of being cut off
- Darken the mobile menu to match
2026-10-04 21:35:57 +02:00
14 changed files with 182 additions and 216 deletions

No files matched your search

+76 -1
View File
@@ -1,4 +1,10 @@
name: Build and Test
name: Build and Release
# Releases are driven by the version in package.json: bump it (e.g.
# `npm version patch --no-git-tag-version`) and push to main. If no tag
# v<version> exists yet, the release job pushes the Docker image and creates
# the tag and Gitea release. Build and release run in sequence in one
# workflow, so they never race for the runner's action cache.
on:
push:
@@ -31,3 +37,72 @@ jobs:
push: false
cache-from: type=gha
cache-to: type=gha,mode=max
release:
needs: build
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: https://github.com/actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Check version
id: version
run: |
VERSION="v$(node -p "require('./package.json').version")"
echo "tag=$VERSION" >> "$GITHUB_OUTPUT"
if git rev-parse -q --verify "refs/tags/$VERSION" >/dev/null; then
echo "$VERSION already released, nothing to do"
echo "release=false" >> "$GITHUB_OUTPUT"
else
echo "Releasing $VERSION"
echo "release=true" >> "$GITHUB_OUTPUT"
fi
- name: Set up QEMU
if: steps.version.outputs.release == 'true'
uses: https://github.com/docker/setup-qemu-action@v3
- name: Set up Docker Buildx
if: steps.version.outputs.release == 'true'
uses: https://github.com/docker/setup-buildx-action@v3
with:
config-inline: |
[registry."gitea.sanplex.xyz/sansan"]
http = true
insecure = true
- name: Login to Gitea Container Registry
if: steps.version.outputs.release == 'true'
uses: docker/login-action@v3
with:
registry: gitea.sanplex.xyz
username: sansan
password: ${{ secrets.TOKEN }}
- name: Build and push multi-arch image
if: steps.version.outputs.release == 'true'
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: |
gitea.sanplex.xyz/sansan/musicfrontend:${{ steps.version.outputs.tag }}
gitea.sanplex.xyz/sansan/musicfrontend:latest
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Create Release
if: steps.version.outputs.release == 'true'
uses: akkuman/gitea-release-action@v1
env:
NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18
with:
tag_name: ${{ steps.version.outputs.tag }}
target_commitish: ${{ github.sha }}
body: |
Docker image: `gitea.sanplex.xyz/sansan/musicfrontend:${{ steps.version.outputs.tag }}`
-32
View File
@@ -1,32 +0,0 @@
name: Publish
on:
push:
branches:
- '*.*'
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: https://github.com/actions/checkout@v4
- name: Set up Docker Buildx
uses: https://github.com/docker/setup-buildx-action@v3
with:
config-inline: |
[registry."gitea.sanplex.xyz/sansan"]
http = true
insecure = true
- name: Login to Gitea
uses: docker/login-action@v2
with:
registry: gitea.sanplex.xyz
username: ${{ github.repository_owner }}
password: ${{ secrets.TOKEN }}
- name: Build and push Docker image
uses: https://github.com/docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: "gitea.sanplex.xyz/sansan/musicfrontend:${{gitea.ref_name}}, gitea.sanplex.xyz/sansan/musicfrontend:latest"
-43
View File
@@ -1,43 +0,0 @@
name: Release Docker Image
on:
push:
tags:
- 'v*'
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- uses: https://github.com/actions/checkout@v4
- name: Set up QEMU
uses: https://github.com/docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: https://github.com/docker/setup-buildx-action@v3
with:
config-inline: |
[registry."gitea.sanplex.xyz/sansan"]
http = true
insecure = true
- name: Login to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: gitea.sanplex.xyz
username: sansan
password: ${{ secrets.TOKEN }}
- name: Build and push multi-arch image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: |
gitea.sanplex.xyz/sansan/musicfrontend:${{ gitea.ref_name }}
gitea.sanplex.xyz/sansan/musicfrontend:latest
cache-from: type=gha
cache-to: type=gha,mode=max
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "music-randomizer-vue",
"version": "0.1.0",
"version": "1.1.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "music-randomizer-vue",
"version": "0.1.0",
"version": "1.1.2",
"dependencies": {
"axios": "^1.7.2",
"core-js": "^3.37.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "music-randomizer-vue",
"version": "0.1.0",
"version": "1.1.2",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
+21 -6
View File
@@ -87,14 +87,16 @@ body {
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
overflow: hidden;
overflow-y: auto; /* Scroll tall modals instead of cutting them off */
background-color: rgb(0, 0, 0); /* Fallback color */
background-color: rgba(0, 0, 0, 0.5); /* Black w/ opacity */
background-color: rgba(0, 0, 0, 0.6); /* Black w/ opacity */
}
.modalContainer {
background-color: #eeeeee;
background-color: #333;
color: white;
margin: 10% auto; /* 15% from the top and centered */
border: 1px solid #888;
border: 1px solid #ff9c00;
border-radius: 10px;
width: 50%; /* Could be more or less, depending on screen size */
}
.modalWrapper {
@@ -114,16 +116,29 @@ body {
top: 8px;
cursor: pointer;
opacity: 70%;
font-size: 2rem;
line-height: 1;
color: #ff9c00;
}
/* The close icon is a black svg, recolor it to the app's orange */
img.closeModalImg {
filter: invert(66%) sepia(96%) saturate(2783%) hue-rotate(1deg) brightness(105%) contrast(103%);
}
.closeModalImg:hover {
opacity: 100%;
}
.modal h1 {
color: black;
color: white;
margin-left: auto;
margin-right: auto;
padding-top: 10px;
font-size: 1.9rem;
}
.modal p {
color: black;
color: white;
}
.modal a {
color: #ff9c00;
}
@media only screen and (max-width: 1000px) {
+9 -12
View File
@@ -1,6 +1,6 @@
<template>
<transition name="modalAni">
<div v-if="show" class="modal" @click="checkIfClickShouldCloseModal">
<div v-if="show" class="modal" @click.self="closeModal">
<div class="modalContainer">
<div class="modalWrapper">
<span class="closeModalImg" @click="closeModal">&times;</span>
@@ -68,11 +68,6 @@ export default {
this.searchTimeout = null;
}
},
checkIfClickShouldCloseModal(event) {
if (event.target.classList[0] === "modal") {
this.closeModal();
}
},
debouncedSearch() {
// Clear existing timeout
if (this.searchTimeout) {
@@ -160,7 +155,9 @@ export default {
width: 100%;
padding: 12px;
font-size: 1.1rem;
border: 1px solid #ccc;
border: 1px solid #777;
background-color: rgb(66, 66, 66);
color: white;
border-radius: 5px;
box-sizing: border-box;
}
@@ -178,7 +175,7 @@ export default {
}
.loading {
color: #666;
color: #bbb;
font-style: italic;
text-align: center;
padding: 20px;
@@ -191,19 +188,19 @@ export default {
.resultItem {
padding: 12px;
margin-bottom: 8px;
background-color: #f8f8f8;
border: 1px solid #ddd;
background-color: rgb(66, 66, 66);
border: 1px solid #555;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.2s;
}
.resultItem:hover {
background-color: #e8e8e8;
background-color: #555;
}
.noResults {
color: #666;
color: #bbb;
text-align: center;
padding: 20px;
font-style: italic;
+64 -84
View File
@@ -1,67 +1,69 @@
<template>
<div class="modal" v-if="isOpen">
<div class="modalContainer syncProgressContainer">
<span class="closeModalImg" @click="closeModal">&times;</span>
<h1>Sync in Progress</h1>
<transition name="modalAni">
<div class="modal" v-if="isOpen" @click.self="closeModal">
<div class="modalContainer syncProgressContainer">
<span class="closeModalImg" @click="closeModal">&times;</span>
<h1>Sync in Progress</h1>
<div v-if="!syncComplete" class="progressSection">
<div class="progressBarContainer">
<div class="progressBar" :style="{ width: progress + '%' }"></div>
</div>
<p class="progressText">{{ progress }}% complete</p>
<p class="timeText">Time spent: {{ timeSpent }}</p>
</div>
<div v-else class="resultsSection">
<h2>Sync Complete!</h2>
<p>Total time: {{ syncData.total_time }}</p>
<div v-if="syncData.soundtracks_added && syncData.soundtracks_added.length > 0" class="resultItem">
<h3>Soundtracks Added: {{ syncData.soundtracks_added.length }}</h3>
<ul class="soundtrackList">
<li v-for="soundtrack in syncData.soundtracks_added" :key="soundtrack">{{ soundtrack }}</li>
</ul>
<div v-if="!syncComplete" class="progressSection">
<div class="progressBarContainer">
<div class="progressBar" :style="{ width: progress + '%' }"></div>
</div>
<p class="progressText">{{ progress }}% complete</p>
<p class="timeText">Time spent: {{ timeSpent }}</p>
</div>
<div v-if="syncData.soundtracks_re_added && syncData.soundtracks_re_added.length > 0" class="resultItem">
<h3>Soundtracks Re-added: {{ syncData.soundtracks_re_added.length }}</h3>
<ul class="soundtrackList">
<li v-for="soundtrack in syncData.soundtracks_re_added" :key="soundtrack">{{ soundtrack }}</li>
</ul>
</div>
<div v-else class="resultsSection">
<h2>Sync Complete!</h2>
<p>Total time: {{ syncData.total_time }}</p>
<div v-if="syncData.soundtracks_removed && syncData.soundtracks_removed.length > 0" class="resultItem">
<h3>Soundtracks Removed: {{ syncData.soundtracks_removed.length }}</h3>
<ul class="soundtrackList">
<li v-for="soundtrack in syncData.soundtracks_removed" :key="soundtrack">{{ soundtrack }}</li>
</ul>
</div>
<div v-if="syncData.soundtracks_added && syncData.soundtracks_added.length > 0" class="resultItem">
<h3>Soundtracks Added: {{ syncData.soundtracks_added.length }}</h3>
<ul class="soundtrackList">
<li v-for="soundtrack in syncData.soundtracks_added" :key="soundtrack">{{ soundtrack }}</li>
</ul>
</div>
<div v-if="Object.keys(syncData.soundtracks_changed_title || {}).length > 0" class="resultItem">
<h3>Soundtracks with Changed Title: {{ Object.keys(syncData.soundtracks_changed_title || {}).length }}</h3>
<ul class="soundtrackList">
<li v-for="(newTitle, oldTitle) in syncData.soundtracks_changed_title" :key="oldTitle">{{ oldTitle }} → {{ newTitle }}</li>
</ul>
</div>
<div v-if="syncData.soundtracks_re_added && syncData.soundtracks_re_added.length > 0" class="resultItem">
<h3>Soundtracks Re-added: {{ syncData.soundtracks_re_added.length }}</h3>
<ul class="soundtrackList">
<li v-for="soundtrack in syncData.soundtracks_re_added" :key="soundtrack">{{ soundtrack }}</li>
</ul>
</div>
<div v-if="syncData.soundtracks_changed_content && syncData.soundtracks_changed_content.length > 0" class="resultItem">
<h3>Soundtracks with Changed Content: {{ syncData.soundtracks_changed_content.length }}</h3>
<ul class="soundtrackList">
<li v-for="soundtrack in syncData.soundtracks_changed_content" :key="soundtrack">{{ soundtrack }}</li>
</ul>
</div>
<div v-if="syncData.soundtracks_removed && syncData.soundtracks_removed.length > 0" class="resultItem">
<h3>Soundtracks Removed: {{ syncData.soundtracks_removed.length }}</h3>
<ul class="soundtrackList">
<li v-for="soundtrack in syncData.soundtracks_removed" :key="soundtrack">{{ soundtrack }}</li>
</ul>
</div>
<div v-if="syncData.catched_errors && syncData.catched_errors.length > 0" class="resultItem errors">
<h3>Errors: {{ syncData.catched_errors.length }}</h3>
<ul class="soundtrackList">
<li v-for="error in syncData.catched_errors" :key="error">{{ error }}</li>
</ul>
</div>
<div v-if="Object.keys(syncData.soundtracks_changed_title || {}).length > 0" class="resultItem">
<h3>Soundtracks with Changed Title: {{ Object.keys(syncData.soundtracks_changed_title || {}).length }}</h3>
<ul class="soundtrackList">
<li v-for="(newTitle, oldTitle) in syncData.soundtracks_changed_title" :key="oldTitle">{{ oldTitle }} → {{ newTitle }}</li>
</ul>
</div>
<p v-if="noChanges" class="noChanges">No changes detected.</p>
<div v-if="syncData.soundtracks_changed_content && syncData.soundtracks_changed_content.length > 0" class="resultItem">
<h3>Soundtracks with Changed Content: {{ syncData.soundtracks_changed_content.length }}</h3>
<ul class="soundtrackList">
<li v-for="soundtrack in syncData.soundtracks_changed_content" :key="soundtrack">{{ soundtrack }}</li>
</ul>
</div>
<div v-if="syncData.catched_errors && syncData.catched_errors.length > 0" class="resultItem errors">
<h3>Errors: {{ syncData.catched_errors.length }}</h3>
<ul class="soundtrackList">
<li v-for="error in syncData.catched_errors" :key="error">{{ error }}</li>
</ul>
</div>
<p v-if="noChanges" class="noChanges">No changes detected.</p>
</div>
</div>
</div>
</div>
</transition>
</template>
<script>
@@ -211,48 +213,26 @@ export default {
<style scoped>
.syncProgressContainer {
background-color: #eeeeee;
margin: 10% auto;
border: 1px solid #888;
position: relative;
width: 70%;
max-width: 800px;
padding: 20px;
border-radius: 10px;
}
.closeModalImg {
position: absolute;
right: 8px;
top: 8px;
cursor: pointer;
opacity: 70%;
font-size: 2rem;
color: #555;
}
.closeModalImg:hover {
opacity: 100%;
box-sizing: border-box;
}
.syncProgressContainer h1 {
color: black;
margin-left: auto;
margin-right: auto;
padding-top: 10px;
font-size: 1.9rem;
text-align: center;
margin-bottom: 20px;
}
.syncProgressContainer h2 {
color: black;
color: white;
text-align: center;
font-size: 1.5rem;
margin-bottom: 15px;
}
.syncProgressContainer p {
color: black;
text-align: center;
}
@@ -266,7 +246,7 @@ export default {
.progressBarContainer {
width: 100%;
height: 30px;
background-color: #ddd;
background-color: rgb(66, 66, 66);
border-radius: 15px;
overflow: hidden;
margin-bottom: 10px;
@@ -290,7 +270,7 @@ export default {
.timeText {
font-size: 1rem;
color: #666;
color: #bbb;
}
.resultsSection {
@@ -301,7 +281,7 @@ export default {
.resultItem {
margin-bottom: 20px;
padding: 10px;
background-color: #f8f8f8;
background-color: rgb(66, 66, 66);
border-radius: 8px;
border-left: 4px solid #4CAF50;
}
@@ -311,7 +291,7 @@ export default {
}
.resultItem h3 {
color: #333;
color: white;
font-size: 1.1rem;
margin-bottom: 8px;
}
@@ -324,12 +304,12 @@ export default {
.soundtrackList li {
padding: 3px 0;
color: #555;
color: #ddd;
}
.noChanges {
text-align: center;
color: #666;
color: #bbb;
font-style: italic;
padding: 20px;
}
+1 -6
View File
@@ -1,6 +1,6 @@
<template>
<transition name="modalAni">
<div v-if="show" class="modal" @click="checkIfClickShouldCloseModal">
<div v-if="show" class="modal" @click.self="closeModal">
<div class="modalContainer">
<div class="modalWrapper">
<img
@@ -46,11 +46,6 @@ export default {
openModal() {
this.show = true;
},
checkIfClickShouldCloseModal(event) {
if (event.target.classList[0] === "modal") {
this.closeModal();
}
},
},
};
</script>
+2 -7
View File
@@ -1,6 +1,6 @@
<template>
<transition name="modalAni">
<div v-if="show" class="modal" @click="checkIfClickShouldCloseModal">
<div v-if="show" class="modal" @click.self="closeModal">
<div class="modalContainer">
<div class="modalWrapper">
<img
@@ -50,11 +50,6 @@ export default {
this.show = true;
await this.fetchCharacters();
},
checkIfClickShouldCloseModal(event) {
if (event.target.classList[0] === "modal") {
this.closeModal();
}
},
async fetchCharacters() {
this.isLoading = true;
try {
@@ -150,7 +145,7 @@ export default {
text-align: center;
padding: 40px;
font-size: 1.2rem;
color: #666;
color: #bbb;
}
@media only screen and (max-width: 1000px) {
+2 -1
View File
@@ -109,7 +109,8 @@ export default {
}
.mobileMenuContainer {
background-color: #c4c4c4;
background-color: #333;
color: white;
width: 80vw;
height: 100%;
margin-left: 20%;
+2 -9
View File
@@ -1,7 +1,7 @@
<template>
<!-- Round Settings, Stop after current, Don't hide title -->
<transition name="modalAni">
<div v-if="show" class="modal" @click="checkIfClickShouldCloseModal">
<div v-if="show" class="modal" @click.self="closeModal">
<div class="modalContainer">
<div class="modalWrapper">
<img
@@ -98,11 +98,6 @@ export default {
openModal() {
this.show = true;
},
checkIfClickShouldCloseModal(event) {
if (event.target.classList[0] === "modal") {
this.closeModal();
}
},
updateOptionHideTitle() {
if (this.checkboxHideTitle) {
this.$store.dispatch("updateHideNextTitle", true);
@@ -165,6 +160,7 @@ export default {
margin-top: 20px;
}
.checkboxDiv input {
accent-color: #ff9c00;
transform: scale(1.3);
}
.checkboxDiv label {
@@ -181,9 +177,6 @@ export default {
display: flex;
width: 50px;
height: 33px;
background: rgb(16, 99, 194);
color: rgb(235, 235, 235);
border-color: rgb(161, 161, 161);
justify-content: center;
align-items: center;
line-height: 0;
+1 -6
View File
@@ -1,6 +1,6 @@
<template>
<transition name="modalAni">
<div v-if="show" class="modal" @click="checkIfClickShouldCloseModal">
<div v-if="show" class="modal" @click.self="closeModal">
<div class="modalContainer">
<div class="modalWrapper">
<img
@@ -35,11 +35,6 @@ export default {
openModal() {
this.show = true;
},
checkIfClickShouldCloseModal(event) {
if (event.target.classList[0] === "modal") {
this.closeModal();
}
},
},
};
</script>
+1 -6
View File
@@ -1,6 +1,6 @@
<template>
<transition name="modalAni">
<div v-if="show" class="modal" @click="checkIfClickShouldCloseModal">
<div v-if="show" class="modal" @click.self="closeModal">
<div class="modalContainer">
<div class="modalWrapper">
<img
@@ -38,11 +38,6 @@ export default {
this.playerName = playerName;
this.show = true;
},
checkIfClickShouldCloseModal(event) {
if (event.target.classList[0] === "modal") {
this.closeModal();
}
},
},
};
</script>