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),