Compare commits
2 commits
5ec899c9cb
...
9efcfff484
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9efcfff484 | ||
|
|
91e7502d3b |
12 changed files with 262 additions and 102 deletions
|
|
@ -1140,13 +1140,39 @@ export default function DownloadsPage() {
|
||||||
<td>
|
<td>
|
||||||
{j.movieTitle}
|
{j.movieTitle}
|
||||||
{j.movieYear ? ` (${j.movieYear})` : ""}
|
{j.movieYear ? ` (${j.movieYear})` : ""}
|
||||||
{(() => {
|
{j.episodes && j.episodes.length > 0 ? (
|
||||||
|
<ul className="downloads-episode-list">
|
||||||
|
{j.episodes.map((ep) => (
|
||||||
|
<li key={ep.episodeNumber}>
|
||||||
|
<span>
|
||||||
|
E{String(ep.episodeNumber).padStart(2, "0")}
|
||||||
|
{ep.episodeTitle ? ` · ${ep.episodeTitle}` : ""}
|
||||||
|
{!ep.importedPath
|
||||||
|
? " · niet geïmporteerd"
|
||||||
|
: ep.subtitlePath
|
||||||
|
? " · sub OK"
|
||||||
|
: " · sub mist"}
|
||||||
|
</span>
|
||||||
|
{ep.importedPath && !ep.subtitlePath && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void openSubtitlePicker(j, ep.episodeNumber)}
|
||||||
|
>
|
||||||
|
Sub
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
(() => {
|
||||||
const sub = subtitleStatus(j);
|
const sub = subtitleStatus(j);
|
||||||
if (!sub || (j.episodes && j.episodes.length > 0)) return null;
|
if (!sub) return null;
|
||||||
if (sub.kind === "ok") return <div className="ok-text">{sub.text}</div>;
|
if (sub.kind === "ok") return <div className="ok-text">{sub.text}</div>;
|
||||||
if (sub.kind === "pending") return <div className="muted">{sub.text}</div>;
|
if (sub.kind === "pending") return <div className="muted">{sub.text}</div>;
|
||||||
return <div className="danger-text">{sub.text}</div>;
|
return <div className="danger-text">{sub.text}</div>;
|
||||||
})()}
|
})()
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="muted">{j.sourceTitle}</td>
|
<td className="muted">{j.sourceTitle}</td>
|
||||||
<td>{j.nodeName}</td>
|
<td>{j.nodeName}</td>
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,7 @@ export default function ViewersPage() {
|
||||||
const [iptvLocalErr, setIptvLocalErr] = useState<Record<string, string | null>>({});
|
const [iptvLocalErr, setIptvLocalErr] = useState<Record<string, string | null>>({});
|
||||||
const [addonInstallUrls, setAddonInstallUrls] = useState<Record<string, string | null>>({});
|
const [addonInstallUrls, setAddonInstallUrls] = useState<Record<string, string | null>>({});
|
||||||
const [claimCodes, setClaimCodes] = useState<Record<string, string>>({});
|
const [claimCodes, setClaimCodes] = useState<Record<string, string>>({});
|
||||||
|
const [newPasswords, setNewPasswords] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
const load = useCallback(() => {
|
const load = useCallback(() => {
|
||||||
fetch("/api/v1/admin/viewers", { credentials: "include" })
|
fetch("/api/v1/admin/viewers", { credentials: "include" })
|
||||||
|
|
@ -584,6 +585,7 @@ export default function ViewersPage() {
|
||||||
|
|
||||||
async function unlockLogin(v: Viewer) {
|
async function unlockLogin(v: Viewer) {
|
||||||
setActionId(`unlock-${v.id}`);
|
setActionId(`unlock-${v.id}`);
|
||||||
|
setMsg(null);
|
||||||
setErr(null);
|
setErr(null);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/v1/admin/viewers/${v.id}/unlock-login`, {
|
const res = await fetch(`/api/v1/admin/viewers/${v.id}/unlock-login`, {
|
||||||
|
|
@ -602,6 +604,35 @@ export default function ViewersPage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function changeViewerPassword(v: Viewer) {
|
||||||
|
const next = (newPasswords[v.id] ?? "").trim();
|
||||||
|
if (next.length < 8) {
|
||||||
|
setErr("Nieuw wachtwoord minstens 8 tekens");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setActionId(`pw-${v.id}`);
|
||||||
|
setMsg(null);
|
||||||
|
setErr(null);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/v1/admin/viewers/${v.id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
credentials: "include",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ password: next }),
|
||||||
|
});
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
if (!res.ok) {
|
||||||
|
setErr(data.error?.message ?? "Wachtwoord wijzigen mislukt");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setNewPasswords((prev) => ({ ...prev, [v.id]: "" }));
|
||||||
|
setMsg(`Wachtwoord bijgewerkt voor ${v.email}`);
|
||||||
|
load();
|
||||||
|
} finally {
|
||||||
|
setActionId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteViewer(v: Viewer) {
|
async function deleteViewer(v: Viewer) {
|
||||||
const label = v.name || v.email;
|
const label = v.name || v.email;
|
||||||
if (
|
if (
|
||||||
|
|
@ -917,8 +948,13 @@ export default function ViewersPage() {
|
||||||
<p className="muted detail-copy">
|
<p className="muted detail-copy">
|
||||||
Bepaalt of deze gebruiker kan inloggen en de app-inhoud mag gebruiken.
|
Bepaalt of deze gebruiker kan inloggen en de app-inhoud mag gebruiken.
|
||||||
</p>
|
</p>
|
||||||
{(selected.loginLocked || (selected.loginFailedCount ?? 0) > 0) && (
|
<div
|
||||||
<div className="login-lock-box">
|
className={
|
||||||
|
selected.loginLocked || (selected.loginFailedCount ?? 0) > 0
|
||||||
|
? "login-lock-box"
|
||||||
|
: "login-lock-box idle"
|
||||||
|
}
|
||||||
|
>
|
||||||
<p>
|
<p>
|
||||||
{selected.loginLocked
|
{selected.loginLocked
|
||||||
? `Login vergrendeld tot ${
|
? `Login vergrendeld tot ${
|
||||||
|
|
@ -926,19 +962,58 @@ export default function ViewersPage() {
|
||||||
? new Date(selected.loginLockedUntil).toLocaleString("nl-NL")
|
? new Date(selected.loginLockedUntil).toLocaleString("nl-NL")
|
||||||
: "…"
|
: "…"
|
||||||
} (3 foute pogingen).`
|
} (3 foute pogingen).`
|
||||||
: `${selected.loginFailedCount} foute wachtwoordpoging${
|
: (selected.loginFailedCount ?? 0) > 0
|
||||||
|
? `${selected.loginFailedCount} foute wachtwoordpoging${
|
||||||
(selected.loginFailedCount ?? 0) === 1 ? "" : "en"
|
(selected.loginFailedCount ?? 0) === 1 ? "" : "en"
|
||||||
} (lock na 3).`}
|
} (lock na 3).`
|
||||||
|
: "Geen login-blokkade actief. Ontgrendelen wist foute-pogingen-teller."}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={actionId === `unlock-${selected.id}`}
|
disabled={actionId === `unlock-${selected.id}`}
|
||||||
onClick={() => void unlockLogin(selected)}
|
onClick={() => void unlockLogin(selected)}
|
||||||
>
|
>
|
||||||
Login ontgrendelen
|
{actionId === `unlock-${selected.id}`
|
||||||
|
? "Ontgrendelen…"
|
||||||
|
: "Login ontgrendelen"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="form-group password-change">
|
||||||
|
<label className="field">
|
||||||
|
Nieuw wachtwoord
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
placeholder="Minstens 8 tekens"
|
||||||
|
value={newPasswords[selected.id] ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewPasswords((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[selected.id]: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
void changeViewerPassword(selected);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-primary"
|
||||||
|
disabled={
|
||||||
|
actionId === `pw-${selected.id}` ||
|
||||||
|
!(newPasswords[selected.id] ?? "").trim()
|
||||||
|
}
|
||||||
|
onClick={() => void changeViewerPassword(selected)}
|
||||||
|
>
|
||||||
|
{actionId === `pw-${selected.id}`
|
||||||
|
? "Opslaan…"
|
||||||
|
: "Wachtwoord wijzigen"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div className="toggle-rows">
|
<div className="toggle-rows">
|
||||||
<div className="toggle-row">
|
<div className="toggle-row">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -1593,6 +1668,11 @@ export default function ViewersPage() {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid rgba(248, 113, 113, 0.35);
|
border: 1px solid rgba(248, 113, 113, 0.35);
|
||||||
background: rgba(248, 113, 113, 0.08);
|
background: rgba(248, 113, 113, 0.08);
|
||||||
|
margin-bottom: 0.85rem;
|
||||||
|
}
|
||||||
|
.login-lock-box.idle {
|
||||||
|
border-color: var(--border-dim);
|
||||||
|
background: var(--bg-elevated);
|
||||||
}
|
}
|
||||||
.login-lock-box p {
|
.login-lock-box p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
@ -1600,6 +1680,16 @@ export default function ViewersPage() {
|
||||||
min-width: 180px;
|
min-width: 180px;
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
}
|
}
|
||||||
|
.password-change {
|
||||||
|
margin-bottom: 0.85rem;
|
||||||
|
padding: 0.75rem 0.9rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--border-dim);
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
}
|
||||||
|
.password-change .btn-primary {
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
.toggle-rows {
|
.toggle-rows {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.55rem;
|
gap: 0.55rem;
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ android {
|
||||||
applicationId = "nl.vonas.mediacluster"
|
applicationId = "nl.vonas.mediacluster"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 20
|
versionCode = 21
|
||||||
versionName = "1.0.19"
|
versionName = "1.0.20"
|
||||||
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
|
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
||||||
|
|
|
||||||
|
|
@ -885,14 +885,21 @@ private fun JSONArray?.toProfiles(): List<ViewerProfile> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun JSONObject.toProfile(): ViewerProfile = ViewerProfile(
|
private fun JSONObject.toProfile(): ViewerProfile {
|
||||||
|
val hasCustom = optBoolean("hasCustomAvatar")
|
||||||
|
val rawUrl = optString("avatarUrl").takeIf { it.isNotBlank() }
|
||||||
|
return ViewerProfile(
|
||||||
id = getString("id"),
|
id = getString("id"),
|
||||||
name = optString("name"),
|
name = optString("name"),
|
||||||
avatarId = optString("avatarId").ifBlank { "fox" },
|
avatarId = optString("avatarId").ifBlank { "fox" },
|
||||||
isOwner = optBoolean("isOwner"),
|
isOwner = optBoolean("isOwner"),
|
||||||
isKids = optBoolean("isKids"),
|
isKids = optBoolean("isKids"),
|
||||||
hasPin = optBoolean("hasPin"),
|
hasPin = optBoolean("hasPin"),
|
||||||
showAvatar = if (has("showAvatar")) optBoolean("showAvatar") else true,
|
showAvatar = when {
|
||||||
hasCustomAvatar = optBoolean("hasCustomAvatar"),
|
!has("showAvatar") || isNull("showAvatar") -> true
|
||||||
avatarUrl = optString("avatarUrl").ifBlank { null },
|
else -> optBoolean("showAvatar", true)
|
||||||
|
},
|
||||||
|
hasCustomAvatar = hasCustom,
|
||||||
|
avatarUrl = if (hasCustom) rawUrl else null,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,13 +86,12 @@ class SessionStore(private val context: Context) {
|
||||||
if (showAvatar != null) {
|
if (showAvatar != null) {
|
||||||
prefs[profileShowAvatarKey] = if (showAvatar) "1" else "0"
|
prefs[profileShowAvatarKey] = if (showAvatar) "1" else "0"
|
||||||
}
|
}
|
||||||
if (avatarUrl != null) {
|
// Altijd syncen: null/blank = geen custom foto (emoji tonen)
|
||||||
if (avatarUrl.isBlank()) prefs.remove(profileAvatarUrlKey)
|
if (avatarUrl.isNullOrBlank()) prefs.remove(profileAvatarUrlKey)
|
||||||
else prefs[profileAvatarUrlKey] = avatarUrl
|
else prefs[profileAvatarUrlKey] = avatarUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun setApiBase(url: String) {
|
suspend fun setApiBase(url: String) {
|
||||||
context.dataStore.edit { it[apiKey] = url.trim().trimEnd('/') }
|
context.dataStore.edit { it[apiKey] = url.trim().trimEnd('/') }
|
||||||
|
|
|
||||||
|
|
@ -1350,7 +1350,10 @@ private fun SettingsScreen(
|
||||||
.background(Color(0xFF2E2E3A)),
|
.background(Color(0xFF2E2E3A)),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
if (profileShowAvatar && !profileAvatarUrl.isNullOrBlank()) {
|
if (profileShowAvatar) {
|
||||||
|
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Text(avatarEmoji(profileAvatarId ?: "fox"), fontSize = 22.sp)
|
||||||
|
if (!profileAvatarUrl.isNullOrBlank()) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = ImageRequest.Builder(LocalContext.current)
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
.data(profileAvatarUrl)
|
.data(profileAvatarUrl)
|
||||||
|
|
@ -1364,8 +1367,15 @@ private fun SettingsScreen(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Text(avatarEmoji(profileAvatarId ?: "fox"), fontSize = 22.sp)
|
Text(
|
||||||
|
(profileName?.firstOrNull()?.uppercaseChar() ?: '?').toString(),
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Column(Modifier.weight(1f)) {
|
Column(Modifier.weight(1f)) {
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ android {
|
||||||
applicationId = "nl.vonas.mediacluster.tv"
|
applicationId = "nl.vonas.mediacluster.tv"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 106
|
versionCode = 107
|
||||||
versionName = "0.14.43"
|
versionName = "0.14.44"
|
||||||
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
|
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
||||||
|
|
|
||||||
|
|
@ -875,14 +875,21 @@ private fun JSONArray?.toProfiles(): List<ViewerProfile> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun JSONObject.toProfile(): ViewerProfile = ViewerProfile(
|
private fun JSONObject.toProfile(): ViewerProfile {
|
||||||
|
val hasCustom = optBoolean("hasCustomAvatar")
|
||||||
|
val rawUrl = optString("avatarUrl").takeIf { it.isNotBlank() }
|
||||||
|
return ViewerProfile(
|
||||||
id = getString("id"),
|
id = getString("id"),
|
||||||
name = optString("name"),
|
name = optString("name"),
|
||||||
avatarId = optString("avatarId").ifBlank { "fox" },
|
avatarId = optString("avatarId").ifBlank { "fox" },
|
||||||
isOwner = optBoolean("isOwner"),
|
isOwner = optBoolean("isOwner"),
|
||||||
isKids = optBoolean("isKids"),
|
isKids = optBoolean("isKids"),
|
||||||
hasPin = optBoolean("hasPin"),
|
hasPin = optBoolean("hasPin"),
|
||||||
showAvatar = if (has("showAvatar")) optBoolean("showAvatar") else true,
|
showAvatar = when {
|
||||||
hasCustomAvatar = optBoolean("hasCustomAvatar"),
|
!has("showAvatar") || isNull("showAvatar") -> true
|
||||||
avatarUrl = optString("avatarUrl").ifBlank { null },
|
else -> optBoolean("showAvatar", true)
|
||||||
|
},
|
||||||
|
hasCustomAvatar = hasCustom,
|
||||||
|
avatarUrl = if (hasCustom) rawUrl else null,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -105,13 +105,12 @@ class SessionStore(private val context: Context) {
|
||||||
if (showAvatar != null) {
|
if (showAvatar != null) {
|
||||||
prefs[profileShowAvatarKey] = if (showAvatar) "1" else "0"
|
prefs[profileShowAvatarKey] = if (showAvatar) "1" else "0"
|
||||||
}
|
}
|
||||||
if (avatarUrl != null) {
|
// Altijd syncen: null/blank = geen custom foto (emoji tonen)
|
||||||
if (avatarUrl.isBlank()) prefs.remove(profileAvatarUrlKey)
|
if (avatarUrl.isNullOrBlank()) prefs.remove(profileAvatarUrlKey)
|
||||||
else prefs[profileAvatarUrlKey] = avatarUrl
|
else prefs[profileAvatarUrlKey] = avatarUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun setApiBase(url: String) {
|
suspend fun setApiBase(url: String) {
|
||||||
context.dataStore.edit { it[apiKey] = url.trim().trimEnd('/') }
|
context.dataStore.edit { it[apiKey] = url.trim().trimEnd('/') }
|
||||||
|
|
|
||||||
|
|
@ -784,6 +784,7 @@ private fun MainHub(
|
||||||
MainTab.Settings -> SettingsScreen(
|
MainTab.Settings -> SettingsScreen(
|
||||||
api = api,
|
api = api,
|
||||||
profileAvatarId = profileAvatarId,
|
profileAvatarId = profileAvatarId,
|
||||||
|
profileShowAvatar = profileShowAvatar,
|
||||||
profileName = profileName,
|
profileName = profileName,
|
||||||
isOwner = isOwner,
|
isOwner = isOwner,
|
||||||
onBack = {
|
onBack = {
|
||||||
|
|
@ -1644,6 +1645,7 @@ private fun ShelfBlock(
|
||||||
private fun SettingsScreen(
|
private fun SettingsScreen(
|
||||||
api: ApiClient,
|
api: ApiClient,
|
||||||
profileAvatarId: String? = null,
|
profileAvatarId: String? = null,
|
||||||
|
profileShowAvatar: Boolean = true,
|
||||||
profileName: String? = null,
|
profileName: String? = null,
|
||||||
isOwner: Boolean = false,
|
isOwner: Boolean = false,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
|
|
@ -1737,12 +1739,21 @@ private fun SettingsScreen(
|
||||||
.background(Color(0xFF2E2E3A)),
|
.background(Color(0xFF2E2E3A)),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
|
if (profileShowAvatar) {
|
||||||
Text(
|
Text(
|
||||||
avatarEmoji(profileAvatarId ?: "fox"),
|
avatarEmoji(profileAvatarId ?: "fox"),
|
||||||
fontSize = 30.sp,
|
fontSize = 30.sp,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
(profileName?.firstOrNull()?.uppercaseChar() ?: '?').toString(),
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 26.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Column(Modifier.weight(1f)) {
|
Column(Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -321,6 +321,14 @@ fun TopNavBar(
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
) {
|
) {
|
||||||
if (profileShowAvatar) {
|
if (profileShowAvatar) {
|
||||||
|
Box(
|
||||||
|
Modifier.size(22.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
// Emoji altijd als basis; custom foto eroverheen indien beschikbaar
|
||||||
|
if (profileAvatarEmoji != null) {
|
||||||
|
Text(profileAvatarEmoji, fontSize = 15.sp)
|
||||||
|
}
|
||||||
if (!profileAvatarUrl.isNullOrBlank()) {
|
if (!profileAvatarUrl.isNullOrBlank()) {
|
||||||
val ctx = LocalContext.current
|
val ctx = LocalContext.current
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
|
|
@ -338,8 +346,7 @@ fun TopNavBar(
|
||||||
.clip(RoundedCornerShape(50)),
|
.clip(RoundedCornerShape(50)),
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
)
|
)
|
||||||
} else if (profileAvatarEmoji != null) {
|
}
|
||||||
Text(profileAvatarEmoji, fontSize = 15.sp)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!profileLabel.isNullOrBlank()) {
|
if (!profileLabel.isNullOrBlank()) {
|
||||||
|
|
|
||||||
|
|
@ -252,7 +252,7 @@ export class ViewerService {
|
||||||
}
|
}
|
||||||
patch.passwordHash = await hashPassword(data.password);
|
patch.passwordHash = await hashPassword(data.password);
|
||||||
}
|
}
|
||||||
return prisma.viewerUser.update({
|
const updated = await prisma.viewerUser.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: patch,
|
data: patch,
|
||||||
select: {
|
select: {
|
||||||
|
|
@ -266,6 +266,10 @@ export class ViewerService {
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (patch.passwordHash) {
|
||||||
|
await this.clearLoginLock(updated.email.toLowerCase());
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
async revokeDevice(deviceId: string) {
|
async revokeDevice(deviceId: string) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue