Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0474e168e5 | ||
|
|
5eb93dffae | ||
|
|
ba405e606b | ||
|
|
c7bd8f0ad3 |
@@ -22,8 +22,8 @@ jobs:
|
|||||||
runs-on: musicshare-android
|
runs-on: musicshare-android
|
||||||
container:
|
container:
|
||||||
image: musicshare-android-builder
|
image: musicshare-android-builder
|
||||||
# Keep Gradle and npm downloads between builds.
|
# Keep Gradle (downloads and build cache), npm, and compiled C++ (ccache) between builds.
|
||||||
options: -v musicshare-gradle:/root/.gradle -v musicshare-npm:/root/.npm
|
options: -v musicshare-gradle:/root/.gradle -v musicshare-npm:/root/.npm -v musicshare-ccache:/root/.ccache
|
||||||
timeout-minutes: 150
|
timeout-minutes: 150
|
||||||
env:
|
env:
|
||||||
SERVER: ${{ github.server_url }}
|
SERVER: ${{ github.server_url }}
|
||||||
|
|||||||
@@ -10,3 +10,5 @@ next-env.d.ts
|
|||||||
# Production secrets and database backups
|
# Production secrets and database backups
|
||||||
deploy/production.env
|
deploy/production.env
|
||||||
backups/
|
backups/
|
||||||
|
.aider*
|
||||||
|
deploy/gitea-runner/data/
|
||||||
|
|||||||
@@ -103,15 +103,27 @@ export async function toNotificationDTOs(rows: Row[]): Promise<NotificationDTO[]
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Newest first, 30 at a time; the cursor is the last one's time. */
|
/** Newest first, 30 at a time; the cursor is the last one's time. */
|
||||||
export async function listNotifications(userId: string, cursor: string | null) {
|
export async function listNotifications(
|
||||||
|
userId: string,
|
||||||
|
cursor: string | null,
|
||||||
|
actorId?: string // Added optional actorId filter
|
||||||
|
): Promise<{
|
||||||
|
notifications: NotificationDTO[];
|
||||||
|
unread: number;
|
||||||
|
nextCursor: string | null;
|
||||||
|
}> {
|
||||||
const n = schema.notifications;
|
const n = schema.notifications;
|
||||||
const before = cursor ? new Date(cursor) : null;
|
const before = cursor ? new Date(cursor) : null;
|
||||||
const rows = await db
|
const query = db
|
||||||
.select()
|
.select()
|
||||||
.from(n)
|
.from(n)
|
||||||
.where(and(eq(n.userId, userId), before && !isNaN(before.getTime()) ? lt(n.createdAt, before) : undefined))
|
.where(and(
|
||||||
.orderBy(desc(n.createdAt))
|
eq(n.userId, userId),
|
||||||
.limit(31);
|
before && !isNaN(before.getTime()) ? lt(n.createdAt, before) : undefined,
|
||||||
|
actorId ? eq(n.actorId, actorId) : undefined // Apply actor filter if provided
|
||||||
|
));
|
||||||
|
|
||||||
|
const rows = await query.orderBy(desc(n.createdAt)).limit(31);
|
||||||
const page = rows.slice(0, 30);
|
const page = rows.slice(0, 30);
|
||||||
return {
|
return {
|
||||||
notifications: await toNotificationDTOs(page),
|
notifications: await toNotificationDTOs(page),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"expo": {
|
"expo": {
|
||||||
"name": "musicshare",
|
"name": "musicshare",
|
||||||
"slug": "musicshare",
|
"slug": "musicshare",
|
||||||
"version": "1.4.0",
|
"version": "1.5.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/images/icon.png",
|
"icon": "./assets/images/icon.png",
|
||||||
"scheme": "musicshare",
|
"scheme": "musicshare",
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
},
|
},
|
||||||
"predictiveBackGestureEnabled": false,
|
"predictiveBackGestureEnabled": false,
|
||||||
"package": "tech.chrisedwards.musicshare",
|
"package": "tech.chrisedwards.musicshare",
|
||||||
"versionCode": 5
|
"versionCode": 6
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"expo-router",
|
"expo-router",
|
||||||
|
|||||||
+4
-2
@@ -121,9 +121,11 @@ class MediaListenerService : NotificationListenerService() {
|
|||||||
private fun snapshot(controller: MediaController): Playback? {
|
private fun snapshot(controller: MediaController): Playback? {
|
||||||
val metadata = controller.metadata
|
val metadata = controller.metadata
|
||||||
val title = metadata?.text(MediaMetadata.METADATA_KEY_TITLE) ?: metadata?.text(MediaMetadata.METADATA_KEY_DISPLAY_TITLE)
|
val title = metadata?.text(MediaMetadata.METADATA_KEY_TITLE) ?: metadata?.text(MediaMetadata.METADATA_KEY_DISPLAY_TITLE)
|
||||||
val artist = metadata?.text(MediaMetadata.METADATA_KEY_ARTIST)
|
val realArtist = metadata?.text(MediaMetadata.METADATA_KEY_ARTIST)
|
||||||
?: metadata?.text(MediaMetadata.METADATA_KEY_ALBUM_ARTIST)
|
?: metadata?.text(MediaMetadata.METADATA_KEY_ALBUM_ARTIST)
|
||||||
?: metadata?.text(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE)
|
val artist = realArtist ?: metadata?.text(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE)
|
||||||
|
// A phone video or other non-song: treat the session as if it weren't there.
|
||||||
|
if (NonMusicFilter.isNonMusic(controller.packageName, title, realArtist)) return null
|
||||||
val playbackState = controller.playbackState
|
val playbackState = controller.playbackState
|
||||||
val state = when (playbackState?.state) {
|
val state = when (playbackState?.state) {
|
||||||
PlaybackState.STATE_PLAYING, PlaybackState.STATE_BUFFERING,
|
PlaybackState.STATE_PLAYING, PlaybackState.STATE_BUFFERING,
|
||||||
|
|||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
package tech.chrisedwards.musicshare.nowplaying
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps things that aren't songs (phone videos, screen recordings) out of what we share.
|
||||||
|
* General-purpose players report a local file's name as its title, so a video played
|
||||||
|
* in VLC would otherwise show up as "VID_20261025_181502.mp4".
|
||||||
|
*/
|
||||||
|
internal object NonMusicFilter {
|
||||||
|
/** Players that also open videos and arbitrary files: only share from them when there's a real artist. */
|
||||||
|
private val GENERAL_PLAYERS = setOf(
|
||||||
|
"org.videolan.vlc",
|
||||||
|
"org.videolan.vlc.debug",
|
||||||
|
"com.mxtech.videoplayer.ad",
|
||||||
|
"com.mxtech.videoplayer.pro",
|
||||||
|
"is.xyz.mpv",
|
||||||
|
"com.brouken.player", // Just Player
|
||||||
|
"com.google.android.apps.photos",
|
||||||
|
"com.sec.android.gallery3d",
|
||||||
|
"com.android.gallery3d",
|
||||||
|
"com.miui.gallery",
|
||||||
|
"com.google.android.apps.nbu.files",
|
||||||
|
"com.sec.android.app.myfiles",
|
||||||
|
)
|
||||||
|
|
||||||
|
private val FILE_NAME = Regex(
|
||||||
|
""".*\.(mp4|m4v|mkv|mov|webm|avi|3gp|3g2|wmv|flv|ts|mts|m2ts|mpg|mpeg)$""",
|
||||||
|
RegexOption.IGNORE_CASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Camera, screen-recorder and messaging-app naming schemes. */
|
||||||
|
private val RECORDING_NAME = Regex(
|
||||||
|
"""^(VID|PXL|IMG|MVIMG|MOV|DSC|GOPR|GX\d{2}|Screen[_ ]?Recording|Screenrecorder|Record|signal|WhatsApp Video)[-_ ]?\d.*""",
|
||||||
|
RegexOption.IGNORE_CASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [realArtist] is the artist/album-artist tag only, not the display-subtitle fallback,
|
||||||
|
* which players fill with folder names and the like.
|
||||||
|
*/
|
||||||
|
fun isNonMusic(packageName: String, title: String?, realArtist: String?): Boolean {
|
||||||
|
val name = title?.trim().orEmpty()
|
||||||
|
if (name.isEmpty()) return false
|
||||||
|
if (FILE_NAME.matches(name) || RECORDING_NAME.matches(name)) return true
|
||||||
|
return packageName in GENERAL_PLAYERS && realArtist.isNullOrBlank()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@musicshare/mobile",
|
"name": "@musicshare/mobile",
|
||||||
"main": "expo-router/entry",
|
"main": "expo-router/entry",
|
||||||
"version": "1.4.0",
|
"version": "1.5.0",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo-google-fonts/poppins": "^0.4.1",
|
"@expo-google-fonts/poppins": "^0.4.1",
|
||||||
|
|||||||
@@ -7,24 +7,37 @@
|
|||||||
#
|
#
|
||||||
# Both are signed with the upload key when it's configured (see plugins/withReleaseSigning.js;
|
# Both are signed with the upload key when it's configured (see plugins/withReleaseSigning.js;
|
||||||
# in CI it comes from ORG_GRADLE_PROJECT_MUSICSHARE_UPLOAD_* variables). Set REQUIRE_SIGNED=1 to
|
# in CI it comes from ORG_GRADLE_PROJECT_MUSICSHARE_UPLOAD_* variables). Set REQUIRE_SIGNED=1 to
|
||||||
# fail rather than produce unsigned APKs. ABIS picks the CPU types (default: phones, 32 and 64 bit).
|
# fail rather than produce unsigned APKs. ABIS picks the CPU types (default: 64-bit phones, which is
|
||||||
# Regenerates apps/mobile/android each time (prebuild --clean).
|
# nearly every phone from the last several years; ABIS=armeabi-v7a,arm64-v8a adds old 32-bit ones).
|
||||||
|
# Regenerates apps/mobile/android each time (prebuild --clean); Gradle's build cache and ccache (when
|
||||||
|
# installed) make the rebuild cheap anyway.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
OUT=$(realpath -m "${1:-dist}")
|
OUT=$(realpath -m "${1:-dist}")
|
||||||
VERSION=$(node -p "require('./app.json').expo.version")
|
VERSION=$(node -p "require('./app.json').expo.version")
|
||||||
ABIS=${ABIS:-armeabi-v7a,arm64-v8a}
|
ABIS=${ABIS:-arm64-v8a}
|
||||||
BUILD_TOOLS=$(ls -d "$ANDROID_HOME"/build-tools/*/ | sort -V | tail -1)
|
BUILD_TOOLS=$(ls -d "$ANDROID_HOME"/build-tools/*/ | sort -V | tail -1)
|
||||||
mkdir -p "$OUT"
|
mkdir -p "$OUT"
|
||||||
|
|
||||||
say() { printf '\n==> %s\n' "$*"; }
|
say() { printf '\n==> %s\n' "$*"; }
|
||||||
|
|
||||||
|
# Most of a build is compiling the C++ in React Native and its libraries, which rarely changes:
|
||||||
|
# with ccache, CMake (3.17+) reuses earlier results via these launcher variables.
|
||||||
|
if command -v ccache > /dev/null; then
|
||||||
|
export CMAKE_C_COMPILER_LAUNCHER=ccache CMAKE_CXX_COMPILER_LAUNCHER=ccache
|
||||||
|
export CCACHE_BASEDIR=${CCACHE_BASEDIR:-$PWD} CCACHE_COMPILERCHECK=content CCACHE_MAXSIZE=${CCACHE_MAXSIZE:-6G}
|
||||||
|
ccache --zero-stats > /dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
local suffix=$1 foss=$2
|
local suffix=$1 foss=$2
|
||||||
say "Building musicshare-$VERSION$suffix.apk"
|
say "Building musicshare-$VERSION$suffix.apk"
|
||||||
MUSICSHARE_FOSS=$foss CI=1 npx expo prebuild --platform android --clean --no-install
|
MUSICSHARE_FOSS=$foss CI=1 npx expo prebuild --platform android --clean --no-install
|
||||||
(cd android && ./gradlew :app:assembleRelease -PreactNativeArchitectures="$ABIS" --console=plain)
|
# --build-cache reuses compiled Kotlin/Java, dex and resources from earlier builds (kept in ~/.gradle).
|
||||||
|
# Release lint is skipped: it takes a while and TypeScript, ESLint and the tests cover what matters.
|
||||||
|
(cd android && ./gradlew :app:assembleRelease -PreactNativeArchitectures="$ABIS" --build-cache \
|
||||||
|
-x lintVitalAnalyzeRelease -x lintVitalReportRelease -x lintVitalRelease --console=plain)
|
||||||
local apk
|
local apk
|
||||||
apk=$(ls android/app/build/outputs/apk/release/*.apk | head -1)
|
apk=$(ls android/app/build/outputs/apk/release/*.apk | head -1)
|
||||||
|
|
||||||
@@ -56,3 +69,4 @@ build() {
|
|||||||
build "" 0
|
build "" 0
|
||||||
build "-foss" 1
|
build "-foss" 1
|
||||||
(cd "$OUT" && sha256sum musicshare-"$VERSION"*.apk > SHA256SUMS && cat SHA256SUMS)
|
(cd "$OUT" && sha256sum musicshare-"$VERSION"*.apk > SHA256SUMS && cat SHA256SUMS)
|
||||||
|
if command -v ccache > /dev/null; then say "ccache"; ccache --show-stats | grep -iE 'hits|misses|cache size' || true; fi
|
||||||
|
|||||||
@@ -8,32 +8,43 @@ import { notificationPath, notificationText, timeAgo, type NotificationListDTO }
|
|||||||
import { Avatar } from '@/components/Avatar';
|
import { Avatar } from '@/components/Avatar';
|
||||||
import { client, useMe } from '@/lib/api';
|
import { client, useMe } from '@/lib/api';
|
||||||
|
|
||||||
|
const ADMIN_ACTOR_USERNAME = 'seriouscritter';
|
||||||
|
|
||||||
/** Reactions, comments, new followers and songs sent to you, newest first. Opening it marks them read. */
|
/** Reactions, comments, new followers and songs sent to you, newest first. Opening it marks them read. */
|
||||||
export default function NotificationsPage() {
|
export default function NotificationsPage() {
|
||||||
const { me, loggedOut } = useMe();
|
const { me, loggedOut } = useMe();
|
||||||
const { mutate } = useSWRConfig();
|
const { mutate } = useSWRConfig();
|
||||||
|
|
||||||
|
// Fetch all notifications for the user
|
||||||
const { data, size, setSize, isLoading } = useSWRInfinite<NotificationListDTO>(
|
const { data, size, setSize, isLoading } = useSWRInfinite<NotificationListDTO>(
|
||||||
(i, prev: NotificationListDTO | null) => (!me || (i > 0 && !prev?.nextCursor) ? null : ['notifications', prev?.nextCursor ?? null]),
|
(i, prev: NotificationListDTO | null) => (!me || (i > 0 && !prev?.nextCursor) ? null : ['notifications', prev?.nextCursor ?? null]),
|
||||||
([, cursor]: [string, string | null]) => client.notifications(cursor),
|
([, cursor]: [string, string | null]) => client.notifications(cursor),
|
||||||
{ revalidateOnFocus: false },
|
{ revalidateOnFocus: false },
|
||||||
);
|
);
|
||||||
const items = data?.flatMap((p) => p.notifications) ?? [];
|
|
||||||
|
const allItems = data?.flatMap((p) => p.notifications) ?? [];
|
||||||
const unread = data?.[0]?.unread ?? 0;
|
const unread = data?.[0]?.unread ?? 0;
|
||||||
|
|
||||||
|
// Filter messages specifically from seriouscritter
|
||||||
|
const seriousCritterMessages = allItems.filter(n => n.actor?.username === ADMIN_ACTOR_USERNAME);
|
||||||
|
|
||||||
// Seen once you've looked: clear the bell (rows stay highlighted until you come back).
|
// Seen once you've looked: clear the bell (rows stay highlighted until you come back).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (unread) client.markNotificationsRead().then(() => mutate('/notifications'), () => {});
|
if (unread) client.markNotificationsRead().then(() => mutate('/notifications'), () => {});
|
||||||
}, [unread, mutate]);
|
}, [unread, mutate]);
|
||||||
|
|
||||||
if (loggedOut) return <p className="muted"><Link href="/login">Log in</Link> to see your notifications.</p>;
|
if (loggedOut) return <p className="muted"><Link href="/login">Log in</Link> to see your notifications.</p>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="stack">
|
<div className="stack">
|
||||||
<h1>Notifications</h1>
|
<h1>Notifications</h1>
|
||||||
{items.length === 0 && !isLoading && (
|
|
||||||
<p className="muted">Nothing yet. Reactions, new followers and songs sent to you show up here.</p>
|
{/* Dedicated section for seriouscritter's messages */}
|
||||||
)}
|
{seriousCritterMessages.length > 0 && (
|
||||||
|
<div className="admin-section">
|
||||||
|
<h2 className="muted">From {ADMIN_ACTOR_USERNAME}</h2>
|
||||||
<ul className="notifications">
|
<ul className="notifications">
|
||||||
{items.map((n) => {
|
{seriousCritterMessages.map((n) => {
|
||||||
const { title, body } = notificationText(n);
|
const { title, body } = notificationText(n);
|
||||||
return (
|
return (
|
||||||
<li key={n.id} className={n.read ? '' : 'unread'}>
|
<li key={n.id} className={n.read ? '' : 'unread'}>
|
||||||
@@ -41,7 +52,7 @@ export default function NotificationsPage() {
|
|||||||
<Avatar user={n.actor} size={40} />
|
<Avatar user={n.actor} size={40} />
|
||||||
<span className="notification-text">
|
<span className="notification-text">
|
||||||
<strong>{title}</strong>
|
<strong>{title}</strong>
|
||||||
<span>{body}</span>
|
<span>{body</span >
|
||||||
<span className="muted">{timeAgo(n.createdAt)}</span>
|
<span className="muted">{timeAgo(n.createdAt)}</span>
|
||||||
</span>
|
</span>
|
||||||
{n.track?.artworkUrl && <img className="artwork-sm" src={n.track.artworkUrl} alt="" />}
|
{n.track?.artworkUrl && <img className="artwork-sm" src={n.track.artworkUrl} alt="" />}
|
||||||
@@ -50,11 +61,37 @@ export default function NotificationsPage() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* General notifications list */}
|
||||||
|
{allItems.length === 0 && !isLoading && (
|
||||||
|
<p className="muted">Nothing yet. Reactions, new followers and songs sent to you show up here.</p>
|
||||||
|
)}
|
||||||
|
<ul className="notifications">
|
||||||
|
{allItems.map((n) => {
|
||||||
|
const { title, body } = notificationText(n);
|
||||||
|
return (
|
||||||
|
<li key={n.id} className={n.read ? '' : 'unread'}>
|
||||||
|
<Link href={notificationPath(n)} className="notification">
|
||||||
|
<Avatar user={n.actor} size={40} />
|
||||||
|
<span className="notification-text">
|
||||||
|
<strong>{title}</strong>
|
||||||
|
<span>{body</span >
|
||||||
|
<span className="muted">{timeAgo(n.createdAt)}</span>
|
||||||
|
</span>
|
||||||
|
{n.track?.artworkUrl && <img className="artwork-sm" src={n.track.artworkUrl} alt="" />}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
|
||||||
{data?.at(-1)?.nextCursor && (
|
{data?.at(-1)?.nextCursor && (
|
||||||
<button className="btn" onClick={() => setSize(size + 1)}>
|
<button className="btn" onClick={() => setSize(size + 1)}>
|
||||||
Show more
|
Show more
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
#
|
#
|
||||||
# Versions match what React Native 0.86 / Expo 57 ask for
|
# Versions match what React Native 0.86 / Expo 57 ask for
|
||||||
# (node_modules/react-native/gradle/libs.versions.toml). Gradle downloads anything
|
# (node_modules/react-native/gradle/libs.versions.toml). Gradle downloads anything
|
||||||
# else it needs on first use; the workflow keeps ~/.gradle in a volume.
|
# else it needs on first use; the workflow keeps ~/.gradle (and ccache's ~/.ccache) in volumes.
|
||||||
FROM node:22-bookworm
|
FROM node:22-bookworm
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends openjdk-17-jdk-headless unzip \
|
&& apt-get install -y --no-install-recommends openjdk-17-jdk-headless unzip ccache \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 \
|
ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 \
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ container:
|
|||||||
# Use Docker's default network instead of creating one per job: a server with many compose
|
# Use Docker's default network instead of creating one per job: a server with many compose
|
||||||
# projects runs out of address pools ("all predefined address pools have been fully subnetted").
|
# projects runs out of address pools ("all predefined address pools have been fully subnetted").
|
||||||
network: bridge
|
network: bridge
|
||||||
# The volumes the workflow may mount (Gradle and npm caches), and nothing else.
|
# The volumes the workflow may mount (Gradle, npm and ccache caches), and nothing else.
|
||||||
valid_volumes:
|
valid_volumes:
|
||||||
- musicshare-gradle
|
- musicshare-gradle
|
||||||
- musicshare-npm
|
- musicshare-npm
|
||||||
|
- musicshare-ccache
|
||||||
|
|||||||
+5
-2
@@ -20,8 +20,11 @@ The pieces:
|
|||||||
|
|
||||||
## One-time setup (on the Gitea server)
|
## One-time setup (on the Gitea server)
|
||||||
|
|
||||||
The server needs about 8 GB of free memory during a build and about 15 GB of disk for the image and
|
The server needs about 8 GB of free memory during a build and about 20 GB of disk for the image and
|
||||||
caches. A first build takes 30-40 minutes; later ones reuse the Gradle cache.
|
caches. A first build takes 15-40 minutes depending on the machine; later ones reuse the Gradle build
|
||||||
|
cache and ccache's compiled C++ (volumes `musicshare-gradle` and `musicshare-ccache`) and take a few
|
||||||
|
minutes. The APKs are for 64-bit phones (`arm64-v8a`); set `ABIS=armeabi-v7a,arm64-v8a` in the workflow's
|
||||||
|
Build step to include old 32-bit ones.
|
||||||
The caches are Docker volumes, which live wherever Docker keeps its data (usually `/var/lib/docker`).
|
The caches are Docker volumes, which live wherever Docker keeps its data (usually `/var/lib/docker`).
|
||||||
Check that disk has room (`df -h /var/lib/docker`): if it fills up, everything else using Docker on the
|
Check that disk has room (`df -h /var/lib/docker`): if it fills up, everything else using Docker on the
|
||||||
server, including databases, stops writing.
|
server, including databases, stops writing.
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Videos and screen recordings played on your phone no longer show up as your song, and notifications now have their own section.
|
||||||
Generated
+1
-1
@@ -809,7 +809,7 @@
|
|||||||
},
|
},
|
||||||
"apps/mobile": {
|
"apps/mobile": {
|
||||||
"name": "@musicshare/mobile",
|
"name": "@musicshare/mobile",
|
||||||
"version": "1.4.0",
|
"version": "1.5.0",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo-google-fonts/poppins": "^0.4.1",
|
"@expo-google-fonts/poppins": "^0.4.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user