diff --git a/src/backend/backend.js b/src/backend/backend.js
index 68bf26e..2b9ac3a 100644
--- a/src/backend/backend.js
+++ b/src/backend/backend.js
@@ -8487,7 +8487,7 @@ router
profile: true,
ux: true,
workflow: isLoopbackRequest(ctx),
- theme: process.env.OASIS_MOBILE !== '1',
+ theme: true, // en el movil ofrece los skins de movil (WIZARD_THEMES)
community: checkMod(ctx, 'invitesMod'),
greeting: checkMod(ctx, 'feedMod')
};
@@ -14669,6 +14669,8 @@ router
})
.post("/settings/theme", koaBody(), async (ctx) => {
const theme = String(ctx.request.body.theme || "").trim(), cfg = getConfig();
+ // en el movil solo los skins de movil: un tema de escritorio deja la app sin maqueta
+ if (process.env.OASIS_MOBILE === '1' && !require('../views/main_views').MOBILE_THEMES.some(([value]) => value === theme)) { ctx.redirect("/settings#theme"); return; }
cfg.themes.current = theme || "Dark-SNH";
saveConfig(cfg);
ctx.cookies.set("theme", cfg.themes.current, { httpOnly: true, sameSite: 'strict', secure: ctx.secure });
diff --git a/src/client/assets/images/theme-acampada.png b/src/client/assets/images/theme-acampada.png
new file mode 100644
index 0000000..c635277
Binary files /dev/null and b/src/client/assets/images/theme-acampada.png differ
diff --git a/src/client/assets/images/theme-oasismobile.png b/src/client/assets/images/theme-oasismobile.png
new file mode 100644
index 0000000..2f9bfaf
Binary files /dev/null and b/src/client/assets/images/theme-oasismobile.png differ
diff --git a/src/client/assets/themes/Acampada.css b/src/client/assets/themes/Acampada.css
index ddc297f..cf40f26 100644
--- a/src/client/assets/themes/Acampada.css
+++ b/src/client/assets/themes/Acampada.css
@@ -3,12 +3,11 @@
* Los colores y las formas salen de acampada26s.net: bordes negros gruesos,
* botones con sombra solida que se hunden al pulsar y titulos en mayusculas.
*
- * La maqueta de movil es la de OasisMobile.css de upstream, que se importa entera;
- * aqui solo se cambian colores, letra y formas encima. Por eso casi todo lleva
+ * La maqueta de movil es la de OasisMobile.css, que main_views.js carga justo antes
+ * con su propio versionado (no con @import: se quedaba en la cache del
+ * WebView); aqui solo se cambian colores, letra y formas encima. Por eso casi todo lleva
* !important: OasisMobile.css tambien lo usa. Las piezas propias de la rama
* (panal, barras) se ajustan en styles/hive_acampada.css. */
-@import url("OasisMobile.css");
-
@font-face {
font-family: "Anton";
src: url("../fonts/Anton-Regular.ttf") format("truetype");
diff --git a/src/client/assets/themes/OasisMobile.css b/src/client/assets/themes/OasisMobile.css
index c7172c6..1c94839 100644
--- a/src/client/assets/themes/OasisMobile.css
+++ b/src/client/assets/themes/OasisMobile.css
@@ -1258,3 +1258,24 @@ body:has(.oasis-nav-side-toggle) { padding-top: 84px !important; }
align-items: center !important;
justify-content: center !important;
}
+
+/* Skins y workflows en Ajustes (views/settings_view.js): tarjetas de un toque */
+.skin-pick { display: grid !important; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; margin: 0; }
+.skin-card {
+ display: flex !important; flex-direction: column; align-items: stretch; gap: 8px;
+ width: 100% !important; height: auto !important; min-height: 0 !important; margin: 0 !important; padding: 8px !important;
+ background: #1c1c1c !important; color: #e6e6e6 !important;
+ border: 1px solid #444 !important; border-radius: 16px !important; cursor: pointer;
+}
+.skin-card .skin-shot { width: 100%; aspect-ratio: 3 / 4; object-fit: cover; object-position: top; border-radius: 10px; display: block; }
+.skin-card .skin-name { font-size: 17px; font-weight: 700; text-align: center; }
+.skin-card.is-current { border: 2px solid #FFD700 !important; background: #2a2412 !important; }
+.skin-card.is-current .skin-name::after { content: " \2713"; }
+.workflow-pick { display: flex !important; flex-direction: column; gap: 10px; margin: 0; }
+.workflow-card {
+ width: 100% !important; min-height: 56px !important; margin: 0 !important; padding: 10px 16px !important;
+ font-size: 18px !important; text-align: left !important;
+ background: #1c1c1c !important; color: #e6e6e6 !important; border: 1px solid #444 !important; border-radius: 14px !important;
+}
+.workflow-card.is-current { border: 2px solid #FFD700 !important; background: #2a2412 !important; color: #FFD700 !important; }
+.workflow-card.is-current::after { content: " \2713"; }
diff --git a/src/client/assets/translations/i18n_fork.js b/src/client/assets/translations/i18n_fork.js
index 6eb8379..6baf961 100644
--- a/src/client/assets/translations/i18n_fork.js
+++ b/src/client/assets/translations/i18n_fork.js
@@ -17,6 +17,9 @@
module.exports = {
en: {
+ // skins y workflows en el movil (settings_view.js)
+ skinIntroMobile: "Pick how Oasis looks on your phone. It applies as soon as you tap it.",
+ workflowsDescriptionMobile: "A workflow sets which modules are on. On the phone your skin stays as it is.",
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "This image could not be cleaned of its metadata (location, camera…), so it has not been uploaded. Save it as JPEG or PNG and try again.",
// Karvan
@@ -82,6 +85,9 @@ module.exports = {
spreadMore: "more"
},
es: {
+ // skins y workflows en el movil (settings_view.js)
+ skinIntroMobile: "Elige cómo se ve Oasis en tu móvil. Se aplica al tocarlo.",
+ workflowsDescriptionMobile: "Un workflow fija qué módulos están activos. En el móvil tu skin se queda como está.",
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "No se han podido quitar los metadatos de esta imagen (ubicación, cámara…), así que no se ha subido. Guárdala como JPEG o PNG y vuelve a intentarlo.",
// Karvan
@@ -153,6 +159,9 @@ module.exports = {
totalspreads: "Réplicas totales"
},
fr: {
+ // skins y workflows en el movil (settings_view.js)
+ skinIntroMobile: "Choisis l'apparence d'Oasis sur ton téléphone. Elle s'applique dès que tu touches.",
+ workflowsDescriptionMobile: "Un workflow fixe les modules actifs. Sur le téléphone, ton thème ne change pas.",
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "Impossible de retirer les métadonnées de cette image (position, appareil…), elle n'a donc pas été envoyée. Enregistrez-la en JPEG ou PNG et réessayez.",
// Karvan
@@ -218,6 +227,9 @@ module.exports = {
spreadMore: "plus"
},
eu: {
+ // skins y workflows en el movil (settings_view.js)
+ skinIntroMobile: "Aukeratu nolakoa den Oasis zure mugikorrean. Ukitu bezain laster aplikatzen da.",
+ workflowsDescriptionMobile: "Workflow batek zein modulu dauden aktibo finkatzen du. Mugikorrean zure itxura ez da aldatzen.",
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "Ezin izan dira irudi honen metadatuak kendu (kokapena, kamera…), beraz ez da igo. Gorde JPEG edo PNG gisa eta saiatu berriro.",
// Karvan
@@ -283,6 +295,9 @@ module.exports = {
spreadMore: "gehiago"
},
de: {
+ // skins y workflows en el movil (settings_view.js)
+ skinIntroMobile: "Wähle, wie Oasis auf deinem Handy aussieht. Es wird beim Antippen übernommen.",
+ workflowsDescriptionMobile: "Ein Workflow legt fest, welche Module aktiv sind. Auf dem Handy bleibt dein Skin, wie er ist.",
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "Die Metadaten dieses Bildes (Standort, Kamera…) konnten nicht entfernt werden, daher wurde es nicht hochgeladen. Speichere es als JPEG oder PNG und versuche es erneut.",
// Karvan
@@ -348,6 +363,9 @@ module.exports = {
spreadMore: "mehr"
},
it: {
+ // skins y workflows en el movil (settings_view.js)
+ skinIntroMobile: "Scegli come appare Oasis sul tuo telefono. Si applica appena lo tocchi.",
+ workflowsDescriptionMobile: "Un workflow stabilisce quali moduli sono attivi. Sul telefono la tua skin resta com'è.",
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "Non è stato possibile rimuovere i metadati di questa immagine (posizione, fotocamera…), quindi non è stata caricata. Salvala come JPEG o PNG e riprova.",
// Karvan
@@ -413,6 +431,9 @@ module.exports = {
spreadMore: "altro"
},
pt: {
+ // skins y workflows en el movil (settings_view.js)
+ skinIntroMobile: "Escolhe como o Oasis aparece no teu telemóvel. Aplica-se ao tocar.",
+ workflowsDescriptionMobile: "Um workflow define que módulos estão ativos. No telemóvel o teu tema fica como está.",
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "Não foi possível remover os metadados desta imagem (localização, câmara…), por isso não foi enviada. Guarda-a como JPEG ou PNG e tenta de novo.",
// Karvan
@@ -478,6 +499,9 @@ module.exports = {
spreadMore: "mais"
},
zh: {
+ // skins y workflows en el movil (settings_view.js)
+ skinIntroMobile: "选择 Oasis 在手机上的外观。点一下就会生效。",
+ workflowsDescriptionMobile: "工作流决定启用哪些模块。在手机上,你的外观保持不变。",
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "无法清除这张图片的元数据(位置、相机等),因此没有上传。请另存为 JPEG 或 PNG 后重试。",
// Karvan
@@ -543,6 +567,9 @@ module.exports = {
spreadMore: "更多"
},
ar: {
+ // skins y workflows en el movil (settings_view.js)
+ skinIntroMobile: "اختر مظهر Oasis على هاتفك. يُطبَّق بمجرد اللمس.",
+ workflowsDescriptionMobile: "يحدد سير العمل الوحدات المفعّلة. على الهاتف يبقى مظهرك كما هو.",
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "تعذّر إزالة البيانات الوصفية من هذه الصورة (الموقع، الكاميرا…)، لذلك لم تُرفع. احفظها بصيغة JPEG أو PNG وحاول مجددًا.",
// Karvan
@@ -608,6 +635,9 @@ module.exports = {
spreadMore: "المزيد"
},
hi: {
+ // skins y workflows en el movil (settings_view.js)
+ skinIntroMobile: "चुनें कि आपके फ़ोन पर Oasis कैसा दिखे। छूते ही लागू हो जाता है।",
+ workflowsDescriptionMobile: "वर्कफ़्लो तय करता है कि कौन-से मॉड्यूल चालू हैं। फ़ोन पर आपकी स्किन वैसी ही रहती है।",
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "इस छवि का मेटाडेटा (स्थान, कैमरा…) हटाया नहीं जा सका, इसलिए इसे अपलोड नहीं किया गया। इसे JPEG या PNG के रूप में सहेजें और फिर कोशिश करें।",
// Karvan
@@ -673,6 +703,9 @@ module.exports = {
spreadMore: "और"
},
ru: {
+ // skins y workflows en el movil (settings_view.js)
+ skinIntroMobile: "Выберите, как Oasis выглядит на телефоне. Применяется сразу по нажатию.",
+ workflowsDescriptionMobile: "Рабочий процесс задаёт, какие модули включены. На телефоне ваше оформление не меняется.",
// fotos que no se pueden limpiar (blobHandler.js)
imageMetadataUnsafe: "Не удалось удалить метаданные этого изображения (местоположение, камера…), поэтому оно не загружено. Сохраните его как JPEG или PNG и попробуйте снова.",
// Karvan
diff --git a/src/models/workflows_model.js b/src/models/workflows_model.js
index 947e8fd..85a1230 100644
--- a/src/models/workflows_model.js
+++ b/src/models/workflows_model.js
@@ -89,10 +89,14 @@ const WORKFLOWS = [
const WORKFLOW_KEYS = WORKFLOWS.map(w => w.key);
const isMobile = () => process.env.OASIS_MOBILE === '1';
-// en el movil todos los workflows llevan el tema Acampada; quien venga de OasisMobile
-// sigue reconociendo su workflow (Acampada es OasisMobile con otros colores)
+// en el movil el workflow no toca el skin: se queda el que haya elegido cada cual
+// (Acampada u OasisMobile); si no hay ninguno de movil, Acampada
const MOBILE_THEMES = ['Acampada', 'OasisMobile'];
-const themeOf = (workflow) => (isMobile() ? 'Acampada' : workflow.theme);
+const mobileSkin = (cfg) => {
+ const cur = cfg && cfg.themes && cfg.themes.current;
+ return MOBILE_THEMES.includes(cur) ? cur : 'Acampada';
+};
+const themeOf = (workflow, cfg) => (isMobile() ? mobileSkin(cfg) : workflow.theme);
const sameTheme = (theme, workflow) => (isMobile() ? MOBILE_THEMES.includes(theme) : theme === workflow.theme);
const getWorkflow = (key) => WORKFLOWS.find(w => w.key === String(key || '')) || null;
@@ -117,7 +121,7 @@ const applyWorkflow = (cfg, workflow) => {
const enabled = new Set(modulesOf(workflow));
cfg.modules = cfg.modules || {};
ALL_MODULES.forEach(mod => { cfg.modules[`${mod}Mod`] = enabled.has(mod) ? 'on' : 'off'; });
- if (workflow.theme) cfg.themes = { ...(cfg.themes || {}), current: themeOf(workflow) };
+ if (workflow.theme) cfg.themes = { ...(cfg.themes || {}), current: themeOf(workflow, cfg) };
if (workflow.homePage) cfg.homePage = workflow.homePage;
return cfg;
};
diff --git a/src/views/main_views.js b/src/views/main_views.js
index 2ab5424..faf4d70 100644
--- a/src/views/main_views.js
+++ b/src/views/main_views.js
@@ -438,9 +438,17 @@ exports.CONTENT_SPREADABLE = CONTENT_SPREADABLE;
exports.CONTENT_TORRENTABLE = CONTENT_TORRENTABLE;
exports.contentDeleteAction = contentDeleteAction;
exports.THEMES = [['Acampada', 'Acampada'], ['Dark-SNH', 'Dark-SNH'], ['Clear-SNH', 'Clear-SNH'], ['Purple-SNH', 'Purple-SNH'], ['Matrix-SNH', 'Matrix-SNH'], ['OasisMobile', 'Oasis-Mobile']];
-// Acampada importa la maqueta de OasisMobile.css: los dos son solo de movil
-exports.WIZARD_THEMES = exports.THEMES.filter(([value]) => value !== 'OasisMobile' && value !== 'Acampada');
-// con el tema Acampada, hive_acampada.css va detras del tema: pinta lanzador, barras y tarjetas
+// Acampada importa la maqueta de OasisMobile.css: los dos son solo de movil, y en el
+// movil son los unicos que valen (los de escritorio no traen la maqueta)
+exports.MOBILE_THEMES = [['Acampada', 'Acampada'], ['OasisMobile', 'Oasis Mobile']];
+exports.WIZARD_THEMES = process.env.OASIS_MOBILE === '1'
+ ? exports.MOBILE_THEMES
+ : exports.THEMES.filter(([value]) => value !== 'OasisMobile' && value !== 'Acampada');
+// el tema Acampada es la maqueta de OasisMobile.css con otros colores. OasisMobile.css va
+// en su propio versionado y no como @import dentro de Acampada.css: un @import sin
+// ?v= se queda en la cache del WebView y tras actualizar se veia la maqueta vieja.
+// hive_acampada.css va detras del tema: pinta lanzador, barras y tarjetas.
+const acampadaBase = (theme) => theme === 'Acampada' ? link({ rel: "stylesheet", href: `/assets/themes/OasisMobile.css?v=${assetVersion()}` }) : null;
const acampadaLink = (theme) => theme === 'Acampada' ? link({ rel: "stylesheet", href: `/assets/styles/hive_acampada.css?v=${assetVersion()}` }) : null;
const renderDocumentActions = (kind, id, leadingNodes = []) => {
@@ -2006,7 +2014,7 @@ const template = (titlePrefix, ...elements) => {
// movil: el tema va DESPUES de mobile.css para ganar por cascada sin !important;
// escritorio: orden original de epsylon
...(process.env.OASIS_MOBILE === '1'
- ? [ link({ rel: "stylesheet", href: `/assets/styles/mobile.css?v=${assetVersion()}` }), themeLink, acampadaLink(theme) ]
+ ? [ link({ rel: "stylesheet", href: `/assets/styles/mobile.css?v=${assetVersion()}` }), acampadaBase(theme), themeLink, acampadaLink(theme) ]
: [ themeLink, link({ rel: "stylesheet", href: `/assets/styles/mobile.css?v=${assetVersion()}`, media: "(max-width: 768px)" }) ]),
// el modulo Karvan trae su propia hoja: asi se ve igual en cualquier tema
// y tambien en escritorio, donde no se carga OasisMobile.css
@@ -2395,7 +2403,7 @@ exports.ainavHomeView = ({ recentTags = [] } = {}) => {
// movil: el tema va DESPUES de mobile.css para ganar por cascada sin !important;
// escritorio: orden original de epsylon
...(process.env.OASIS_MOBILE === '1'
- ? [ link({ rel: "stylesheet", href: `/assets/styles/mobile.css?v=${assetVersion()}` }), link({ rel: "stylesheet", href: `/assets/themes/${theme}.css?v=${assetVersion()}` }), acampadaLink(theme) ]
+ ? [ link({ rel: "stylesheet", href: `/assets/styles/mobile.css?v=${assetVersion()}` }), acampadaBase(theme), link({ rel: "stylesheet", href: `/assets/themes/${theme}.css?v=${assetVersion()}` }), acampadaLink(theme) ]
: [ link({ rel: "stylesheet", href: `/assets/themes/${theme}.css?v=${assetVersion()}` }), link({ rel: "stylesheet", href: `/assets/styles/mobile.css?v=${assetVersion()}`, media: "(max-width: 768px)" }) ]),
// el modulo Karvan trae su propia hoja: asi se ve igual en cualquier tema
// y tambien en escritorio, donde no se carga OasisMobile.css
diff --git a/src/views/settings_view.js b/src/views/settings_view.js
index df412f4..ded6bc8 100644
--- a/src/views/settings_view.js
+++ b/src/views/settings_view.js
@@ -2,7 +2,7 @@ const { form, button, div, h2, h3, p, section, select, option, input, br, a, lab
const fs = require('fs');
const path = require('path');
const { getConfig } = require('../configs/config-manager.js');
-const { template, selectedLanguage, i18n, setLanguage, INBOX_BOT_ORDER, inboxBotLabel, THEMES, userLink } = require('./main_views');
+const { template, selectedLanguage, i18n, setLanguage, INBOX_BOT_ORDER, inboxBotLabel, THEMES, MOBILE_THEMES, userLink } = require('./main_views');
const i18nBase = require("../client/assets/translations/i18n");
const moment = require("../server/node_modules/moment");
const { WORKFLOWS, currentWorkflow } = require('../models/workflows_model');
@@ -58,7 +58,26 @@ const settingsView = (params) => {
const modOn = (name) => (currentConfig.modules || {})[`${name}Mod`] === 'on';
const isMobile = process.env.OASIS_MOBILE === '1';
- const workflowSection = section({ id: "workflows" },
+ // movil: skins y workflows como tarjetas que se aplican al tocarlas (un boton
+ // de envio por opcion); el desplegable de escritorio quedaba enterrado y dejaba
+ // poner temas de escritorio, que en el movil se quedan sin maqueta
+ const skinPicker = () => section({ id: "theme" },
+ div({ class: "tags-header" }, h2(i18n.theme), p(i18n.skinIntroMobile)),
+ form({ action: "/settings/theme", method: "post", class: "skin-pick" },
+ ...MOBILE_THEMES.map(([value, name]) => button({ type: "submit", name: "theme", value, class: theme === value ? "skin-card is-current" : "skin-card" },
+ img({ src: `/assets/images/theme-${value.toLowerCase()}.png`, alt: "", class: "skin-shot" }),
+ span({ class: "skin-name" }, name)))
+ )
+ );
+ const workflowPicker = () => section({ id: "workflows" },
+ div({ class: "tags-header" }, h2(i18n.workflowsTitle), p(i18n.workflowsDescriptionMobile)),
+ form({ action: "/settings/workflow", method: "POST", class: "workflow-pick" },
+ ...WORKFLOWS.map(w => button({ type: "submit", name: "workflow", value: w.key, class: activeWorkflow === w.key ? "workflow-card is-current" : "workflow-card" },
+ i18n[`workflow_${w.key}`] || w.key))
+ )
+ );
+
+ const workflowSection = isMobile ? workflowPicker() : section({ id: "workflows" },
div({ class: "tags-header" },
h2(i18n.workflowsTitle),
p(i18n.workflowsDescription),
@@ -181,7 +200,7 @@ const settingsView = (params) => {
)
)
),
- section({ id: "theme" },
+ isMobile ? skinPicker() : section({ id: "theme" },
div({ class: "tags-header" },
h2(i18n.theme),
p(i18n.themeIntro),