3 Commits

Author SHA1 Message Date
schalli 87ba56db93 docs(changelog): Version 1.10.1 abgeschlossen
Tessera CI/CD / Tests (push) Successful in 2m2s
Tessera CI/CD / Lint & Type Check (push) Successful in 49s
Tessera CI/CD / Build & Publish Images (push) Blocked by required conditions
Tessera CI/CD / Desktop-Pakete bauen (push) Has started running
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 13:03:16 +02:00
schalli 6ccab8330c fix(web): Favoriten-Platzhalterbuchstabe verschwindet, sobald das Logo geladen ist
Tessera CI/CD / Lint & Type Check (push) Successful in 59s
Tessera CI/CD / Tests (push) Successful in 1m40s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 13m15s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m51s
Bei Logos mit durchsichtigem Hintergrund schien der Buchstabe durch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 11:53:18 +02:00
schalli ba4c4d52b3 fix(desktop): Linux-AppImage oeffnet Links ueber das xdg-open des Systems
tauri-plugin-opener rief das xdg-open aus dem AppImage mit dessen Umgebung
(LD_LIBRARY_PATH, GTK_PATH, GIO_EXTRA_MODULES, GDK_BACKEND ...) auf; das
Oeffnen-Programm der Arbeitsoberflaeche erbt das. Im AppImage jetzt das
System-xdg-open mit bereinigter Umgebung, sonst wie bisher der Opener.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 11:52:10 +02:00
4 changed files with 201 additions and 15 deletions
+7
View File
@@ -4,6 +4,13 @@ Diese Liste beschreibt in einfachen Worten, was sich von Version zu Version an T
## Unveröffentlicht
## 1.10.1 – 2026-10-06
### Behoben
- Favoriten: Bei Logos mit durchsichtigem Hintergrund schien der graue Platzhalter-Buchstabe durch. Er verschwindet jetzt, sobald das Logo geladen ist.
- Linux-App: Links, die in einem neuen Fenster aufgehen (zum Beispiel aus Notizen oder Favoriten), übergibt die App jetzt an das Öffnen-Programm Ihres Systems statt an eine mitgebrachte Kopie. Auf manchen Systemen öffnete sich dadurch vorher kein Browser.
## 1.10.0 – 2026-10-06
### Neu
+153 -6
View File
@@ -776,7 +776,7 @@ fn spawn_version_check(app: AppHandle, server_url: String, install_after: bool)
fn open_download_page(app: &AppHandle) {
if let Some(server) = stored_server_url(app) {
let target = format!("{}/settings/general/desktop", server.trim_end_matches('/'));
let _ = app.opener().open_url(target, None::<&str>);
open_in_system_browser(app, target);
}
}
@@ -940,6 +940,111 @@ fn get_client_info() -> String {
/// Ziel fuer den System-Browser, wenn die Seite ein neues Fenster verlangt
/// (`window.open`, Links mit `target="_blank"`). Nur http/https -- andere
/// Schemata (file:, javascript:, eigene Protokolle) werden verworfen.
/// Oeffnet eine Adresse im System-Browser.
///
/// Linux-AppImage (06.10.2026, Link aus der Notiz-Kachel oeffnete auf
/// EndeavourOS nichts): `tauri-plugin-opener` ruft dort das `xdg-open`
/// AUS dem AppImage auf, und zwar mit der Umgebung des AppImage
/// (`LD_LIBRARY_PATH` auf die mitgebrachten Debian-Bibliotheken, `GTK_PATH`,
/// `GIO_EXTRA_MODULES`, `GDK_BACKEND=x11` ...). Das Oeffnen-Programm der
/// Arbeitsoberflaeche bzw. der Browser erbt das und kann still scheitern.
/// Nachgewiesen per strace im Container: Aufruf von
/// `$APPDIR/usr/bin/xdg-open` mit 33 geerbten Variablen. Deshalb hier im
/// AppImage das `xdg-open` des SYSTEMS mit bereinigter Umgebung; gelingt
/// das nicht (kein System-`xdg-open`), wie bisher ueber den Opener.
fn open_in_system_browser(app: &AppHandle, url: String) {
#[cfg(target_os = "linux")]
{
if appimage_system_open(&url) {
return;
}
}
let _ = app.opener().open_url(url, None::<&str>);
}
/// Variablen, die der AppRun-Hook von linuxdeploy (plugin-gtk) bzw. die
/// AppImage-Laufzeit setzt -- fuer Programme ausserhalb der App falsch.
#[cfg(target_os = "linux")]
const APPIMAGE_ONLY_VARS: &[&str] = &[
"LD_LIBRARY_PATH",
"LD_PRELOAD",
"GTK_DATA_PREFIX",
"GTK_THEME",
"GTK_EXE_PREFIX",
"GTK_PATH",
"GTK_IM_MODULE_FILE",
"GDK_PIXBUF_MODULE_FILE",
"GDK_BACKEND",
"GIO_EXTRA_MODULES",
"GSETTINGS_SCHEMA_DIR",
"GST_PLUGIN_PATH",
"GST_PLUGIN_SYSTEM_PATH",
"GST_PLUGIN_SYSTEM_PATH_1_0",
"GST_PLUGIN_SCANNER",
"APPDIR",
"APPIMAGE",
"ARGV0",
"OWD",
];
/// Bereinigte Umgebung fuer ein Programm ausserhalb des AppImage: die
/// AppImage-eigenen Variablen fallen weg, aus Pfadlisten (PATH,
/// XDG_DATA_DIRS ...) verschwinden alle Eintraege unterhalb von `appdir`.
/// Rein, damit testbar.
#[cfg(target_os = "linux")]
fn cleaned_env(
vars: impl Iterator<Item = (String, String)>,
appdir: &str,
) -> Vec<(String, String)> {
let appdir = appdir.trim_end_matches('/');
vars.filter(|(k, _)| !APPIMAGE_ONLY_VARS.contains(&k.as_str()))
.filter_map(|(k, v)| {
if appdir.is_empty() || !v.contains(appdir) {
return Some((k, v));
}
let kept: Vec<&str> = v
.split(':')
.filter(|part| !part.is_empty() && !part.starts_with(appdir))
.collect();
if kept.is_empty() {
None
} else {
Some((k, kept.join(":")))
}
})
.collect()
}
#[cfg(target_os = "linux")]
fn appimage_system_open(url: &str) -> bool {
let Ok(appdir) = std::env::var("APPDIR") else {
return false; // kein AppImage (z. B. Entwicklungsstart): Opener genuegt
};
let Some(bin) = [
"/usr/bin/xdg-open",
"/usr/local/bin/xdg-open",
"/bin/xdg-open",
]
.into_iter()
.find(|p| std::path::Path::new(p).exists()) else {
return false;
};
let mut cmd = std::process::Command::new(bin);
cmd.arg(url)
.env_clear()
.envs(cleaned_env(std::env::vars(), &appdir));
match cmd.spawn() {
Ok(mut child) => {
// Prozess abholen, sonst bleibt er bis zum App-Ende als Zombie.
std::thread::spawn(move || {
let _ = child.wait();
});
true
}
Err(_) => false,
}
}
fn external_target(url: &tauri::Url) -> Option<String> {
match url.scheme() {
"http" | "https" => Some(url.to_string()),
@@ -1047,7 +1152,7 @@ pub fn run() {
let handle = app.handle().clone();
move |url, _features| {
if let Some(target) = external_target(&url) {
let _ = handle.opener().open_url(target, None::<&str>);
open_in_system_browser(&handle, target);
}
tauri::webview::NewWindowResponse::Deny
}
@@ -1062,10 +1167,7 @@ pub fn run() {
if saves_in_app(&url) {
return true;
}
let _ = webview
.app_handle()
.opener()
.open_url(url.to_string(), None::<&str>);
open_in_system_browser(webview.app_handle(), url.to_string());
false
}
tauri::webview::DownloadEvent::Finished { url, path, success } => {
@@ -1591,6 +1693,51 @@ mod tests {
assert_eq!(body, "Die Datei wurde gespeichert.");
}
#[test]
#[cfg(target_os = "linux")]
fn cleaned_env_entfernt_appimage_umgebung() {
let appdir = "/tmp/.mount_TesseraAbc";
let vars = vec![
("HOME".to_string(), "/home/u".to_string()),
("LD_LIBRARY_PATH".to_string(), format!("{appdir}/usr/lib/")),
("GDK_BACKEND".to_string(), "x11".to_string()),
("APPDIR".to_string(), appdir.to_string()),
(
"PATH".to_string(),
format!("{appdir}/usr/bin/:/usr/local/bin:/usr/bin"),
),
(
"XDG_DATA_DIRS".to_string(),
format!("{appdir}/usr/share:/usr/share:/usr/local/share"),
),
(
"GSETTINGS_SCHEMA_DIR".to_string(),
format!("{appdir}//usr/share/glib-2.0/schemas"),
),
("ONLY_APP".to_string(), format!("{appdir}/x")),
];
let env: std::collections::HashMap<_, _> =
cleaned_env(vars.into_iter(), appdir).into_iter().collect();
assert_eq!(env.get("HOME").map(String::as_str), Some("/home/u"));
assert_eq!(
env.get("PATH").map(String::as_str),
Some("/usr/local/bin:/usr/bin")
);
assert_eq!(
env.get("XDG_DATA_DIRS").map(String::as_str),
Some("/usr/share:/usr/local/share")
);
for gone in [
"LD_LIBRARY_PATH",
"GDK_BACKEND",
"APPDIR",
"GSETTINGS_SCHEMA_DIR",
"ONLY_APP",
] {
assert!(!env.contains_key(gone), "{gone}");
}
}
#[test]
fn external_target_nur_http_und_https() {
let https = tauri::Url::parse("https://www.google.com/search?q=tessera").unwrap();
@@ -414,6 +414,25 @@ describe('FavoritesWidget', () => {
expect(screen.getByTestId('letter-fallback-fav-id-1')).toHaveTextContent('G');
});
it('06.10.: geladenes Logo blendet den Buchstaben aus (durchsichtige Logos), Fehler zeigt ihn wieder', async () => {
render(<FavoritesWidget instanceId="fav-1" config={{}} isEditMode={false} />);
await waitFor(() => {
expect(screen.getByText('GitHub')).toBeInTheDocument();
});
const proxyImg = screen.getByTestId('icon-proxy-fav-id-1') as HTMLImageElement;
expect(screen.getByTestId('letter-fallback-fav-id-1')).toBeInTheDocument();
act(() => {
fireEvent.load(proxyImg);
});
expect(screen.queryByTestId('letter-fallback-fav-id-1')).not.toBeInTheDocument();
act(() => {
fireEvent.error(proxyImg);
});
expect(screen.getByTestId('letter-fallback-fav-id-1')).toHaveTextContent('G');
});
it('ausdrueckliche Symbol-Adresse (260929-lh3): Proxy -> Browser laedt iconUrl direkt (no-referrer) -> Origin-Favicon -> Buchstabe', async () => {
mockFetch.mockResolvedValue([
{
@@ -543,7 +543,9 @@ function getDirectCandidates(iconUrl: string | null, url: string): string[] {
* Browser aber das Bild), dann `{origin}/favicon.ico` (entfaellt, wenn
* identisch). Erreicht auch interne Hosts, die der SSRF-Schutz des
* Servers absichtlich ablehnt.
* 3. `none` — der Buchstaben-Platzhalter liegt IMMER darunter.
* 3. `none` — der Buchstaben-Platzhalter steht da, solange kein Bild
* geladen ist. Sobald ein Bild geladen ist, verschwindet er: bei Logos mit
* durchsichtigem Hintergrund schien er sonst durch (06.10.2026).
*
* Bewusst KEIN Drittanbieter-Favicon-Dienst: der wuerde Hostnamen nach
* aussen geben und interne Hosts ohnehin nicht kennen.
@@ -590,15 +592,18 @@ function FavoriteIcon({
// 'proxy' | Index in directSrcs | Ende der Kette (Buchstabe).
const [stage, setStage] = useState<'proxy' | number>(proxySrc ? 'proxy' : 0);
const directSrc = typeof stage === 'number' ? (directSrcs[stage] ?? null) : null;
const [imageLoaded, setImageLoaded] = useState(false);
return (
<div className={`relative flex shrink-0 items-center justify-center bg-muted ${box}`}>
{!imageLoaded && (
<span
data-testid={`letter-fallback-${fav.id}`}
className={`absolute inset-0 flex items-center justify-center font-semibold text-muted-foreground ${large ? 'text-base' : 'text-xs'}`}
>
{getFallbackLetter(fav.title)}
</span>
)}
{stage === 'proxy' && proxySrc && (
<img
data-testid={`icon-proxy-${fav.id}`}
@@ -609,7 +614,11 @@ function FavoriteIcon({
height={px}
loading="lazy"
className={`relative rounded-sm ${img}`}
onError={() => setStage(0)}
onLoad={() => setImageLoaded(true)}
onError={() => {
setImageLoaded(false);
setStage(0);
}}
/>
)}
{directSrc && (
@@ -623,7 +632,11 @@ function FavoriteIcon({
loading="lazy"
referrerPolicy="no-referrer"
className={`relative rounded-sm ${img}`}
onError={() => setStage((prev) => (typeof prev === 'number' ? prev + 1 : 0))}
onLoad={() => setImageLoaded(true)}
onError={() => {
setImageLoaded(false);
setStage((prev) => (typeof prev === 'number' ? prev + 1 : 0));
}}
/>
)}
</div>